August 9, 2026
I Built an Automated Intrusion Detection System Using Python, iptables, and Log Analysis”…
I built a Python tool that detects SSH brute-force attacks in real time and automatically blocks the attacker’s IP using iptables — running…

By Sagar rj
3 min read
I Built an Automated Intrusion Detection System Using Python, iptables, and Log Analysis" (Targets: intrusion detection system, Python security project — high search volume terms)
I built a Python tool that detects SSH brute-force attacks in real time and automatically blocks the attacker's IP using iptables — running on an isolated two-VM lab so I could safely simulate real attacks instead of working from sample data. Here's the full architecture, the detection logic, and the bugs I hit along the way.
System Overview
The system consists of three components operating on a two-VM isolated lab:
The two VMs sit on an isolated VirtualBox NAT Network — no host bridging, no internet-facing exposure. This matters technically, not just for safety: it guarantees the only traffic in auth.log is traffic I generated, which keeps the detection logic's signal clean during development.
Attack Simulation
Brute-force traffic is generated with Hydra against a disposable test account:
Early on, I ran this with -p (lowercase — single password) instead of -P (password list), which silently changed the entire nature of the traffic being generated. It's a useful failure mode to know about: Hydra won't error out, it'll just run a much smaller, wrong test, and if you're not watching attempt counts closely, you can build your detection thresholds around bad data.
A second variable worth knowing about at the environment layer: OpenSSH's built-in srclimit_penalise mechanism rate-limits repeated connection attempts from a single source. This meant my raw attempt count from Hydra didn't map 1:1 to entries in auth.log. Any detection engine built on this log source needs to treat OpenSSH's own throttling as a confound, not assume the log is a raw, unfiltered record of attacker behavior.
Detection Engine
The core script tails /var/log/auth.log, extracts source IPs via regex, and maintains a sliding 60-second window of failed attempts per IP:
he bug worth calling out: my first draft never appended the timestamp to ip_attempts[ip] — the list-pruning logic ran, but the list was always empty, so the threshold check silently never fired. It's a good example of a bug that produces no error and no crash, just quiet, wrong behavior — the kind that only shows up when you instrument your code and watch the actual state, not just the final output.
Design tradeoff: using an in-memory sliding window (a list of timestamps per IP, pruned on each check) is O(n) per check in the worst case, but at the scale of a single-host lab this is a non-issue. In a production/multi-host setting, I'd want this backed by something like Redis with TTL-based expiry instead of a live Python dict, so state survives restarts and can be shared across multiple log-collector instances.
Automated Response Layer
Once an IP crosses threshold, the response layer shells out to iptables to drop future traffic from that source, with an expiry mechanism so blocks aren't permanent:
Bugs found in this phase (debugged manually, not wholesale-replaced):
- Incorrect flag usage on the
iptablescall - A variable name typo that caused the wrong IP to be passed to the block command
- Logic that checked block-status after attempting to reapply a rule instead of before, causing redundant rule insertion
- A syntax error from a misplaced colon in a conditional block
None of these are exotic — they're the standard cost of writing subprocess-driven infrastructure code by hand. The interesting part isn't the bugs themselves, it's the failure mode each one produces: flag errors fail loud, but the logic-ordering bug fails quiet (duplicate rules stacking up in iptables -L without any error), which is arguably more dangerous in a real system.
Known Limitations (by design, not oversight)
- Single log source: only reads
auth.log. No correlation across hosts, no SIEM ingestion. - Single attack vector: tuned specifically for SSH password brute-forcing. Doesn't generalize to key-based attacks, slow/low-and-slow brute-forcing under the threshold window, or non-SSH services.
- Local-scale only: in-memory state, no persistence, no horizontal scaling.
- No allowlisting: a legitimate user fat-fingering their password 5 times in a minute gets blocked identically to an attacker. There's no reputation scoring or human-in-the-loop override yet — this is the top item on my roadmap.
What's Next
- Streamlit dashboard — visualizing block events, attempt frequency, and time-to-detection
- False positive / false negative analysis — deliberately testing edge cases like slow brute-forcing (below the threshold rate) and legitimate high-frequency failed logins
- Human-in-the-loop review panel — before a block becomes permanent, or as an audit layer after the fact
- Second attack pattern — likely testing detection generalization against a different auth failure signature