<?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: Ian alex</title>
    <description>The latest articles on Forem by Ian alex (@ik_again).</description>
    <link>https://forem.com/ik_again</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%2F758309%2Fc9c69cf6-5597-4947-9e34-ecd68a5f2cdd.jpg</url>
      <title>Forem: Ian alex</title>
      <link>https://forem.com/ik_again</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/ik_again"/>
    <language>en</language>
    <item>
      <title>How I Built a DDoS Detection Tool From Scratch Using Go</title>
      <dc:creator>Ian alex</dc:creator>
      <pubDate>Wed, 29 Apr 2026 22:27:39 +0000</pubDate>
      <link>https://forem.com/ik_again/how-i-built-a-ddos-detection-tool-from-scratch-using-go-1212</link>
      <guid>https://forem.com/ik_again/how-i-built-a-ddos-detection-tool-from-scratch-using-go-1212</guid>
      <description>&lt;p&gt;&lt;em&gt;A beginner-friendly guide to building an anomaly detection engine that watches web traffic in real time and automatically blocks attackers.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;What This Project Does (And Why It Matters)&lt;/strong&gt;&lt;br&gt;
Imagine you run a website. Most days, you get maybe 50 visitors per hour. One afternoon, a single computer starts sending 10,000 requests per second. Your server slows to a crawl. Real users can't load the page. This is a DDoS attack — Distributed Denial of Service — and it's one of the most common threats on the internet.&lt;br&gt;
My project is a daemon (a program that runs continuously in the background) that watches all incoming HTTP traffic to a Nextcloud server, learns what "normal" looks like, and automatically blocks IPs that behave abnormally. Think of it like a bouncer at a club who knows how busy a normal Friday night is and starts turning people away when the crowd gets suspiciously large — or kicks out the one person who keeps trying to barge through the door 100 times a minute.&lt;br&gt;
The entire system is built in Go and runs alongside the web server inside Docker containers.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The Architecture at a Glance&lt;/strong&gt;&lt;br&gt;
Here's how the pieces fit together:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet Traffic
       |
       v
+------------------+
|      Nginx       |  ──&amp;gt; writes JSON access log
|   (reverse proxy)|       to /var/log/nginx/hng-access.log
+--------+---------+
         |
         v
+------------------+
|    Nextcloud     |   (the actual web app)
+------------------+

Meanwhile, running alongside...

+------------------------------------------+
|       Anomaly Detector Daemon            |
|                                          |
|  1. monitor.go   → reads the log file   |
|  2. detector.go  → counts requests      |
|  3. baseline.go  → learns "normal"      |
|  4. blocker.go   → bans bad IPs         |
|  5. unbanner.go  → lifts bans later     |
|  6. notifier.go  → alerts via Slack     |
|  7. dashboard.go → live web UI          |
+------------------------------------------+
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nginx writes every request as a JSON line to a log file. My detector tails that file (like running &lt;code&gt;tail -f&lt;/code&gt; in a terminal), parses each line, and feeds it into the detection engine. Let me walk through each core concept.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How the Sliding Window Works&lt;/strong&gt;&lt;br&gt;
The first question the detector needs to answer is: how fast is this IP sending requests right now?&lt;/p&gt;

&lt;p&gt;To answer that, I use a sliding window — a list that only keeps events from the last 60 seconds. Here's how it works in plain language:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Every time a request comes in from an IP, I record the timestamp.&lt;/li&gt;
&lt;li&gt;Before counting, I throw away any timestamps older than 60 seconds.&lt;/li&gt;
&lt;li&gt;The number of timestamps left = the number of requests in the last minute.&lt;/li&gt;
&lt;li&gt;Divide by 60 = requests per second.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;In code, this looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="n"&gt;SlidingWindow&lt;/span&gt; &lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;windowSeconds&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;        &lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="kt"&gt;float64&lt;/span&gt;  &lt;span class="c"&gt;// timestamps&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SlidingWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&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="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evict&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="c"&gt;// remove old entries&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SlidingWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&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="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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;i&lt;/span&gt;&lt;span class="o"&gt;++&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;i&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c"&gt;// chop off the old ones&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;SlidingWindow&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;Rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evict&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="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="m"&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="m"&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="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="kt"&gt;float64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;windowSeconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Per-IP:&lt;/strong&gt; Each IP address gets its own window. This catches a single attacker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Global:&lt;/strong&gt; One window for all traffic combined. This catches distributed attacks where many IPs flood you at once.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why not just count requests per minute?&lt;/strong&gt; Because a per-minute counter resets at minute boundaries. If an attacker sends 500 requests in the last 5 seconds of one minute and 500 in the first 5 seconds of the next, a per-minute counter would show 500 each minute. The sliding window correctly shows 1000 in 10 seconds — much more suspicious.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How the Baseline Learns From Traffic&lt;/strong&gt;&lt;br&gt;
Knowing the current rate isn't enough. Is 10 requests per second a lot? For Google, that's nothing. For a personal blog, that's an attack. The detector needs to learn what your normal traffic looks like.&lt;br&gt;
This is where the rolling baseline comes in.&lt;br&gt;
Every second, I record the total request count. I keep 30 minutes of these per-second counts. Every 60 seconds, I calculate two numbers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mean:&lt;/strong&gt; The average requests per second over the last 30 minutes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standard deviation (stddev):&lt;/strong&gt; How much the traffic varies from that average.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your server normally gets 2 requests per second, with occasional bumps to 5, the mean might be 2.5 and the stddev might be 1.2.&lt;br&gt;
Here's the clever part: I also maintain per-hour slots. Traffic at 3am is very different from traffic at 3pm. If I have enough data for the current hour, I use that hour's baseline instead of the global 30-minute window. This prevents the detector from thinking normal afternoon traffic is an attack just because the morning was quiet.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// Prefer current hour's slot if it has enough data&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hourlySlots&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;currentHour&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Size&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;rawMean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slot&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="n"&gt;rawStddev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;slot&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StdDev&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;// Fall back to full rolling window&lt;/span&gt;
    &lt;span class="c"&gt;// ... calculate from all data&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I also enforce floor values — the mean never goes below 1.0 and stddev never below 0.5. Without these floors, a server with zero traffic would have a mean of 0, and any single request would look like an anomaly. That would be useless.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;How the Detection Logic Makes a Decision&lt;/strong&gt;&lt;br&gt;
Now we have two pieces of information:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Current rate&lt;/strong&gt; for an IP (from the sliding window)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normal rate&lt;/strong&gt; for the server (from the baseline)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The detector flags traffic as anomalous if either of these conditions is true:&lt;br&gt;
&lt;strong&gt;Condition 1: Z-Score &amp;gt; 3.0&lt;/strong&gt;&lt;br&gt;
The z-score answers: "How many standard deviations away from normal is this?"&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;z-score = (current_rate - mean) / stddev&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;If the mean is 2.5 req/s and stddev is 1.2, and an IP is sending 8 req/s:&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;&lt;code&gt;z-score = (8 - 2.5) / 1.2 = 4.58&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;p&gt;That's way above 3.0 — anomaly detected.&lt;br&gt;
In statistics, a z-score above 3 means the value is in the top 0.1% — it almost certainly isn't normal traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Condition 2: Rate &amp;gt; 5x the Baseline Mean&lt;/strong&gt;&lt;br&gt;
This is a simpler check. If normal traffic is 2 req/s and an IP is sending 15 req/s, that's 7.5x the baseline — clearly suspicious.&lt;br&gt;
Why have both? Because if the stddev is very low (traffic is extremely consistent), the z-score threshold becomes very sensitive. The 5x multiplier acts as a sanity check.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Bonus: Error Surge Tightening&lt;/strong&gt;&lt;br&gt;
If an IP is generating a lot of 4xx and 5xx errors (failed login attempts, scanning for vulnerabilities), the detector automatically halves the thresholds. Now the z-score threshold drops from 3.0 to 1.5, and the rate multiplier drops from 5x to 2.5x. This catches attackers who are probing your server even if their request rate isn't extremely high.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;How iptables Blocks an IP&lt;/strong&gt;&lt;br&gt;
When the detector identifies a bad IP, it needs to actually stop that traffic from reaching the server. This is where &lt;code&gt;iptables&lt;/code&gt; comes in.&lt;br&gt;
&lt;code&gt;iptables&lt;/code&gt; is the Linux firewall. It's a set of rules that tell the operating system what to do with incoming network packets. The two commands that matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ban an IP:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iptables -A INPUT -s 203.0.113.42 -j DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;br&gt;
This says: "Append a rule to the INPUT chain: any packet from source 203.0.113.42, DROP it." The traffic never reaches Nginx, never reaches Nextcloud. It's as if that IP doesn't exist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unban an IP:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;iptables -D INPUT -s 203.0.113.42 -j DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;Same rule, but &lt;code&gt;-D&lt;/code&gt; (delete) instead of &lt;code&gt;-A&lt;/code&gt; (append).&lt;/p&gt;

&lt;p&gt;In Go, I run these commands using &lt;code&gt;os/exec&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;func addIptablesRule(ip string) bool {
    cmd := exec.Command("iptables", "-A", "INPUT", "-s", ip, "-j", "DROP")
    _, err := cmd.CombinedOutput()
    return err == nil
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;&lt;br&gt;
plaintext&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Ban Escalation Schedule&lt;/strong&gt;&lt;br&gt;
Not every attacker deserves a permanent ban. Maybe it was a misconfigured bot, not a malicious attack. So bans follow an escalating schedule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;*&lt;em&gt;Ban *&lt;/em&gt;
&lt;/th&gt;
&lt;th&gt;*&lt;em&gt;Duration *&lt;/em&gt;
&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1st&lt;/td&gt;
&lt;td&gt;2 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2nd&lt;/td&gt;
&lt;td&gt;30 minutes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3rd&lt;/td&gt;
&lt;td&gt;2 hours&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4th+&lt;/td&gt;
&lt;td&gt;Permanent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;An unbanner goroutine checks every 10 seconds for expired bans and removes them. If the same IP triggers detection again, the next ban is longer. By the 4th offense, they're blocked permanently.&lt;/p&gt;



&lt;p&gt;&lt;strong&gt;The Dashboard&lt;/strong&gt;&lt;br&gt;
Everything above runs silently. To see what's happening in real time, the daemon serves a web dashboard that auto-refreshes every 3 seconds. It shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Global requests per second — how busy the server is right now&lt;/li&gt;
&lt;li&gt;Banned IPs — who's blocked and for how long&lt;/li&gt;
&lt;li&gt;Top 10 source IPs — who's sending the most traffic&lt;/li&gt;
&lt;li&gt;CPU and memory usage — system health&lt;/li&gt;
&lt;li&gt;Baseline stats — the current effective mean and standard deviation&lt;/li&gt;
&lt;li&gt;Hourly slots — how traffic patterns differ by hour&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The dashboard is built with Go's built-in net/http server and plain HTML with inline CSS. No JavaScript frameworks, no build tools. The auto-refresh is a single HTML meta tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;meta http-equiv="refresh" content="3"&amp;gt;.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Statistics has practical uses.&lt;/strong&gt; Mean, standard deviation, and z-score aren't just textbook concepts — they're the foundation of anomaly detection in production systems.
2.** Go's concurrency model is elegant.** Each component (log monitor, unbanner, dashboard) runs in its own goroutine. They communicate through channels. No complex threading code needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker networking is tricky.&lt;/strong&gt; The detector needs network_mode: host to run iptables on the host's network stack. Without it, iptables rules only affect the container's isolated network — useless for blocking real attackers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Disk space matters.&lt;/strong&gt; Access logs grow fast under heavy traffic. I learned this the hard way when my server ran out of space multiple times during testing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't hardcode baselines.&lt;/strong&gt; A static threshold like "ban anyone over 100 req/s" would either miss slow attacks or block legitimate traffic spikes. The rolling baseline adapts to whatever your actual traffic looks like.&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;&lt;strong&gt;Try It Yourself&lt;/strong&gt;&lt;br&gt;
The full source code is available on GitHub: &lt;a href="https://github.com/ik-alex/HNG14-Stage3-Devops.git" rel="noopener noreferrer"&gt;https://github.com/ik-alex/HNG14-Stage3-Devops.git&lt;/a&gt;&lt;br&gt;
The live dashboard is at: &lt;a href="http://ikalex.duckdns.org:5000" rel="noopener noreferrer"&gt;http://ikalex.duckdns.org:5000&lt;/a&gt;&lt;br&gt;
If you want to understand security tooling, I'd recommend starting with just the sliding window. Write a small program that counts events per second using a deque. Once that clicks, everything else builds on top of it.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>cybersecurity</category>
      <category>go</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My First Task In HNG Internship</title>
      <dc:creator>Ian alex</dc:creator>
      <pubDate>Wed, 03 Jul 2024 00:40:39 +0000</pubDate>
      <link>https://forem.com/ik_again/my-first-task-in-hng-internship-12f6</link>
      <guid>https://forem.com/ik_again/my-first-task-in-hng-internship-12f6</guid>
      <description>&lt;p&gt;I signup for an internship program named HNG. It is expected that the intern should have an intermediate to advance experience for any track they wish to participate in. For more information regarding the internship, your can follow this link &lt;a href="https://hng.tech/internship"&gt;https://hng.tech/internship&lt;/a&gt; and applying for a job at HNG you can also checkout this link &lt;a href="https://hng.tech/hire"&gt;https://hng.tech/hire&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;Task 1: We were tasked to write a script named create_user.sh for creating a user and adding the user to a group via reading from an input file.&lt;/p&gt;






&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash
# Log file and password file
PASSWORD_FILE="/var/secure/user_passwords.txt"
LOG_FILE="/var/log/user_management.log"
# ensure to check if the number of argument provided is 1
# if !true exit running the entire codebase
if [ $# -ne 1 ]; then
    echo "Usage: $0 &amp;lt;input_textfile&amp;gt;" | sudo tee -a $LOG_FILE
    exit 1
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Considering the above code block;&lt;br&gt;
&lt;code&gt;#!/bin/bash&lt;/code&gt; the shebang declaration specifying that this file is a bash script.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PASSWORD_FILE="/var/secure/user_passwords.txt"
LOG_FILE="/var/log/user_management.log"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the above block of code assigns the path &lt;code&gt;/var/secure/user_passwords.txt&lt;/code&gt; to variable &lt;code&gt;PASSWORD_FILE&lt;/code&gt; and path &lt;code&gt;/var/log/user_management.log&lt;/code&gt; to variable &lt;code&gt;LOG_FILE&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if [ $# -ne 1 ]; then
    echo "This is how to run the script: $0 &amp;lt;input_textfile&amp;gt;" | sudo tee -a $LOG_FILE
    exit 1
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The above block of code checks if only argument is passed to the script.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;$# -ne 1&lt;/code&gt; checks if the number of argument passed is not equal to one and prints the output to the terminal and also log the data.&lt;/li&gt;
&lt;li&gt;else if the condition doesn't hold true it exits the block of the code.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if [ ! -f "$input_textfile" ]; then
    echo "Error: The file $input_textfile does not exists" | sudo tee -a $LOG_FILE
    exit 1
fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;! -f "$input_textfile&lt;/code&gt; this checks if an input file is not passed to the script it exits
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo chown root:root $PASSWORD_FILE
sudo mkdir -p /var/secure
sudo touch $PASSWORD_FILE
sudo chmod 600 $PASSWORD_FILE
sudo touch $LOG_FILE
sudo chmod 640 $LOG_FILE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This Create necessary directories such as $LOG_FILE $PASSWORD_FILE and set permissions such as making the $PASSWORD_FILE have root administrative privilege and setting the permission to read and write privilege. &lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo chmod 640 $LOG_FILE&lt;/code&gt; this ensure that the user has a read and write privilege and the group has only read privilege.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;generate_password() {
    &amp;lt; /dev/urandom tr -dc 'A-Za-z0-9!@#$%&amp;amp;*' | head -c 12
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This function is responsible for generating random password&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Read File&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;while IFS=';' read -r user groups; do
    if [ -z "$user" ] || [ -z "$groups" ]; then
        echo "Skipping invalid line: $user;$groups" | sudo tee -a $LOG_FILE
        continue
    fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Start a loop that reads a line from the $FILENAME, splits it into two parts separated by &lt;code&gt;;&lt;/code&gt; based on IFS.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read -r user groups&lt;/code&gt; Assign the first part to username and the remaining parts to groups.&lt;/li&gt;
&lt;li&gt;-z "$user" -z "$groups" checks to see if the user and group name is empty.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Creating users&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if id -u "$user" &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
        echo "This particular User $user exists" | sudo tee -a $LOG_FILE
    else
        sudo useradd -m "$user"
        if [ $? -eq 0 ]; then
            echo "User $user created" | sudo tee -a $LOG_FILE

            # Generating the random password for each user 
            password=$(generate_password)
            echo "$user,$password" | sudo tee -a $PASSWORD_FILE &amp;gt;/dev/null
            echo "$user:$password" | sudo chpasswd
            echo "User $user password is set" | sudo tee -a $LOG_FILE

            # Set appropriate permissions for the home directory
            sudo chmod 700 /home/$user
            sudo chown $user:$user /home/$user
            echo "Home directory for user $user set up with appropriate permissions" | sudo tee -a $LOG_FILE
        else
            echo "Failed to create user $user" | sudo tee -a $LOG_FILE
            continue
        fi
    fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;id -u "$user" &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt; this looks for the user id and suppress the standard output and error to &lt;code&gt;/dev/null&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sudo useradd -m "$user"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;useradd&lt;/code&gt;: This is the command used to add a new user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-m&lt;/code&gt;: This option tells useradd to create a home directory for the new user if it does not already exist. The home directory will be created in the /home/ directory and named after the user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"$user"&lt;/code&gt;: This is the username of the new user being created. The $user variable should contain the name of the user&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;[ $? -eq 0 ]&lt;/code&gt; this checks if the previous command successfully executed and 0 indicates success.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;password=$(generate_password)&lt;/code&gt; calls the generate_password function and assigns the result to the password variable.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo "$user,$password" | sudo tee -a $PASSWORD_FILE &amp;gt;/dev/null&lt;/code&gt; this suppresses the output due to the /dev/null&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo "$user:$password" | sudo chpasswd&lt;/code&gt; this allows the user to change password.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo "User $user password is set" | sudo tee -a $LOG_FILE&lt;/code&gt; displays the output to the terminal.
&lt;code&gt;sudo chmod 700 /home/$user&lt;/code&gt; gives the user a full privileged.
&lt;code&gt;sudo chown $user:$user /home/$user&lt;/code&gt; gives the owner of the directory to the user.
&lt;code&gt;else&lt;/code&gt; if the condition doesn't hold true it print the output of failed user creation to the terminal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Adding Users to Group&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;IFS=',' read -r -a group_array &amp;lt;&amp;lt;&amp;lt; "$groups"
    for group in "${group_array[@]}"; do
        if getent group "$group" &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
            sudo usermod -aG "$group" "$user"
            echo "User $user added to existing group $group" | sudo tee -a $LOG_FILE
        else
            sudo groupadd "$group"
            sudo usermod -aG "$group" "$user"
            echo "Group $group created and user $user added to it" | sudo tee -a $LOG_FILE
        fi
    done
done &amp;lt; "$input_textfile"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;-&lt;code&gt;IFS=',' read -r -a group_array &amp;lt;&amp;lt;&amp;lt; "$groups"&lt;/code&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;IFS=','&lt;/code&gt;: Sets the Internal Field Separator to a comma. This means the read command will split the input string based on commas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;read -r -a group_array &amp;lt;&amp;lt;&amp;lt; "$groups"&lt;/code&gt; Reads the group variable, splits it by comma and stores the value to the group_array.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;group=$(echo "$group" | xargs)&lt;/code&gt; this removes any leading whitespace in the group.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;for group in "${group_array[@]}"&lt;/code&gt; this loops through the group_array array and stores each iteration to group.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;if getent group "$group" &amp;gt;/dev/null 2&amp;gt;&amp;amp;1&lt;/code&gt; if the group exists in the system; also suppress the standard output and error.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo usermod -aG "$group" "$user"&lt;/code&gt; adds users to the existing group&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo groupadd "$group"&lt;/code&gt; this creates a new group.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sudo usermod -aG "$group" "$user"&lt;/code&gt; this adds user to the group&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo "Group $group created and user $user added to it" | sudo tee -a $LOG_FILE&lt;/code&gt; prints the output and log it into the log file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;done&lt;/code&gt; ends the for loop&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;done &amp;lt; "$input_textfile"&lt;/code&gt; ends the while loop that reads from the input file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;echo "User creation and group assignment created." | sudo tee -a $LOG_FILE&lt;/code&gt; outputting the finished the creation of users and group.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Running The Script&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;created the file named called &lt;code&gt;name-of-text-file.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;nano name-of-text-file.txt

#file content of the file
kachi; security, crypto, signals
dika; werey, genuis, smartkid
diamond; werey, soc, faith
chimummy; boss, theboss, smartguy
david; psycho, funny, jovial
faith; babe, babygirl, fine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;execute the script &lt;code&gt;create_userss.sh&lt;/code&gt; with the text file &lt;code&gt;name-of-text-file.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# making the script file to be executable
chmod +x create_userss.sh
# running the script
./create_userss.sh name-of-text-file.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;checking the LOG_FILE
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cat /var/log/user_management.log 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;The display output for the log file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct92terz4sznyh4v6qlh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fct92terz4sznyh4v6qlh.png" alt="Image description" width="437" height="412"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;checking the password file PASSWORD_FILE
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo cat /var/secure/user_passwords.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;This displayed output for the password file&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9q9nldvvx57ro7x6i1a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fj9q9nldvvx57ro7x6i1a.png" alt="Image description" width="170" height="145"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
