<?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: DevOps Journey</title>
    <description>The latest articles on DEV Community by DevOps Journey (@devops_journey_4b18fb2ab9).</description>
    <link>https://dev.to/devops_journey_4b18fb2ab9</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3898954%2Fb53109ad-8d26-4f7e-8993-9ec5e3b04517.png</url>
      <title>DEV Community: DevOps Journey</title>
      <link>https://dev.to/devops_journey_4b18fb2ab9</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/devops_journey_4b18fb2ab9"/>
    <language>en</language>
    <item>
      <title>How I Built a Real-Time DDoS Detection Engine from Scratch (No Fail2Ban Allowed)</title>
      <dc:creator>DevOps Journey</dc:creator>
      <pubDate>Sun, 26 Apr 2026 15:25:48 +0000</pubDate>
      <link>https://dev.to/devops_journey_4b18fb2ab9/how-i-built-a-real-time-ddos-detection-engine-from-scratch-no-fail2ban-allowed-2e59</link>
      <guid>https://dev.to/devops_journey_4b18fb2ab9/how-i-built-a-real-time-ddos-detection-engine-from-scratch-no-fail2ban-allowed-2e59</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Real-Time DDoS Detection Engine from Scratch (No Fail2Ban Allowed)
&lt;/h1&gt;

&lt;p&gt;When my boss said "build something that watches all incoming traffic, learns what normal looks like, and automatically blocks attackers" — I had no idea where to start. No Fail2Ban. No rate-limiting libraries. Just Python, math, and Linux firewall rules.&lt;/p&gt;

&lt;p&gt;This is the story of how I built it, and how you can understand every piece of it — even if you've never worked on security tooling before.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Does This Project Do?
&lt;/h2&gt;

&lt;p&gt;Imagine a security guard standing at the entrance of a building. Their job is to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Watch&lt;/strong&gt; everyone who comes in&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learn&lt;/strong&gt; what a normal busy day looks like&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Notice&lt;/strong&gt; when something unusual happens — like 200 people trying to enter at once&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act&lt;/strong&gt; — block the suspicious person and alert the team&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's exactly what this tool does, but for HTTP traffic hitting a web server.&lt;/p&gt;

&lt;p&gt;The system runs alongside a Nextcloud instance and continuously monitors Nginx access logs. When it detects abnormal traffic — either from a single aggressive IP or a global traffic spike — it automatically blocks the attacker using Linux firewall rules and sends a Slack alert within 10 seconds.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Internet Traffic&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
Nginx (writes JSON logs)&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
Nextcloud&lt;br&gt;
│&lt;br&gt;
[shared log volume]&lt;br&gt;
│&lt;br&gt;
▼&lt;br&gt;
Detector Daemon (Python)&lt;br&gt;
├── Monitor    → reads log line by line&lt;br&gt;
├── Baseline   → learns normal traffic patterns&lt;br&gt;
├── Detector   → spots anomalies using math&lt;br&gt;
├── Blocker    → adds iptables firewall rules&lt;br&gt;
├── Unbanner   → releases bans on a schedule&lt;br&gt;
├── Notifier   → sends Slack alerts&lt;br&gt;
└── Dashboard  → live web UI&lt;/p&gt;

&lt;p&gt;Everything runs in Docker containers. The detector mounts the Nginx log volume read-only so it can watch logs without touching the web server.&lt;/p&gt;


&lt;h2&gt;
  
  
  How the Sliding Window Works
&lt;/h2&gt;

&lt;p&gt;The first problem to solve: how do you measure "how many requests per second is this IP sending right now?"&lt;/p&gt;

&lt;p&gt;The answer is a &lt;strong&gt;sliding window&lt;/strong&gt; — a moving view of the last 60 seconds of traffic.&lt;/p&gt;

&lt;p&gt;Think of it like a 60-second conveyor belt. Every time a request arrives, we add a timestamp to the belt. Every time we want to know the current rate, we count how many timestamps are still on the belt (within the last 60 seconds). Old timestamps fall off the end automatically.&lt;/p&gt;

&lt;p&gt;In Python, we use &lt;code&gt;collections.deque&lt;/code&gt; — a double-ended queue that lets us add to the right and remove from the left efficiently:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;deque&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="c1"&gt;# One deque per IP
&lt;/span&gt;&lt;span class="n"&gt;ip_window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_window&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;  &lt;span class="c1"&gt;# 60-second window
&lt;/span&gt;
    &lt;span class="c1"&gt;# Add new timestamp
&lt;/span&gt;    &lt;span class="n"&gt;ip_window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Evict expired timestamps from the left
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;ip_window&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;ip_window&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ip_window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_window&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Rate = number of timestamps in the window
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We maintain two windows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-IP window&lt;/strong&gt;: tracks requests from a single IP address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global window&lt;/strong&gt;: tracks all requests across all IPs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the core of real-time rate measurement. No databases, no counters that reset every minute — just timestamps in a deque.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Baseline Learns From Traffic
&lt;/h2&gt;

&lt;p&gt;Detecting "too many requests" only makes sense if you know what "normal" looks like. That's where the baseline comes in.&lt;/p&gt;

&lt;p&gt;Every second, we record how many total requests arrived. We store these per-second counts in a rolling 30-minute window — again using a deque:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;baseline_window&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;deque&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# stores (timestamp, count) tuples
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_second_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;cutoff&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# 30 minutes
&lt;/span&gt;
    &lt;span class="n"&gt;baseline_window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Evict data older than 30 minutes
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;baseline_window&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;baseline_window&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;cutoff&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;baseline_window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;popleft&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every 60 seconds, we recalculate the &lt;strong&gt;mean&lt;/strong&gt; (average) and &lt;strong&gt;standard deviation&lt;/strong&gt; from all the counts in the window:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compute_stats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
    &lt;span class="n"&gt;variance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;stddev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mean tells us "on average, how many requests per second do we get?" The standard deviation tells us "how much does it vary?"&lt;/p&gt;

&lt;p&gt;We also store results in hourly slots — so if your server gets more traffic in the afternoon than at night, the baseline adapts to each hour's pattern rather than using one global average.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Floor values&lt;/strong&gt;: When the system first starts, there isn't enough data yet. We use floor values (mean=1.0, stddev=1.0) until at least 10 samples are collected. This prevents false positives during startup.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Detection Logic Makes a Decision
&lt;/h2&gt;

&lt;p&gt;Once we have a baseline, we can detect anomalies. We use two conditions — whichever fires first triggers a block:&lt;/p&gt;

&lt;h3&gt;
  
  
  Condition 1: Z-Score &amp;gt; 3.0
&lt;/h3&gt;

&lt;p&gt;The z-score measures how many standard deviations away from the mean the current rate is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;zscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A z-score of 3.0 means "this value is 3 standard deviations above normal." Statistically, that happens less than 0.3% of the time under normal conditions. If we see it, something unusual is happening.&lt;/p&gt;

&lt;h3&gt;
  
  
  Condition 2: Rate &amp;gt; 5x the Mean
&lt;/h3&gt;

&lt;p&gt;Even if the standard deviation is small, a rate 5 times higher than normal is suspicious:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;is_anomalous&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nf"&gt;zscore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stddev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt;
    &lt;span class="n"&gt;ip_rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Error Surge Tightening
&lt;/h3&gt;

&lt;p&gt;If an IP is also generating lots of 4xx/5xx errors (like hammering login endpoints), we tighten the thresholds automatically — the z-score threshold drops from 3.0 to 1.5 and the rate multiplier from 5x to 2.5x. A misbehaving IP gets less tolerance.&lt;/p&gt;




&lt;h2&gt;
  
  
  How iptables Blocks an IP
&lt;/h2&gt;

&lt;p&gt;When an anomaly is detected, we need to actually stop the traffic. Linux has a built-in firewall called &lt;strong&gt;iptables&lt;/strong&gt; that operates at the kernel level — before the traffic even reaches Nginx or our application.&lt;/p&gt;

&lt;p&gt;We add a DROP rule using Python's &lt;code&gt;subprocess&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ban_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iptables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# append to INPUT chain
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# source IP to match
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-j&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# action: drop the packet silently
&lt;/span&gt;    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a packet is dropped, the sender gets no response — it's like the server doesn't exist. This is more effective than sending a "rejected" response because it wastes the attacker's time waiting for a reply that never comes.&lt;/p&gt;

&lt;p&gt;To unban:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unban_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iptables&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-D&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INPUT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# delete from INPUT chain
&lt;/span&gt;        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-s&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-j&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DROP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Auto-Unban with Backoff
&lt;/h3&gt;

&lt;p&gt;Bans aren't permanent by default. We use a backoff schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First ban: 10 minutes&lt;/li&gt;
&lt;li&gt;Second ban: 30 minutes
&lt;/li&gt;
&lt;li&gt;Third ban: 2 hours&lt;/li&gt;
&lt;li&gt;Fourth ban onwards: permanent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is fair — if an IP triggers once, it might be a misconfigured client. If it keeps coming back, it gets progressively longer bans.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Dashboard
&lt;/h2&gt;

&lt;p&gt;A live web dashboard built with Flask shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current global request rate&lt;/li&gt;
&lt;li&gt;Effective mean and standard deviation&lt;/li&gt;
&lt;li&gt;All currently banned IPs with their ban conditions&lt;/li&gt;
&lt;li&gt;Top 10 source IPs by request count&lt;/li&gt;
&lt;li&gt;CPU and memory usage&lt;/li&gt;
&lt;li&gt;System uptime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It refreshes every 3 seconds automatically so you can watch the system react in real time.&lt;/p&gt;




&lt;h2&gt;
  
  
  Slack Alerts
&lt;/h2&gt;

&lt;p&gt;Every significant event sends a Slack message:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;🚨 &lt;strong&gt;IP Ban&lt;/strong&gt;: IP address, condition that fired, current rate, baseline mean, timestamp&lt;/li&gt;
&lt;li&gt;✅ &lt;strong&gt;IP Unban&lt;/strong&gt;: same info plus next ban duration&lt;/li&gt;
&lt;li&gt;⚠️ &lt;strong&gt;Global Anomaly&lt;/strong&gt;: when total traffic spikes (no single IP to block)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;Building this from scratch taught me things no tutorial covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Math matters in production&lt;/strong&gt;: z-scores aren't just textbook concepts — they're genuinely useful for anomaly detection&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deques are underrated&lt;/strong&gt;: Python's &lt;code&gt;collections.deque&lt;/code&gt; is perfect for sliding windows — O(1) append and popleft&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baselines must be dynamic&lt;/strong&gt;: hardcoding thresholds fails in production because traffic patterns change by hour, day, and season&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;iptables is powerful&lt;/strong&gt;: blocking at the kernel level is far more effective than application-level rate limiting&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The full source code is available at:&lt;br&gt;
&lt;strong&gt;&lt;a href="https://github.com/travispocr/hng-stage3-devops" rel="noopener noreferrer"&gt;https://github.com/travispocr/hng-stage3-devops&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The live dashboard is running at:&lt;br&gt;
&lt;strong&gt;&lt;a href="http://psitdev.duckdns.org:8080" rel="noopener noreferrer"&gt;http://psitdev.duckdns.org:8080&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;To run it on your own server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/travispocr/hng-stage3-devops.git
&lt;span class="nb"&gt;cd &lt;/span&gt;hng-stage3-devops
&lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
&lt;span class="c"&gt;# Edit .env with your Slack webhook URL&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;&lt;em&gt;Built as part of the HNG Internship DevOps Track — Stage 3.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>python</category>
      <category>security</category>
      <category>networking</category>
    </item>
  </channel>
</rss>
