It started with an SSH alert I've seen a thousand times:
sshd: Invalid user taow from 203.0.113.239 port 50612
Then again. And again. Dozens of times, from a different IP almost every time, but always inside the same /24.
Was it dangerous? No. That server only accepts SSH keys, and I double-checked from another box:
$ ssh user@server
user@server: Permission denied (publickey).
No password prompt, nothing to guess. The bot was trying usernames against a door that doesn't have a keyhole.
But it was still noise: alerts piling up in the SIEM, auth logs filling up, and CPU spent rejecting the same junk over and over. When the real alert eventually arrives, I want to see it, not dig for it under 500 copies of Invalid user oracle.
Why the usual quick fixes didn't feel right
Banning single IPs is pointless against a bot that rotates through a whole range.
ufw deny from 203.0.113.0/24 looks right but often does nothing. UFW appends the rule after your existing allow 22, so the allow matches first and the deny never fires. You need ufw insert 1 ..., and you have to remember that every time.
A raw iptables -I rule works, but then it lives there forever. Six months later, someone finds a mystery DROP rule with no comment, no date and no owner, and nobody dares delete it.
What I actually wanted:
- ban a whole subnet, not an IP
- have it expire by itself (most of these bots move on after a day or two)
- optionally make it permanent when a range keeps coming back
- not touch the existing firewall setup (UFW, firewalld, whatever's there)
- leave a readable trail for the next admin
So I wrote a small tool for it: netban.
phaedonv
/
netban
netban is a single Bash script for blocking whole networks (for example a /24) on a Linux host, either for a fixed time or permanently.
Manual IPv4 network bans for Linux, built on nftables.
Temporary bans that expire on their own. Permanent bans that survive reboots
netban is a single Bash script for blocking whole networks (for example a
/24) on a Linux host, either for a fixed time or permanently. It's meant as
a manual fallback next to automated defences such as Wazuh active
response, Suricata, fail2ban or CrowdSec: you use it when those miss something
or when you need to stop traffic right now without editing firewall configs.
$ netban 203.0.113.57/24
banned 203.0.113.0/24 for 24h
Why
A typical case: a server exposes SSH with key-only auth, and a bot keeps hammering it from a rotating pool of IPs inside one subnet:
sshd: Invalid user taow from 203.0.113.239 port 50612
sshd: Invalid user admin from 203.0.113.12 port 41877
sshd: Invalid user oracle from 203.0.113.190 port 39020
It isn't a real threat, because passwords…
What it looks like
$ netban 203.0.113.239/24
banned 203.0.113.0/24 for 24h
You can paste the IP straight from the alert with /24 on the end. The host bits get masked, so it bans the network.
Need a different duration, or a permanent ban with a note for future you?
netban 203.0.113.0/24 6h
netban 198.51.100.0/24 perm "returns daily, ssh user enum"
Or run it with no arguments for an interactive prompt, which is handy when you're triaging a few ranges at once:
$ netban
netban> 203.0.113.0/24
banned 203.0.113.0/24 for 24h
netban> 198.51.100.7/24 2d
banned 198.51.100.0/24 for 2d
netban> list
netban> quit
And to check it's actually doing something:
$ netban -l
--- temporary ---
elements = { 203.0.113.0/24 timeout 1d expires 23h41m12s }
--- permanent (live) ---
elements = { 198.51.100.0/24 }
--- permanent (/etc/netban/permanent.list) ---
198.51.100.0/24 # added 2026-10-01 - returns daily, ssh user enum
--- drops ---
@perm counter packets 1843 bytes 110580
@temp counter packets 212 bytes 12720
The counters climb, the log goes quiet, and the SIEM stops shouting.
How it works (it's mostly nftables doing the work)
The whole trick is that nftables already supports sets with per-element timeouts. netban just wraps that in something you can type at 2am without looking anything up.
table inet netban
├─ set perm (interval) permanent networks
├─ set temp (interval, timeout) temporary networks, auto-expire
└─ chain input hook input priority -10
ip saddr @perm counter drop
ip saddr @temp counter drop
A few design choices that matter:
Its own table. netban never edits UFW, firewalld or /etc/nftables.conf. Everything lives in inet netban, so removing it is one command and can't break anything else.
Priority -10. UFW and firewalld hook input at priority 0. netban's chain runs before them, so a banned range is dropped no matter what allow rules sit further down. That fixes the "my deny never matched" problem.
The kernel handles expiry. A temporary ban is just a set element with timeout 24h. There's no cron job, no cleanup script and no state to forget about. When the timer runs out, it's gone.
Permanent bans are plain text. They go into /etc/netban/permanent.list with a date and your note, and a small systemd oneshot (netban.service) reloads them at boot. It's also PartOf=nftables.service, so if someone restarts nftables and flushes the ruleset, the permanent bans come straight back.
The core of a temporary ban is really just this:
nft add element inet netban temp '{ 203.0.113.0/24 timeout 24h }'
Everything else is input validation, CIDR masking, and making it pleasant to use.
Install
It's a single Bash script. You need Linux with nft, Bash 4+ and sudo.
curl -fsSLO https://raw.githubusercontent.com/phaedonv/netban/main/netban
sudo bash netban --install
That puts netban in /usr/local/bin, creates /etc/netban/permanent.list, and enables the boot service. Re-running --install updates in place.
What it is not
I want to be upfront about this, because it's a small tool and I'd rather you know its limits:
- It's not automatic. It doesn't watch logs. For automatic per-IP banning, use fail2ban, CrowdSec or Wazuh active response. netban is the manual fallback for when those miss something or you need to act right now.
-
It's not real protection on its own. The actual SSH defence is key-only auth (
PasswordAuthentication no) and limiting who can reach port 22. netban reduces noise and load. - It's IPv4 only. For now.
- It's host-level. If you control the edge router or cloud firewall, blocking there saves bandwidth for every machine behind it.
Where it fits in a typical layered defence:
| Layer | Scope | When |
|---|---|---|
| SIEM / IDS / fail2ban / CrowdSec | automatic, per IP | first line, reacts to alerts |
| Geo blocking (optional) | automatic, per country | broad rotation across many ranges |
| netban | manual, per network, temp or perm | fallback when the above miss it |
Try it, break it, tell me
netban is MIT-licensed and lives here: github.com/phaedonv/netban. It was written at Catalink as part of a growing set of small blue-team tools we use day to day.
If you try it, I'd love to hear:
- How do you handle subnet-rotating bots today? fail2ban with a custom action, CrowdSec, edge firewall, or just ignore them?
- IPv6 support: worth adding, or do you mostly see this noise over v4?
- Anything that broke on your distro. Issues and PRs are very welcome.
If it saved you a few minutes of log-scrolling, a ⭐ on the repo helps other admins find it.
The tool and this post were written with help from an AI assistant. The problem, the servers and the decisions are mine.
Top comments (0)