<?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: anitaalicloud</title>
    <description>The latest articles on DEV Community by anitaalicloud (@anitaalicloud).</description>
    <link>https://dev.to/anitaalicloud</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%2F3901516%2F3aeb27ec-d326-4295-bcd7-bc103e1aa263.png</url>
      <title>DEV Community: anitaalicloud</title>
      <link>https://dev.to/anitaalicloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/anitaalicloud"/>
    <language>en</language>
    <item>
      <title>How I Built an Anomaly Detection Engine for DDoS Protection</title>
      <dc:creator>anitaalicloud</dc:creator>
      <pubDate>Tue, 28 Apr 2026 03:56:36 +0000</pubDate>
      <link>https://dev.to/anitaalicloud/how-i-built-an-anomaly-detection-engine-for-ddos-protection-1ibg</link>
      <guid>https://dev.to/anitaalicloud/how-i-built-an-anomaly-detection-engine-for-ddos-protection-1ibg</guid>
      <description>&lt;p&gt;Introduction&lt;br&gt;
Imagine you run a busy website. On a normal day, about 50 people visit per second. Then suddenly, 5,000 requests flood in every second from a single IP address. Your server crashes. Your real users can't access anything. This is a DDoS (Distributed Denial of Service) attack.&lt;br&gt;
In this post, I'll explain how I built a tool that watches incoming traffic in real time, learns what "normal" looks like, and automatically blocks attackers before they can cause damage.&lt;/p&gt;

&lt;p&gt;What Does the Tool Do?&lt;br&gt;
My anomaly detection engine does 6 things automatically:&lt;/p&gt;

&lt;p&gt;Watches every HTTP request coming into the server&lt;br&gt;
Learns what normal traffic looks like over time&lt;br&gt;
Detects when traffic becomes abnormal&lt;br&gt;
Blocks the attacker using the Linux firewall&lt;br&gt;
Alerts me on Slack within 10 seconds&lt;br&gt;
Unbans the IP automatically after a timeout&lt;/p&gt;

&lt;p&gt;The Architecture&lt;br&gt;
Internet Traffic&lt;br&gt;
      ↓&lt;br&gt;
   Nginx (logs every request as JSON)&lt;br&gt;
      ↓&lt;br&gt;
  Nextcloud (the actual app)&lt;/p&gt;

&lt;p&gt;Detector Daemon reads Nginx logs&lt;br&gt;
      ↓&lt;br&gt;
  Sliding Window → tracks request rates&lt;br&gt;
  Rolling Baseline → learns normal traffic&lt;br&gt;
  Z-score Detection → spots anomalies&lt;br&gt;
  iptables → blocks attackers&lt;br&gt;
  Slack → sends alerts&lt;br&gt;
  Dashboard → shows live metrics&lt;/p&gt;

&lt;p&gt;Part 1 — How the Sliding Window Works&lt;br&gt;
Think of the sliding window like a 60 second camera 🎥&lt;br&gt;
Every request that comes in gets a timestamp. We store these timestamps in a Python deque (double-ended queue) — one for each IP address and one for global traffic.&lt;/p&gt;

&lt;p&gt;from collections import deque&lt;br&gt;
import time&lt;/p&gt;

&lt;h1&gt;
  
  
  One deque per IP
&lt;/h1&gt;

&lt;p&gt;ip_windows = {}&lt;/p&gt;

&lt;p&gt;def record_request(ip):&lt;br&gt;
    now = time.time()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ip not in ip_windows:
    ip_windows[ip] = deque()

# Add this request
ip_windows[ip].append(now)

# Remove requests older than 60 seconds from the LEFT
cutoff = now - 60
while ip_windows[ip] and ip_windows[ip][0] &amp;lt; cutoff:
    ip_windows[ip].popleft()

# Current rate = how many requests in last 60 seconds
current_rate = len(ip_windows[ip]) / 60
return current_rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;from collections import deque&lt;br&gt;
import time&lt;/p&gt;

&lt;h1&gt;
  
  
  One deque per IP
&lt;/h1&gt;

&lt;p&gt;ip_windows = {}&lt;/p&gt;

&lt;p&gt;def record_request(ip):&lt;br&gt;
    now = time.time()&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if ip not in ip_windows:
    ip_windows[ip] = deque()

# Add this request
ip_windows[ip].append(now)

# Remove requests older than 60 seconds from the LEFT
cutoff = now - 60
while ip_windows[ip] and ip_windows[ip][0] &amp;lt; cutoff:
    ip_windows[ip].popleft()

# Current rate = how many requests in last 60 seconds
current_rate = len(ip_windows[ip]) / 60
return current_rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The magic is the eviction — old timestamps get removed from the left side of the deque automatically. So the deque always contains only the last 60 seconds of requests. The current rate is simply the length of the deque divided by 60.&lt;/p&gt;

&lt;p&gt;Part 2 — How the Baseline Learns from Traffic&lt;br&gt;
The baseline answers one question: "What is normal?"&lt;br&gt;
We can't hardcode this because every website is different. A news site might normally get 1000 req/s. A small blog might get 2 req/s. So we let the system learn.&lt;br&gt;
Every second we record how many requests came in. Every 60 seconds we look at the last 30 minutes of data and calculate:&lt;/p&gt;

&lt;p&gt;import math&lt;/p&gt;

&lt;p&gt;def recalculate_baseline(per_second_counts):&lt;br&gt;
    # Calculate average requests per second&lt;br&gt;
    mean = sum(per_second_counts) / len(per_second_counts)&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Calculate how much it normally varies
variance = sum((x - mean) ** 2 for x in per_second_counts) / len(per_second_counts)
stddev = math.sqrt(variance)

# Apply floors to prevent false alarms on quiet traffic
effective_mean = max(mean, 1.0)
effective_stddev = max(stddev, 1.0)

return effective_mean, effective_stddev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;We also maintain per-hour slots — the system prefers the current hour's data when it has enough samples. This means the baseline adapts to time-of-day patterns. Rush hour traffic looks different from 3 AM traffic!&lt;/p&gt;

&lt;p&gt;Part 3 — How the Detection Logic Makes a Decision&lt;br&gt;
Once we have the baseline we use a Z-score to decide if current traffic is anomalous.&lt;br&gt;
The Z-score answers: "How many standard deviations away from normal is this?"&lt;/p&gt;

&lt;p&gt;def is_anomalous(current_rate, mean, stddev):&lt;br&gt;
    # Z-score calculation&lt;br&gt;
    z_score = (current_rate - mean) / stddev&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Rate multiplier
rate_multiplier = current_rate / mean

# Flag as anomalous if EITHER condition fires
if z_score &amp;gt; 3.0:
    return True, "z-score exceeded 3.0"

if rate_multiplier &amp;gt; 5.0:
    return True, "rate exceeded 5x baseline"

return False, None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Example:&lt;/p&gt;

&lt;p&gt;Normal traffic: 50 req/s (mean=50, stddev=10)&lt;br&gt;
Attack traffic: 5000 req/s from one IP&lt;br&gt;
Z-score = (5000 - 50) / 10 = 495&lt;br&gt;
495 &amp;gt; 3.0 → ANOMALY DETECTED! 🚨&lt;/p&gt;

&lt;p&gt;We also detect error surges — if an IP is getting lots of 404/500 errors it might be scanning for vulnerabilities. In that case we tighten the thresholds automatically.&lt;/p&gt;

&lt;p&gt;Part 4 — How iptables Blocks an IP&lt;br&gt;
iptables is Linux's built-in firewall. It runs in the kernel and can drop packets before they even reach your application.&lt;br&gt;
When we detect an attack:&lt;/p&gt;

&lt;p&gt;import subprocess&lt;/p&gt;

&lt;p&gt;def ban_ip(ip):&lt;br&gt;
    # Add a DROP rule — silently discard all packets from this IP&lt;br&gt;
    subprocess.run([&lt;br&gt;
        "iptables", "-A", "INPUT", &lt;br&gt;
        "-s", ip, &lt;br&gt;
        "-j", "DROP"&lt;br&gt;
    ])&lt;br&gt;
    print(f"Banned {ip}")&lt;/p&gt;

&lt;p&gt;def unban_ip(ip):&lt;br&gt;
    # Remove the DROP rule&lt;br&gt;
    subprocess.run([&lt;br&gt;
        "iptables", "-D", "INPUT",&lt;br&gt;
        "-s", ip,&lt;br&gt;
        "-j", "DROP"&lt;br&gt;&lt;br&gt;
    ])&lt;br&gt;
    print(f"Unbanned {ip}")&lt;br&gt;
The -j DROP means "jump to DROP action" — the packet is silently discarded. The attacker doesn't even get an error message back. From their perspective the server just stopped responding.&lt;br&gt;
Bans lift automatically on a backoff schedule:&lt;/p&gt;

&lt;p&gt;1st offence → 10 minutes&lt;br&gt;
2nd offence → 30 minutes&lt;br&gt;
3rd offence → 2 hours&lt;br&gt;
4th+ → permanent&lt;/p&gt;

&lt;p&gt;Part 5 — The Live Dashboard&lt;br&gt;
The dashboard is a simple web page that refreshes every 3 seconds showing:&lt;/p&gt;

&lt;p&gt;Global requests per second&lt;br&gt;
Currently banned IPs&lt;br&gt;
Top 10 source IPs&lt;br&gt;
CPU and memory usage&lt;br&gt;
Current baseline mean and stddev&lt;br&gt;
System uptime&lt;/p&gt;

&lt;p&gt;It's built using Python's built-in http.server — no web framework needed!&lt;/p&gt;

&lt;p&gt;Part 6 — Slack Alerts&lt;br&gt;
When an IP gets banned, a Slack message arrives within 10 seconds:&lt;br&gt;
🚨 IP BANNED&lt;br&gt;
IP: 192.168.1.100&lt;br&gt;
Condition: Anomalous request rate&lt;br&gt;
Current Rate: 450.00 req/s&lt;br&gt;
Baseline: 12.00 req/s&lt;br&gt;
Ban Duration: 10 minutes&lt;br&gt;
Timestamp: 2026-04-28 03:22:36 UTC&lt;br&gt;
And when the ban expires:&lt;br&gt;
✅ IP UNBANNED&lt;br&gt;
IP: 192.168.1.100&lt;br&gt;
Reason: ban-expired&lt;br&gt;
Timestamp: 2026-04-28 03:32:36 UTC&lt;/p&gt;

&lt;p&gt;What I Learned&lt;br&gt;
Building this project taught me:&lt;/p&gt;

&lt;p&gt;Z-scores are powerful — a simple maths formula can detect attacks that would be impossible to catch with hardcoded thresholds&lt;br&gt;
Baselines must be dynamic — hardcoding "block if &amp;gt; 100 req/s" is wrong because normal traffic varies by time of day&lt;br&gt;
iptables is incredibly fast — kernel-level packet dropping happens before the request even reaches Python&lt;br&gt;
Threading needs care — shared data structures need locks to prevent race conditions&lt;br&gt;
Deques are perfect for sliding windows — O(1) append and popleft make them ideal for real-time rate tracking&lt;/p&gt;

&lt;p&gt;Try It Yourself&lt;br&gt;
The full source code is available at:&lt;br&gt;
&lt;a href="https://github.com/AnitaAliCloud/hng-stage3-devops" rel="noopener noreferrer"&gt;https://github.com/AnitaAliCloud/hng-stage3-devops&lt;/a&gt;&lt;br&gt;
The live dashboard is running at:&lt;br&gt;
&lt;a href="http://anitacloud.duckdns.org:8080" rel="noopener noreferrer"&gt;http://anitacloud.duckdns.org:8080&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Built as part of the HNG14 DevOps internship programme &lt;/p&gt;

</description>
      <category>monitoring</category>
      <category>networking</category>
      <category>security</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
