<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Phaedon V</title>
    <description>The latest articles on DEV Community by Phaedon V (@phaedonv).</description>
    <link>https://dev.to/phaedonv</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F894036%2F392252d8-712b-4e2a-b024-4f3b0241decd.png</url>
      <title>DEV Community: Phaedon V</title>
      <link>https://dev.to/phaedonv</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/phaedonv"/>
    <language>en</language>
    <item>
      <title>That SSH bot rotating IPs in your logs? Ban the whole subnet, then let the ban expire</title>
      <dc:creator>Phaedon V</dc:creator>
      <pubDate>Fri, 02 Oct 2026 09:39:38 +0000</pubDate>
      <link>https://dev.to/phaedonv/that-ssh-bot-rotating-ips-in-your-logs-ban-the-whole-subnet-then-let-the-ban-expire-g4i</link>
      <guid>https://dev.to/phaedonv/that-ssh-bot-rotating-ips-in-your-logs-ban-the-whole-subnet-then-let-the-ban-expire-g4i</guid>
      <description>&lt;p&gt;It started with an SSH alert I've seen a thousand times:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sshd: Invalid user taow from 203.0.113.239 port 50612
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Then again. And again. &lt;strong&gt;Dozens of times&lt;/strong&gt;, from a different IP almost every time, but always inside the same &lt;code&gt;/24&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Was it dangerous? No. That server only accepts SSH keys, and I double-checked from another box:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ssh user@server
user@server: Permission denied &lt;span class="o"&gt;(&lt;/span&gt;publickey&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;No password prompt, nothing to guess. The bot was trying usernames against a door that doesn't have a keyhole.&lt;/p&gt;

&lt;p&gt;But it was still &lt;strong&gt;noise&lt;/strong&gt;: 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 &lt;code&gt;Invalid user oracle&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why the usual quick fixes didn't feel right
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Banning single IPs&lt;/strong&gt; is pointless against a bot that rotates through a whole range.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ufw deny from 203.0.113.0/24&lt;/code&gt;&lt;/strong&gt; looks right but often does nothing. UFW &lt;em&gt;appends&lt;/em&gt; the rule after your existing &lt;code&gt;allow 22&lt;/code&gt;, so the allow matches first and the deny never fires. You need &lt;code&gt;ufw insert 1 ...&lt;/code&gt;, and you have to remember that every time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A raw &lt;code&gt;iptables -I&lt;/code&gt; rule&lt;/strong&gt; 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.&lt;/p&gt;

&lt;p&gt;What I actually wanted:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ban a &lt;strong&gt;whole subnet&lt;/strong&gt;, not an IP&lt;/li&gt;
&lt;li&gt;have it &lt;strong&gt;expire by itself&lt;/strong&gt; (most of these bots move on after a day or two)&lt;/li&gt;
&lt;li&gt;optionally make it &lt;strong&gt;permanent&lt;/strong&gt; when a range keeps coming back&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;not touch&lt;/strong&gt; the existing firewall setup (UFW, firewalld, whatever's there)&lt;/li&gt;
&lt;li&gt;leave a &lt;strong&gt;readable trail&lt;/strong&gt; for the next admin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I wrote a small tool for it: &lt;strong&gt;&lt;a href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;netban&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/phaedonv" rel="noopener noreferrer"&gt;
        phaedonv
      &lt;/a&gt; / &lt;a href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;
        netban
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      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. 
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;p&gt;
  &lt;a rel="noopener noreferrer" href="https://github.com/phaedonv/netban/assets/netban-logo.svg"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2Fphaedonv%2Fnetban%2FHEAD%2Fassets%2Fnetban-logo.svg" width="220" alt="netban — subnet IP range ban"&gt;&lt;/a&gt;
&lt;/p&gt;
&lt;p&gt;
  Manual IPv4 network bans for Linux, built on nftables.&lt;br&gt;
  Temporary bans that expire on their own. Permanent bans that survive reboots
&lt;/p&gt;
&lt;p&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/10756c3a40b587a6228871d2860c92bc85e113b1a2c6c7e1e3f39b74f0bc165f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f626173682d342532422d3245463541303f6c6f676f3d676e7562617368266c6f676f436f6c6f723d7768697465"&gt;&lt;img alt="Bash" src="https://camo.githubusercontent.com/10756c3a40b587a6228871d2860c92bc85e113b1a2c6c7e1e3f39b74f0bc165f/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f626173682d342532422d3245463541303f6c6f676f3d676e7562617368266c6f676f436f6c6f723d7768697465"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/b0276388a01b9ea3d075799c0ddcd5d04a95cfc108f2d57f8ea8077adae9aef0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6669726577616c6c2d6e667461626c65732d324546354130"&gt;&lt;img alt="nftables" src="https://camo.githubusercontent.com/b0276388a01b9ea3d075799c0ddcd5d04a95cfc108f2d57f8ea8077adae9aef0/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6669726577616c6c2d6e667461626c65732d324546354130"&gt;&lt;/a&gt;
  &lt;a rel="noopener noreferrer nofollow" href="https://camo.githubusercontent.com/c242fdfe42a6ecc4f06afb4b530f7a4f860563f038d39e41daa2434b06a9ff14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d324546354130"&gt;&lt;img alt="License: MIT" src="https://camo.githubusercontent.com/c242fdfe42a6ecc4f06afb4b530f7a4f860563f038d39e41daa2434b06a9ff14/68747470733a2f2f696d672e736869656c64732e696f2f62616467652f6c6963656e73652d4d49542d324546354130"&gt;&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;netban&lt;/code&gt; is a single Bash script for blocking whole networks (for example a
&lt;code&gt;/24&lt;/code&gt;) on a Linux host, either for a fixed time or permanently. It's meant as
a &lt;strong&gt;manual fallback&lt;/strong&gt; 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 &lt;em&gt;right now&lt;/em&gt; without editing firewall configs.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;$ netban 203.0.113.57/24
banned 203.0.113.0/24 for 24h
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why&lt;/h2&gt;
&lt;/div&gt;

&lt;p&gt;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:&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It isn't a real threat, because passwords…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;h2&gt;
  
  
  What it looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;netban 203.0.113.239/24
&lt;span class="go"&gt;banned 203.0.113.0/24 for 24h
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can paste the IP straight from the alert with &lt;code&gt;/24&lt;/code&gt; on the end. The host bits get masked, so it bans the network.&lt;/p&gt;

&lt;p&gt;Need a different duration, or a permanent ban with a note for future you?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;netban 203.0.113.0/24 6h
netban 198.51.100.0/24 perm &lt;span class="s2"&gt;"returns daily, ssh user enum"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or run it with no arguments for an interactive prompt, which is handy when you're triaging a few ranges at once:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;netban
&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;203.0.113.0/24
&lt;span class="go"&gt;banned 203.0.113.0/24 for 24h
&lt;/span&gt;&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;198.51.100.7/24 2d
&lt;span class="go"&gt;banned 198.51.100.0/24 for 2d
&lt;/span&gt;&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;list
&lt;span class="gp"&gt;netban&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;quit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And to check it's actually doing something:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The counters climb, the log goes quiet, and the SIEM stops shouting.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works (it's mostly nftables doing the work)
&lt;/h2&gt;

&lt;p&gt;The whole trick is that nftables already supports &lt;strong&gt;sets with per-element timeouts&lt;/strong&gt;. netban just wraps that in something you can type at 2am without looking anything up.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few design choices that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Its own table.&lt;/strong&gt; netban never edits UFW, firewalld or &lt;code&gt;/etc/nftables.conf&lt;/code&gt;. Everything lives in &lt;code&gt;inet netban&lt;/code&gt;, so removing it is one command and can't break anything else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Priority -10.&lt;/strong&gt; UFW and firewalld hook &lt;code&gt;input&lt;/code&gt; at priority 0. netban's chain runs &lt;em&gt;before&lt;/em&gt; them, so a banned range is dropped no matter what allow rules sit further down. That fixes the "my deny never matched" problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The kernel handles expiry.&lt;/strong&gt; A temporary ban is just a set element with &lt;code&gt;timeout 24h&lt;/code&gt;. There's no cron job, no cleanup script and no state to forget about. When the timer runs out, it's gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Permanent bans are plain text.&lt;/strong&gt; They go into &lt;code&gt;/etc/netban/permanent.list&lt;/code&gt; with a date and your note, and a small systemd oneshot (&lt;code&gt;netban.service&lt;/code&gt;) reloads them at boot. It's also &lt;code&gt;PartOf=nftables.service&lt;/code&gt;, so if someone restarts nftables and flushes the ruleset, the permanent bans come straight back.&lt;/p&gt;

&lt;p&gt;The core of a temporary ban is really just this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nft add element inet netban temp &lt;span class="s1"&gt;'{ 203.0.113.0/24 timeout 24h }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything else is input validation, CIDR masking, and making it pleasant to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;It's a single Bash script. You need Linux with &lt;code&gt;nft&lt;/code&gt;, Bash 4+ and sudo.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/phaedonv/netban/main/netban
&lt;span class="nb"&gt;sudo &lt;/span&gt;bash netban &lt;span class="nt"&gt;--install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That puts &lt;code&gt;netban&lt;/code&gt; in &lt;code&gt;/usr/local/bin&lt;/code&gt;, creates &lt;code&gt;/etc/netban/permanent.list&lt;/code&gt;, and enables the boot service. Re-running &lt;code&gt;--install&lt;/code&gt; updates in place.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is &lt;em&gt;not&lt;/em&gt;
&lt;/h2&gt;

&lt;p&gt;I want to be upfront about this, because it's a small tool and I'd rather you know its limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It's not automatic.&lt;/strong&gt; It doesn't watch logs. For automatic per-IP banning, use fail2ban, CrowdSec or Wazuh active response. netban is the &lt;strong&gt;manual fallback&lt;/strong&gt; for when those miss something or you need to act right now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's not real protection on its own.&lt;/strong&gt; The actual SSH defence is key-only auth (&lt;code&gt;PasswordAuthentication no&lt;/code&gt;) and limiting who can reach port 22. netban reduces noise and load.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's IPv4 only.&lt;/strong&gt; For now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's host-level.&lt;/strong&gt; If you control the edge router or cloud firewall, blocking there saves bandwidth for every machine behind it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Where it fits in a typical layered defence:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Scope&lt;/th&gt;
&lt;th&gt;When&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SIEM / IDS / fail2ban / CrowdSec&lt;/td&gt;
&lt;td&gt;automatic, per IP&lt;/td&gt;
&lt;td&gt;first line, reacts to alerts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Geo blocking (optional)&lt;/td&gt;
&lt;td&gt;automatic, per country&lt;/td&gt;
&lt;td&gt;broad rotation across many ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;netban&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;manual, per network, temp or perm&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;fallback when the above miss it&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Try it, break it, tell me
&lt;/h2&gt;

&lt;p&gt;netban is MIT-licensed and lives here: &lt;strong&gt;&lt;a href="https://github.com/phaedonv/netban" rel="noopener noreferrer"&gt;github.com/phaedonv/netban&lt;/a&gt;&lt;/strong&gt;. It was written at &lt;a href="https://catalink.eu" rel="noopener noreferrer"&gt;Catalink&lt;/a&gt; as part of a growing set of small blue-team tools we use day to day.&lt;/p&gt;

&lt;p&gt;If you try it, I'd love to hear:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How do you handle subnet-rotating bots today?&lt;/strong&gt; fail2ban with a custom action, CrowdSec, edge firewall, or just ignore them?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IPv6 support:&lt;/strong&gt; worth adding, or do you mostly see this noise over v4?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anything that broke&lt;/strong&gt; on your distro. Issues and PRs are very welcome.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If it saved you a few minutes of log-scrolling, a ⭐ on the repo helps other admins find it.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;The tool and this post were written with help from an AI assistant. The problem, the servers and the decisions are mine.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>linux</category>
      <category>security</category>
      <category>opensource</category>
      <category>bash</category>
    </item>
  </channel>
</rss>
