<?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: Hendy</title>
    <description>The latest articles on DEV Community by Hendy (@iamhendy).</description>
    <link>https://dev.to/iamhendy</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%2F3903121%2Fb4f17c85-65c0-4a69-abe3-eef6977df555.png</url>
      <title>DEV Community: Hendy</title>
      <link>https://dev.to/iamhendy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iamhendy"/>
    <language>en</language>
    <item>
      <title>How I Built a Real-Time DDoS Detection Engine for Nextcloud from Scratch</title>
      <dc:creator>Hendy</dc:creator>
      <pubDate>Tue, 28 Apr 2026 20:34:40 +0000</pubDate>
      <link>https://dev.to/iamhendy/how-i-built-a-real-time-ddos-detection-engine-for-nextcloud-from-scratch-17hl</link>
      <guid>https://dev.to/iamhendy/how-i-built-a-real-time-ddos-detection-engine-for-nextcloud-from-scratch-17hl</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Real-Time DDoS Detection Engine for Nextcloud from Scratch
&lt;/h1&gt;

&lt;h2&gt;
  
  
  Introduction
&lt;/h2&gt;

&lt;p&gt;Imagine you're running a cloud storage platform used by thousands of people around the world. One day, your boss walks in and says: &lt;em&gt;"We've been seeing suspicious traffic. Build something that detects and blocks attacks automatically."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's exactly the challenge I faced. In this post, I'll walk you through how I built a real-time anomaly detection engine that watches HTTP traffic, learns what normal looks like, and automatically blocks attackers — all without using any third-party rate-limiting libraries.&lt;/p&gt;

&lt;p&gt;By the end of this post, you'll understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How sliding windows track request rates in real time&lt;/li&gt;
&lt;li&gt;How a baseline learns from your own traffic patterns&lt;/li&gt;
&lt;li&gt;How z-score math decides if something is an attack&lt;/li&gt;
&lt;li&gt;How iptables drops malicious IPs at the kernel level&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;The system sits alongside a Nextcloud instance and does five things continuously:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Reads&lt;/strong&gt; Nginx access logs in real time, line by line&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tracks&lt;/strong&gt; request rates using sliding windows (per IP and globally)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Learns&lt;/strong&gt; what normal traffic looks like using a rolling baseline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Detects&lt;/strong&gt; anomalies when traffic deviates significantly from normal&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blocks&lt;/strong&gt; suspicious IPs automatically using iptables&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's the overall architecture:&lt;br&gt;
Internet → Nginx (JSON logs) → Nextcloud&lt;br&gt;
↓&lt;br&gt;
Log Volume (shared)&lt;br&gt;
↓&lt;br&gt;
Python Daemon&lt;br&gt;
├── Monitor (reads logs)&lt;br&gt;
├── Baseline (learns traffic)&lt;br&gt;
├── Detector (flags anomalies)&lt;br&gt;
├── Blocker (iptables rules)&lt;br&gt;
├── Unbanner (auto-release)&lt;br&gt;
├── Notifier (Slack alerts)&lt;/p&gt;
&lt;h2&gt;
  
  
  └── Dashboard (live metrics)
&lt;/h2&gt;
&lt;h2&gt;
  
  
  How the Sliding Window Works
&lt;/h2&gt;

&lt;p&gt;A sliding window is a way of asking: &lt;em&gt;"How many requests happened in the last 60 seconds?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The naive approach would be to count all requests every minute — but that gives you a stale snapshot, not a real-time view.&lt;/p&gt;

&lt;p&gt;Instead, I used Python's &lt;code&gt;collections.deque&lt;/code&gt; — a double-ended queue. Here's the idea:&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;record_request&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;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 current timestamp to the right
&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 old 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="c1"&gt;# Current rate = requests in last 60 seconds
&lt;/span&gt;    &lt;span class="n"&gt;rate&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;ip_window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every time a request comes in, its timestamp is appended to the right. Old timestamps (older than 60 seconds) are removed from the left using &lt;code&gt;popleft()&lt;/code&gt;. The current rate is simply the length of the deque divided by 60.&lt;/p&gt;

&lt;p&gt;This gives us a perfectly accurate rolling count with O(1) insertions and evictions. No databases, no counters that reset at fixed intervals.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-IP window:&lt;/strong&gt; one deque per IP address&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global window:&lt;/strong&gt; one deque for all traffic combined&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;The baseline answers the question: &lt;em&gt;"What does normal traffic look like on this server?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Instead of hardcoding a threshold like "flag anything above 10 req/s", I compute the mean and standard deviation from actual recent traffic.&lt;/p&gt;

&lt;p&gt;Here's how it works:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rolling 30-minute window:&lt;/strong&gt;&lt;br&gt;
Every second, I record how many requests arrived that second. I keep a rolling window of the last 30 minutes (1800 seconds) of these per-second counts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recalculation every 60 seconds:&lt;/strong&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="n"&gt;counts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&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;counts&lt;/span&gt;&lt;span class="p"&gt;)&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;counts&lt;/span&gt;&lt;span class="p"&gt;)&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;c&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;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;counts&lt;/span&gt;&lt;span class="p"&gt;)&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;counts&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Per-hour slots:&lt;/strong&gt;&lt;br&gt;
The baseline stores separate mean/stddev values per hour. This means if your server is busier at 9am than at 3am, the baseline adapts automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Floor values:&lt;/strong&gt;&lt;br&gt;
To avoid division by zero on idle servers, I set minimum values:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;floor_mean = 0.1 req/s&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;floor_stddev = 0.1&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;Once I have the baseline mean and stddev, I use &lt;strong&gt;z-score&lt;/strong&gt; to decide if a rate is anomalous.&lt;/p&gt;

&lt;p&gt;The z-score measures how many standard deviations a value is from the mean:&lt;br&gt;
z = (current_rate - mean) / stddev&lt;br&gt;
If &lt;code&gt;z &amp;gt; 3.0&lt;/code&gt;, that means the current rate is more than 3 standard deviations above normal — statistically very unlikely under normal conditions.&lt;/p&gt;

&lt;p&gt;I also add a rate multiplier check as a backup:&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;check_ip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;mean&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;baseline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_baseline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ip_rate&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;# Z-score check
&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;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;zscore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&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;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;zscore&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="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zscore=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;zscore&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Rate multiplier check
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;5&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="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &amp;gt; 5x mean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Error surge detection:&lt;/strong&gt;&lt;br&gt;
If an IP's 4xx/5xx error rate is 3x higher than the baseline error rate, the thresholds are automatically tightened by 30%. This catches attackers who probe for vulnerabilities before launching a full attack.&lt;/p&gt;


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

&lt;p&gt;When an IP is flagged as anomalous, blocking happens at the &lt;strong&gt;kernel level&lt;/strong&gt; using iptables — before traffic even reaches Nginx or Nextcloud.&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&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;-I&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="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;p&gt;The &lt;code&gt;-I INPUT&lt;/code&gt; inserts the rule at the top of the INPUT chain. &lt;code&gt;-s&lt;/code&gt; specifies the source IP. &lt;code&gt;-j DROP&lt;/code&gt; silently drops all packets from that IP.&lt;/p&gt;

&lt;p&gt;This is extremely efficient — the kernel drops packets without any application-level processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auto-unban with backoff schedule:&lt;/strong&gt;&lt;br&gt;
Bans don't last forever. The unbanner releases IPs on a backoff schedule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;1st ban: 10 minutes&lt;/li&gt;
&lt;li&gt;2nd ban: 30 minutes
&lt;/li&gt;
&lt;li&gt;3rd ban: 2 hours&lt;/li&gt;
&lt;li&gt;4th ban+: permanent
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;unban_schedule&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7200&lt;/span&gt;&lt;span class="p"&gt;,&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="c1"&gt;# seconds, -1 = permanent
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The system serves a live dashboard at a public domain showing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global request rate vs baseline&lt;/li&gt;
&lt;li&gt;Banned IPs and their ban counts&lt;/li&gt;
&lt;li&gt;Top 10 source IPs by request rate&lt;/li&gt;
&lt;li&gt;CPU and memory usage&lt;/li&gt;
&lt;li&gt;Uptime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built with Flask and auto-refreshes every 3 seconds.&lt;/p&gt;




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

&lt;p&gt;Every ban, unban, and global anomaly sends a Slack notification with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The IP address&lt;/li&gt;
&lt;li&gt;The condition that fired (z-score or rate multiplier)&lt;/li&gt;
&lt;li&gt;Current rate vs baseline&lt;/li&gt;
&lt;li&gt;Ban duration&lt;/li&gt;
&lt;li&gt;Timestamp&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Building this from scratch taught me:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deques are powerful&lt;/strong&gt; — simple data structures can solve complex real-time problems elegantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Statistics beat hardcoded thresholds&lt;/strong&gt; — a z-score adapts to your actual traffic patterns&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel-level blocking is fast&lt;/strong&gt; — iptables drops packets before your app even sees them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Baselines need floors&lt;/strong&gt; — always handle the idle server case&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;The full source code is available on GitHub:&lt;br&gt;
👉 &lt;a href="https://github.com/IamHendy/hng-anomaly-detector" rel="noopener noreferrer"&gt;https://github.com/IamHendy/hng-anomaly-detector&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The live dashboard is running at:&lt;br&gt;
👉 &lt;a href="http://hendyogema.mooo.com" rel="noopener noreferrer"&gt;http://hendyogema.mooo.com&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built as part of the HNG DevSecOps track — Stage 3 challenge.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>networking</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
