<?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>Forem: Frank</title>
    <description>The latest articles on Forem by Frank (@frank_shadow2).</description>
    <link>https://forem.com/frank_shadow2</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%2F3903334%2F6b014329-16c0-42eb-bf2d-3ea6c33cb66b.png</url>
      <title>Forem: Frank</title>
      <link>https://forem.com/frank_shadow2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/frank_shadow2"/>
    <language>en</language>
    <item>
      <title>How I Built a Real-Time DDoS Detection Engine from Scratch</title>
      <dc:creator>Frank</dc:creator>
      <pubDate>Wed, 29 Apr 2026 01:11:25 +0000</pubDate>
      <link>https://forem.com/frank_shadow2/how-i-built-a-real-time-ddos-detection-engine-from-scratch-94i</link>
      <guid>https://forem.com/frank_shadow2/how-i-built-a-real-time-ddos-detection-engine-from-scratch-94i</guid>
      <description>&lt;h1&gt;
  
  
  How I Built a Real-Time DDoS Detection Engine from Scratch
&lt;/h1&gt;

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

&lt;p&gt;Imagine you own a popular website. Thousands of people visit every day. &lt;br&gt;
Then one morning, a hacker sends millions of fake requests to your server &lt;br&gt;
all at once — trying to crash it. This is called a &lt;strong&gt;DDoS attack&lt;/strong&gt; &lt;br&gt;
(Distributed Denial of Service).&lt;/p&gt;

&lt;p&gt;For HNG Stage 3, I was tasked with building a system that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Watches all incoming web traffic in real time&lt;/li&gt;
&lt;li&gt;Learns what "normal" traffic looks like&lt;/li&gt;
&lt;li&gt;Automatically detects and blocks attackers&lt;/li&gt;
&lt;li&gt;Sends instant Slack alerts&lt;/li&gt;
&lt;li&gt;Shows everything on a live dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's exactly how I built it — explained simply enough that &lt;br&gt;
a complete beginner can follow along.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Architecture — How Everything Connects
&lt;/h2&gt;

&lt;p&gt;Think of the system like a &lt;strong&gt;security team for a building&lt;/strong&gt;:&lt;br&gt;
Internet → Nginx (doorman) → Nextcloud (the building)&lt;br&gt;
↓&lt;br&gt;
Access Log (visitor diary)&lt;br&gt;
↓&lt;br&gt;
Python Daemon (security guard reading the diary)&lt;br&gt;
↓&lt;br&gt;
┌──────────────────────────────┐&lt;br&gt;
│  Detect attack → Ban IP      │&lt;br&gt;
│  Send Slack alert            │&lt;br&gt;
│  Show on live dashboard      │&lt;br&gt;
└──────────────────────────────┘&lt;br&gt;
&lt;strong&gt;Nginx&lt;/strong&gt; sits in front of everything. Every single request that &lt;br&gt;
comes in — legitimate user or attacker — passes through Nginx first. &lt;br&gt;
Nginx writes a JSON log entry for every request containing the IP &lt;br&gt;
address, timestamp, URL, and status code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Our Python daemon&lt;/strong&gt; reads those log entries in real time, &lt;br&gt;
learns what normal traffic looks like, and fires when something &lt;br&gt;
looks wrong.&lt;/p&gt;


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

&lt;p&gt;Here's the core question our system needs to answer at any moment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How many requests did this IP make in the last 60 seconds?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;We use a data structure called a &lt;strong&gt;deque&lt;/strong&gt; (double-ended queue) &lt;br&gt;
to answer this efficiently.&lt;/p&gt;

&lt;p&gt;Think of it like a &lt;strong&gt;conveyor belt&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New items (request timestamps) come in from the right&lt;/li&gt;
&lt;li&gt;Old items (timestamps older than 60 seconds) fall off the left automatically
&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="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;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&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;timestamp&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Add new request timestamp to 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;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Remove old timestamps from LEFT
&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;timestamp&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;seconds&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="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;# Length = requests in last 60 seconds
&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;&lt;code&gt;popleft()&lt;/code&gt; is O(1) — it removes from the front instantly. &lt;br&gt;
This is why we use deque instead of a regular list — lists &lt;br&gt;
are slow at removing from the front.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;One &lt;strong&gt;per IP&lt;/strong&gt; — catches single aggressive attackers&lt;/li&gt;
&lt;li&gt;One &lt;strong&gt;global&lt;/strong&gt; — catches distributed attacks from many IPs&lt;/li&gt;
&lt;/ul&gt;


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

&lt;p&gt;Knowing the current rate isn't enough. We need to know if &lt;br&gt;
that rate is &lt;strong&gt;normal or abnormal&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;We solve this with a &lt;strong&gt;rolling 30-minute baseline&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;Every second, we record how many requests arrived in that second. &lt;br&gt;
We keep a 30-minute history of these per-second counts. &lt;br&gt;
Every 60 seconds, we calculate:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mean&lt;/strong&gt; — the average requests per second:&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;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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Standard Deviation&lt;/strong&gt; — how much the traffic usually varies:&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;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;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;We apply &lt;strong&gt;floor values&lt;/strong&gt; to both — mean never drops below 1.0 &lt;br&gt;
and stddev never drops below 0.5. This prevents false alarms &lt;br&gt;
when traffic is extremely stable.&lt;/p&gt;

&lt;p&gt;We also store baselines in &lt;strong&gt;per-hour slots&lt;/strong&gt;. Traffic at 3pm &lt;br&gt;
looks different from traffic at 3am — so we prefer the current &lt;br&gt;
hour's baseline when making decisions.&lt;/p&gt;


&lt;h2&gt;
  
  
  How The Detection Logic Makes A Decision
&lt;/h2&gt;

&lt;p&gt;With the current rate and the baseline established, we calculate &lt;br&gt;
a &lt;strong&gt;z-score&lt;/strong&gt;:&lt;br&gt;
z = (current_rate - baseline_mean) / baseline_stddev&lt;br&gt;
The z-score answers: &lt;strong&gt;"How many standard deviations above &lt;br&gt;
normal is this?"&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Z-score&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1.0&lt;/td&gt;
&lt;td&gt;Slightly above normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.0&lt;/td&gt;
&lt;td&gt;Noticeably above normal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3.0&lt;/td&gt;
&lt;td&gt;Very unusual — only 0.3% of traffic&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.0+&lt;/td&gt;
&lt;td&gt;Almost certainly an attack&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We flag an IP as anomalous if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;z-score &amp;gt; 3.0&lt;/strong&gt; (statistical threshold), OR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;rate &amp;gt; 5x the baseline mean&lt;/strong&gt; (simple multiplier)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Whichever fires first wins. This dual-trigger approach catches &lt;br&gt;
both gradual ramp-up attacks (caught by z-score) and sudden &lt;br&gt;
flood attacks (caught by the multiplier).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error surge detection:&lt;/strong&gt; If an IP is generating a lot of &lt;br&gt;
4xx/5xx errors — like trying hundreds of wrong passwords — &lt;br&gt;
we tighten its detection thresholds by 30%. It's already &lt;br&gt;
behaving suspiciously, so we watch it more closely.&lt;/p&gt;


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

&lt;p&gt;When an IP is flagged, we run this Linux firewall command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables &lt;span class="nt"&gt;-I&lt;/span&gt; INPUT &lt;span class="nt"&gt;-s&lt;/span&gt; 1.2.3.4 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking it down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;iptables&lt;/code&gt; — the Linux kernel firewall tool&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-I INPUT&lt;/code&gt; — INSERT a rule into the INPUT chain (incoming traffic)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-s 1.2.3.4&lt;/code&gt; — match packets from this SOURCE IP&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-j DROP&lt;/code&gt; — silently DROP all matching packets&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;DROP&lt;/strong&gt; means the attacker gets absolutely no response. &lt;br&gt;
Their packets just disappear. They don't even know they've &lt;br&gt;
been blocked — they just stop getting responses.&lt;/p&gt;

&lt;p&gt;We call this from Python using subprocess:&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="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&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;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;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&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="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&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="nf"&gt;print&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;Successfully blocked &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="si"&gt;}&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;&lt;strong&gt;Progressive ban schedule&lt;/strong&gt; — repeat offenders get longer bans:&lt;/p&gt;

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

&lt;p&gt;When a ban expires, we delete the rule:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;iptables &lt;span class="nt"&gt;-D&lt;/span&gt; INPUT &lt;span class="nt"&gt;-s&lt;/span&gt; 1.2.3.4 &lt;span class="nt"&gt;-j&lt;/span&gt; DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;p&gt;The dashboard is a Flask web server running in a background thread. &lt;br&gt;
It serves an HTML page that calls a &lt;code&gt;/api/stats&lt;/code&gt; endpoint every &lt;br&gt;
3 seconds and updates the display with fresh data.&lt;/p&gt;

&lt;p&gt;It shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global requests per second&lt;/li&gt;
&lt;li&gt;Current baseline mean and stddev&lt;/li&gt;
&lt;li&gt;All banned IPs with ban details&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;System uptime&lt;/li&gt;
&lt;li&gt;Hourly baseline slots&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Async Python is powerful&lt;/strong&gt; — running log monitoring, &lt;br&gt;
baseline calculation, ban checking, and serving a dashboard &lt;br&gt;
simultaneously with asyncio.gather() is elegant and efficient.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Read the logs&lt;/strong&gt; — when the Nextcloud container had issues, &lt;br&gt;
the logs told us exactly what was wrong and how to fix it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Never hardcode secrets&lt;/strong&gt; — GitHub Push Protection caught &lt;br&gt;
our Slack webhook URL in the code. Always use environment &lt;br&gt;
variables for secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Docker volumes are the glue&lt;/strong&gt; — the named HNG-nginx-logs &lt;br&gt;
volume is what allows Nginx and our detector (in separate &lt;br&gt;
containers) to share log files seamlessly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Z-scores are surprisingly simple&lt;/strong&gt; — statistical anomaly &lt;br&gt;
detection sounds intimidating but the math is just subtraction &lt;br&gt;
and division.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Building this system taught me that security tooling isn't magic — &lt;br&gt;
it's just careful observation, smart math, and fast response. &lt;br&gt;
The same principles used here are what power enterprise security &lt;br&gt;
tools at companies like Cloudflare and AWS.&lt;/p&gt;

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

&lt;p&gt;If you have questions or suggestions, drop them in the comments!&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>cybersecurity</category>
      <category>monitoring</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
