- Python 100%
| chadnet.py | ||
| README.md | ||
chadnet
Production SSH abuse detection and automatic network dropping by v3ga.
chadnet is a single-file Linux defensive control for internet-facing SSH servers. It monitors authentication logs, scores hostile behavior with weighted rules, persists enforcement state across restarts, escalates repeat offenders, and applies timed network drops through nftables or ipset.
The default backend is nftables.
Capability
- Live SSH monitoring through
systemd-journald. - Historical log ingestion for backfilling enforcement state.
- Weighted detection for OpenSSH and PAM abuse patterns.
- Rolling score windows instead of fragile line counters.
- Repeat-offender escalation with a maximum cap.
- IPv4 and IPv6 enforcement.
- Native
nftablestimed sets. ipsettimed sets for iptables-based hosts.- CIDR allowlists from flags or files.
- Persistent state with active ban metadata.
- Append-only JSON Lines audit events.
- Active blocklist export.
- Manual blocklist import.
- One-address unban.
- Human report output.
- JSON status output for automation.
- Firewall rule installation for the selected backend.
Requirements
Core:
- Linux
- Python 3.10 or newer
systemd-journald- Root privileges for enforcement
For nftables:
nft
For ipset:
ipsetiptablesip6tables
No third-party Python packages are required.
Install
Recommended layout:
sudo install -d -m 0755 /opt/chadnet
sudo install -d -m 0750 /var/lib/chadnet
sudo install -m 0755 chadnet.py /opt/chadnet/chadnet.py
Install enforcement rules for the default nftables backend:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
install-firewall
The default nftables objects are:
table: inet chadnet
chain: input
IPv4 set: chadnet-drop
IPv6 set: chadnet-drop6
For iptables-based systems:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend ipset \
--state-dir /var/lib/chadnet \
install-firewall
The default ipset objects are:
chadnet-drop
chadnet-drop6
Firewall persistence is distribution-specific. Persist the generated nftables, iptables, or ip6tables rules using the standard firewall tooling for your operating system.
Run
Start live monitoring:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
monitor
Backfill from an existing log file and enforce any resulting bans:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
scan /var/log/auth.log
Show status:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
status
Show machine-readable status:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
status --json
Show a human operational report:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
report
Remove one address:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
unban 203.0.113.77
Export active backend drops:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
export /var/lib/chadnet/export.txt
Import an address list:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
--ban-seconds 604800 \
import /root/blocklist.txt --reason external-feed
Clean expired state and rewrite the active file:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
cleanup
Commands
| Command | Purpose |
|---|---|
monitor |
Watch SSH journal units and enforce bans in real time |
scan |
Process an existing log file and enforce matching bans |
status |
Print active drops, state paths, and counters |
report |
Print top rules, top usernames, repeat sources, and totals |
cleanup |
Expire stale state and rewrite the active file |
unban |
Remove one address from enforcement and state |
import |
Import and enforce an address blocklist |
export |
Export active backend drops |
install-firewall |
Create backend sets and install drop rules |
Configuration
Global options must be placed before the command.
| Option | Environment | Default | Purpose |
|---|---|---|---|
--backend |
CHADNET_BACKEND |
nftables |
Enforcement backend: nftables or ipset |
--set-name |
CHADNET_SET |
chadnet-drop |
IPv4 set name; IPv6 appends 6 |
--nft-table |
CHADNET_NFT_TABLE |
chadnet |
nftables table name |
--nft-chain |
CHADNET_NFT_CHAIN |
input |
nftables chain name |
--threshold |
CHADNET_THRESHOLD |
6 |
Score required before a ban |
--window |
CHADNET_WINDOW |
300 |
Rolling score window in seconds |
--suppress |
CHADNET_SUPPRESS |
2 |
Minimum seconds between counted hits per source |
--ban-seconds |
CHADNET_BAN_SECONDS |
259200 |
Initial ban length |
--max-ban-seconds |
CHADNET_MAX_BAN_SECONDS |
2592000 |
Maximum escalated ban length |
--escalation |
CHADNET_ESCALATION |
2.0 |
Repeat-offender multiplier |
--sync-seconds |
CHADNET_SYNC_SECONDS |
300 |
Background state and active-file sync interval |
--state-dir |
CHADNET_STATE_DIR |
. |
Base directory for state files |
--state-file |
CHADNET_STATE_FILE |
chadnet-state.json |
Persistent detector state |
--active-file |
CHADNET_ACTIVE_FILE |
chadnet.txt |
Current active blocklist |
--audit-file |
CHADNET_AUDIT_FILE |
chadnet-events.jsonl |
Audit event stream |
--unit |
ssh, sshd |
Journal unit to monitor; may be repeated | |
--allowlist |
empty | CIDR or address to ignore; may be repeated | |
--allowlist-file |
empty | File containing CIDRs or addresses | |
--quiet |
off | Reduce console output |
Production profile:
sudo /usr/bin/python3 /opt/chadnet/chadnet.py \
--backend nftables \
--state-dir /var/lib/chadnet \
--threshold 6 \
--window 300 \
--ban-seconds 259200 \
--max-ban-seconds 2592000 \
--escalation 2 \
--allowlist 192.0.2.10 \
--allowlist 2001:db8::/32 \
monitor
Detection Model
Each matched SSH log event adds a score to the source address inside the rolling window. The address is banned when its score reaches --threshold.
| Rule | Score |
|---|---|
| Failed password | 2 |
| Invalid user | 3 |
| PAM authentication failure | 2 |
| Closed authenticating pre-auth connection | 2 |
| Closed invalid pre-auth connection | 3 |
| Disconnected invalid user | 3 |
| Missing identification string | 1 |
| Bad protocol identification | 2 |
| Unable to negotiate | 2 |
| Maximum authentication attempts exceeded | 4 |
| Disconnecting invalid user | 3 |
| Reverse mapping failure | 1 |
This model catches credential spraying, brute force, invalid-user enumeration, noisy scanning, and repeated protocol abuse without treating every log line equally.
Repeat sources escalate. A source banned again after returning receives a longer ban according to:
next_ban = min(max_ban_seconds, ban_seconds * escalation ^ previous_bans)
Files
chadnet-state.json
Persistent detector state. It stores rolling hits, active ban metadata, repeat counts, top rule counters, top username counters, and total line/match/ban counters.
chadnet.txt
Current active blocklist, one IP address per line. It is regenerated from backend and state data by monitor, status, scan, and cleanup.
chadnet-events.jsonl
Append-only audit stream. Each line is a compact JSON object with time, action, ip, and details.
Example audit event:
{"action":"ban","details":{"repeat":1,"rule":"maximum-auth","score":9,"seconds":259200,"until":1780970421},"ip":"203.0.113.77","time":1780711221}
systemd
Example unit:
[Unit]
Description=chadnet SSH abuse dropper
Documentation=https://git.nadeko.net/legs/chadnet
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/usr/bin/python3 /opt/chadnet/chadnet.py --backend nftables --state-dir /var/lib/chadnet monitor
Restart=always
RestartSec=5
WorkingDirectory=/var/lib/chadnet
NoNewPrivileges=yes
PrivateTmp=yes
ProtectHome=yes
ProtectSystem=full
ReadWritePaths=/var/lib/chadnet
[Install]
WantedBy=multi-user.target
Install:
sudo install -d -m 0750 /var/lib/chadnet
sudo install -m 0644 chadnet.service /etc/systemd/system/chadnet.service
sudo systemctl daemon-reload
sudo systemctl enable --now chadnet.service
View logs:
journalctl -u chadnet -f
Operations
Allowlists should cover only trusted administrative paths such as management VPNs, office egress addresses, monitoring nodes, and emergency access ranges. Keep them narrow.
Use status --json for automation and external monitoring. Use report during incident review to understand which rules, usernames, and sources are driving enforcement.
Use export before backend migrations. Use import after migrations or when applying a trusted blocklist. Use cleanup after manual firewall changes to refresh state-derived files.
If an administrator is blocked, run unban ADDRESS from a trusted console or out-of-band access path, then add that access path to the allowlist.
Upgrade Path
- Stop the service.
- Preserve
/var/lib/chadnet. - Replace
/opt/chadnet/chadnet.py. - Run
install-firewallif backend names or firewall backend changed. - Start the service.
- Review
statusandreport.
Security Model
chadnet is a reactive network control for SSH abuse. It reduces repeated hostile authentication activity by blocking sources after observed behavior. It does not replace key-only SSH authentication, disabled password login, multi-factor access, patching, least privilege, restricted management networks, or a default-deny firewall policy.
Repository
Publication target:
git.nadeko.net/legs/chadnet
The local source provided for this rework contained attribution to DNSRail dropnet and a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International license notice. If this code remains a derivative of that project, keep the required attribution and license obligations when publishing.