<?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: Wilfrid Okorie</title>
    <description>The latest articles on DEV Community by Wilfrid Okorie (@wilfridk).</description>
    <link>https://dev.to/wilfridk</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%2F1418202%2F5f5ff5d3-2ff9-40b9-bf7f-835cd7e01c01.jpeg</url>
      <title>DEV Community: Wilfrid Okorie</title>
      <link>https://dev.to/wilfridk</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/wilfridk"/>
    <language>en</language>
    <item>
      <title>How I Created a DDoS Protection Engine</title>
      <dc:creator>Wilfrid Okorie</dc:creator>
      <pubDate>Wed, 29 Apr 2026 20:33:14 +0000</pubDate>
      <link>https://dev.to/wilfridk/how-i-created-a-ddos-protection-engine-3kfg</link>
      <guid>https://dev.to/wilfridk/how-i-created-a-ddos-protection-engine-3kfg</guid>
      <description>&lt;p&gt;As part of my tasks in HNG14, track DevOps, stage 3, I was to build an engine to protect a live Nextcloud server from DDoS attacks, without using any existing security tools like Fail2Ban. &lt;/p&gt;

&lt;p&gt;In summary, this means writing a program that watches traffic in real time, learns what how regular traffic is, and automatically locks out attackers the moment something goes wrong i.e. in times of suspicious traffic spikes.&lt;/p&gt;

&lt;p&gt;This post explains exactly how I did it — in plain English, no security background required.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a DDoS Attack
&lt;/h2&gt;

&lt;p&gt;DDoS stands for &lt;strong&gt;Distributed Denial of Service&lt;/strong&gt;. Imagine there is a club that has regular traffic, say 10 people averagely entering every minute. A DDoS Attack would be an attacker sending 200 random people to stand in line, without entering so that honest people that want to enter the club are &lt;strong&gt;denied the service&lt;/strong&gt;.&lt;br&gt;
In this case, instead of fake customers, it is fake http requests, thousands per second to flood your server until your server cannot serve real requests anymore.&lt;br&gt;
The protection engine serves as the bouncer in such a case. &lt;/p&gt;
&lt;h2&gt;
  
  
  Architecture: The Pieces that Work Together
&lt;/h2&gt;

&lt;p&gt;In this setup, there are three pieces to focus on that work together. Here is a diagram of the flow:&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

- Nginx -&amp;gt; reverse proxy, logs everything to JSON

- Nextcloud -&amp;gt; the actual app, don't touch this

- The Detector Daemon -&amp;gt; reads Nginx logs continuously

The tool:
- Detects anomalies
- Blocks IPs via iptables
- Sends Slack alerts
- Serves a live dashboard
- Auto-unbans on schedule
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The idea is that Nextcloud runs behind Nginx (which is a web server, acting as a gatekeeper).&lt;br&gt;
To every request that comes in, Nginx logs it in real time. The tool is an adaptable tool that reads the logs in real time as they come, calculate the average rate of requests at a particular period, to understand what normal is, and then react to suspicious spikes in this rate. The reaction is automatically blocking attackers.&lt;br&gt;
In addition, there is a live web dashboard showing what is happening.&lt;/p&gt;

&lt;p&gt;Here are the steps to building such a tool:&lt;/p&gt;


&lt;h3&gt;
  
  
  1: Set Up Your VPS
&lt;/h3&gt;

&lt;p&gt;Go to your cloud provider (I used AWS). Create an EC2 instance (or a "Virtual machine", or whatever it is called with your cloud provider) with at least &lt;strong&gt;2 vCPUs and 2GB RAM&lt;/strong&gt;. Start the instance, and copy the Public IPv4.&lt;/p&gt;

&lt;p&gt;With your instance running, SSH into it and install the tools you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt upgrade &lt;span class="nt"&gt;-y&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; docker.io docker compose git
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also open these ports in your cloud firewall (AWS calls it a Security Group):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;22&lt;/strong&gt; - SSH&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;80&lt;/strong&gt; - HTTP (Nginx/Nextcloud)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;443&lt;/strong&gt; - HTTPS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;5000&lt;/strong&gt; - Your detector dashboard&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Point a domain or subdomain at your server's public IP - you'll need this for the dashboard URL. If your IP changes after restarting the instance (it will on AWS unless you use an Elastic IP), update your DNS A record.&lt;/p&gt;

&lt;p&gt;Problems I faced at this step:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The default Ubuntu image comes with &lt;code&gt;containerd&lt;/code&gt; already installed, which conflicts with &lt;code&gt;docker.io&lt;/code&gt;. Remove it first, then install Docker.&lt;/li&gt;
&lt;li&gt;I initially created one with less RAM and a tiny 8GB disk, and spent a lot of time debugging crashes that were simply caused by running out of memory and disk space. Save yourself the pain: start with a &lt;strong&gt;t3.small&lt;/strong&gt; (2 vCPU, 2GB RAM) and a &lt;strong&gt;16GB disk minimum&lt;/strong&gt; if you are using AWS.&lt;/li&gt;
&lt;li&gt;Don't forget to also allow port 5000 in UFW (Ubuntu's local firewall) - I missed this and spent time confused about why the dashboard wasn't accessible even though the Security Group was correct:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 5000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  2: Set Up the Docker Compose Stack
&lt;/h3&gt;

&lt;p&gt;I did all of this in my Python codebase, and pulled from git from within my EC2 instance, but besides the code, here is what you need:&lt;/p&gt;

&lt;p&gt;Create your project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/hng_devops_stage_3/nginx
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/hng_devops_stage_3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.9"&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;HNG-nginx-logs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# nginx writes here, detector reads here&lt;/span&gt;
  &lt;span class="na"&gt;detector-audit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;      &lt;span class="c1"&gt;# persists audit logs across restarts&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

  &lt;span class="na"&gt;nginx&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./nginx/nginx.conf:/etc/nginx/nginx.conf:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HNG-nginx-logs:/var/log/nginx&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt;

  &lt;span class="na"&gt;nextcloud&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;kefaslungu/hng-nextcloud&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HNG-nginx-logs:/var/log/nginx:ro&lt;/span&gt;

  &lt;span class="na"&gt;detector&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./detector&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;network_mode&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;host&lt;/span&gt;        &lt;span class="c1"&gt;# required for iptables to affect host firewall&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;            &lt;span class="c1"&gt;# contains your Slack webhook URL&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;HNG-nginx-logs:/var/log/nginx:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config.yaml:/app/config.yaml:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;detector-audit:/var/log/detector&lt;/span&gt;
    &lt;span class="na"&gt;cap_add&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;NET_ADMIN&lt;/span&gt;             &lt;span class="c1"&gt;# required to run iptables commands&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;nginx&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;DETECTOR_CONFIG=/app/config.yaml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are some things to note:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;network_mode: host&lt;/code&gt;?&lt;/strong&gt; Docker containers normally have their own isolated network. But &lt;code&gt;iptables&lt;/code&gt; rules you add inside a container only affect that container, not the actual host machine. With &lt;code&gt;host&lt;/code&gt; networking, the container shares the host's network stack, so iptables rules you add actually block traffic at the server level. Without this, your bans do nothing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;cap_add: NET_ADMIN&lt;/code&gt;?&lt;/strong&gt; By default, containers can't modify firewall rules, since that is a privileged operation. This capability grants exactly the permission needed, and nothing more.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Why a named volume &lt;code&gt;HNG-nginx-logs&lt;/code&gt;?&lt;/strong&gt; This is the shared pipe between Nginx and your detector. Nginx writes logs into it. Your detector reads from it. The name must be exactly &lt;code&gt;HNG-nginx-logs&lt;/code&gt;, since the task requires it.&lt;/p&gt;

&lt;p&gt;When your disk fills up (and it will if you're not careful - the Nextcloud image alone is over 1GB), clean unused Docker data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker system prune &lt;span class="nt"&gt;-f&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you resize your cloud disk, remember to extend the filesystem too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;growpart /dev/nvme0n1 1
&lt;span class="nb"&gt;sudo &lt;/span&gt;resize2fs /dev/nvme0n1p1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  3: Configure Nginx
&lt;/h3&gt;

&lt;p&gt;I also did this in my IDE so it would appear when I pulled from github.&lt;/p&gt;

&lt;p&gt;Create &lt;code&gt;nginx/nginx.conf&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;user&lt;/span&gt;  &lt;span class="s"&gt;nginx&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;worker_processes&lt;/span&gt;  &lt;span class="s"&gt;auto&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;error_log&lt;/span&gt;  &lt;span class="n"&gt;/var/log/nginx/error.log&lt;/span&gt; &lt;span class="s"&gt;warn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;pid&lt;/span&gt;        &lt;span class="n"&gt;/var/run/nginx.pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;events&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;worker_connections&lt;/span&gt;  &lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;http&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;include&lt;/span&gt;       &lt;span class="n"&gt;/etc/nginx/mime.types&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;default_type&lt;/span&gt;  &lt;span class="nc"&gt;application/octet-stream&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# JSON log format — every field the detector needs&lt;/span&gt;
    &lt;span class="kn"&gt;log_format&lt;/span&gt; &lt;span class="s"&gt;json_log&lt;/span&gt; &lt;span class="s"&gt;escape=json&lt;/span&gt;
        &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="kn"&gt;'&lt;/span&gt;
            &lt;span class="s"&gt;'"source_ip":"&lt;/span&gt;&lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="s"&gt;",'&lt;/span&gt;
            &lt;span class="s"&gt;'"timestamp":"&lt;/span&gt;&lt;span class="nv"&gt;$time_iso8601&lt;/span&gt;&lt;span class="s"&gt;",'&lt;/span&gt;
            &lt;span class="s"&gt;'"method":"&lt;/span&gt;&lt;span class="nv"&gt;$request_method&lt;/span&gt;&lt;span class="s"&gt;",'&lt;/span&gt;
            &lt;span class="s"&gt;'"path":"&lt;/span&gt;&lt;span class="nv"&gt;$request_uri&lt;/span&gt;&lt;span class="s"&gt;",'&lt;/span&gt;
            &lt;span class="s"&gt;'"status":&lt;/span&gt;&lt;span class="nv"&gt;$status&lt;/span&gt;&lt;span class="s"&gt;,'&lt;/span&gt;
            &lt;span class="s"&gt;'"response_size":&lt;/span&gt;&lt;span class="nv"&gt;$body_bytes_sent&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;
        &lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="err"&gt;}&lt;/span&gt;&lt;span class="s"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;access_log&lt;/span&gt; &lt;span class="n"&gt;/var/log/nginx/hng-access.log&lt;/span&gt; &lt;span class="s"&gt;json_log&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Trust X-Forwarded-For so real client IPs are logged&lt;/span&gt;
    &lt;span class="kn"&gt;real_ip_header&lt;/span&gt;    &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;set_real_ip_from&lt;/span&gt;  &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="s"&gt;.0.0/0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;sendfile&lt;/span&gt;       &lt;span class="no"&gt;on&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;keepalive_timeout&lt;/span&gt;  &lt;span class="mi"&gt;65&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="kn"&gt;upstream&lt;/span&gt; &lt;span class="s"&gt;nextcloud&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="nf"&gt;nextcloud&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;_&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

        &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_pass&lt;/span&gt;         &lt;span class="s"&gt;http://nextcloud&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;Host&lt;/span&gt;              &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;X-Real-IP&lt;/span&gt;         &lt;span class="nv"&gt;$remote_addr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;X-Forwarded-For&lt;/span&gt;   &lt;span class="nv"&gt;$proxy_add_x_forwarded_for&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt;   &lt;span class="s"&gt;X-Forwarded-Proto&lt;/span&gt; &lt;span class="nv"&gt;$scheme&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

            &lt;span class="kn"&gt;client_max_body_size&lt;/span&gt;    &lt;span class="mi"&gt;10G&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;# allow large file uploads&lt;/span&gt;
            &lt;span class="kn"&gt;proxy_request_buffering&lt;/span&gt; &lt;span class="no"&gt;off&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&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;Two important things here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;JSON logs&lt;/strong&gt;: the detector parses these logs line by line. They must be valid JSON. The &lt;code&gt;escape=json&lt;/code&gt; directive ensures special characters in URLs don't break the JSON structure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real IP forwarding&lt;/strong&gt;: without &lt;code&gt;real_ip_header X-Forwarded-For&lt;/code&gt;, every log entry shows Nginx's internal Docker IP instead of the actual visitor's IP. Your detector would see every request coming from the same internal address and never identify real attackers.&lt;/p&gt;

&lt;p&gt;Test Nginx is working before moving on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; nginx nextcloud
curl http://YOUR_SERVER_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the Nextcloud setup page. If you see a 502 error, Nextcloud is still starting up; wait 30 seconds and try again. If you get a port conflict error, something else is using port 80:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl stop nginx    &lt;span class="c"&gt;# stop any system nginx&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl disable nginx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h3&gt;
  
  
  4: Build the Detector App
&lt;/h3&gt;

&lt;p&gt;Your detector lives in a &lt;code&gt;detector/&lt;/code&gt; folder and is made up of several Python files, each with a single responsibility. Here's what each one does and why it exists:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;config.py&lt;/code&gt;&lt;/strong&gt;: loads &lt;code&gt;config.yaml&lt;/code&gt; and environment variables. All thresholds live here. Nothing is hardcoded anywhere else in the codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;monitor.py&lt;/code&gt;&lt;/strong&gt;: tails the Nginx log file line by line, exactly like &lt;code&gt;tail -f&lt;/code&gt; in your terminal. Every new line gets parsed from JSON and fed into the sliding windows. This runs in its own thread continuously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;baseline.py&lt;/code&gt;&lt;/strong&gt;: keeps a 30-minute rolling history of per-second request counts. Every 60 seconds it recalculates the mean and standard deviation. Maintains per-hour slots so peak-hour traffic doesn't distort off-peak baselines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;detector.py&lt;/code&gt;&lt;/strong&gt;: evaluates current request rates against the baseline. Fires if z-score exceeds 3.0 or rate exceeds 5x the mean. Tightens thresholds for IPs with high error rates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;blocker.py&lt;/code&gt;&lt;/strong&gt;: executes &lt;code&gt;iptables&lt;/code&gt; to block flagged IPs and records the ban.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;unbanner.py&lt;/code&gt;&lt;/strong&gt;: runs on a schedule, checks expired bans, removes iptables rules, and escalates the backoff level for repeat offenders.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;notifier.py&lt;/code&gt;&lt;/strong&gt;: sends HTTP POST requests to your Slack webhook with ban/unban/global alert details.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;dashboard.py&lt;/code&gt;&lt;/strong&gt;: a Flask web server serving a live metrics page that refreshes every 3 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;main.py&lt;/code&gt;&lt;/strong&gt;: the entry point. Starts all threads and keeps the daemon running.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;config.yaml&lt;/code&gt; holds all the tunable values:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;slack_webhook_url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${SLACK_WEBHOOK_URL}"&lt;/span&gt;   &lt;span class="c1"&gt;# loaded from .env at runtime&lt;/span&gt;

&lt;span class="na"&gt;zscore_threshold&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.0&lt;/span&gt;
&lt;span class="na"&gt;rate_multiplier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5.0&lt;/span&gt;
&lt;span class="na"&gt;error_rate_multiplier&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3.0&lt;/span&gt;

&lt;span class="na"&gt;sliding_window_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;
&lt;span class="na"&gt;baseline_window_minutes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;30&lt;/span&gt;
&lt;span class="na"&gt;baseline_recalc_interval_seconds&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;60&lt;/span&gt;

&lt;span class="na"&gt;dashboard_port&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5000&lt;/span&gt;
&lt;span class="na"&gt;log_file_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/log/nginx/hng-access.log"&lt;/span&gt;
&lt;span class="na"&gt;audit_log_path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/log/detector/audit.log"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the Slack webhook URL, never put the real URL in your config file if your repo is public. Instead, I created a &lt;code&gt;.env&lt;/code&gt; file on my server (which I added to &lt;code&gt;.gitignore&lt;/code&gt;) and let Docker inject it as an environment variable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.env&lt;/strong&gt; (on your server only, never committed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;SLACK_WEBHOOK_URL&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;https://hooks.slack.com/services/YOUR/REAL/WEBHOOK&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;Dockerfile&lt;/code&gt; for the detector:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.11-slim&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . ./detector/&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "-m", "detector.main"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To bring the full stack up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; detector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the Flask dashboard starting and log lines being processed. If you see &lt;code&gt;No space left on device&lt;/code&gt;, clean up Docker and resize your disk as described in section 2.&lt;/p&gt;




&lt;h3&gt;
  
  
  5: Audit Log
&lt;/h3&gt;

&lt;p&gt;Every significant action the detector takes gets written to a structured audit log at &lt;code&gt;/var/log/detector/audit.log&lt;/code&gt;. The format is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[timestamp] ACTION ip | condition | rate | baseline | duration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real examples from my running system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;[2026-04-28T08:57:09Z] BAN ip=102.90.99.58 | condition=zscore | rate=1.2/s | baseline=1.0/s | duration=600s
[2026-04-28T09:07:28Z] UNBAN ip=102.90.99.58 | condition=backoff-0 | rate=N/A | baseline=1.0/s | duration=1800s
[2026-04-28T09:00:00Z] BASELINE_RECALC ip=global | mean=1.0 | stddev=0.06
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To read it live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker ps &lt;span class="nt"&gt;-qf&lt;/span&gt; &lt;span class="s2"&gt;"name=detector"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; /var/log/detector/audit.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;detector-audit&lt;/code&gt; Docker volume means this log survives container restarts — if your detector crashes and restarts, the full ban history is still there. This matters because the unbanner needs ban history to know which backoff level to apply next.&lt;/p&gt;




&lt;h3&gt;
  
  
  6: Set Up Slack
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://api.slack.com/apps" rel="noopener noreferrer"&gt;api.slack.com/apps&lt;/a&gt; and &lt;strong&gt;Create New App&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Give it a name (Mine was "HNG DDoS Protection Engine") and pick your workspace&lt;/li&gt;
&lt;li&gt;In the left sidebar, click &lt;strong&gt;Incoming Webhooks&lt;/strong&gt;, toggle it &lt;strong&gt;On&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add New Webhook to Workspace&lt;/strong&gt; → pick the channel you want alerts in, and &lt;strong&gt;Allow&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Copy the webhook URL that looks like &lt;code&gt;https://hooks.slack.com/services/T.../B.../...&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;On your server, add it to your &lt;code&gt;.env&lt;/code&gt; file:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"SLACK_WEBHOOK_URL=https://hooks.slack.com/services/YOUR/WEBHOOK"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; ~/hng_devops_stage_3/.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart the detector to pick it up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;docker compose restart detector
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test it's working by sending a flood of requests to trigger a ban:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;i &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;1..300&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do &lt;/span&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; http://YOUR_SERVER_IP/ &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within 10 seconds you should see a Slack message like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🚨 IP Banned
IP: YOUR_IP
Condition: zscore
Current Rate: 4.8 req/s
Baseline Mean: 1.0 req/s
Ban Duration: 600s
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait 10 minutes and you'll get the unban notification automatically. That confirms the full cycle — detection, blocking, alerting, and auto-unban — is working end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Here is How Some Components Work:
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sliding Window&lt;/strong&gt;: The problem this solves is, how do you measure requests per second in real time? A naive solution would be to count requests per minute, but that is static, if you measure the time against how long an attack takes i.e. the attacker could be done before your system is done counting a minute.&lt;br&gt;
Instead, imagine you have a stick, with a number of spots where items can sit. When things are placed, they move from one end of the stick to the other end, over 60 seconds. After the first 60 seconds, the number of items on the stick gives you your current rate. After the first 60 seconds are gone, items that have gotten to the other end fall off, and more items (this is an analogy for requests) come in.&lt;br&gt;
Whenever there is an attack, the number of items at the same time on the 60-second window stick would get abnormally high, and that is how you would know there is an attack.&lt;br&gt;
This is implemented in Python with a double-ended queue.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Baseline Mean&lt;/strong&gt;: The function of this is to learn from traffic. The sliding window is very good, but it would give false positives if in the first place, you don't know how many requests should be "too many". For a personal blog for instance, having 50 requests per second is a massive spike. For a large cloud platform, or a social media app, it is actually normal during peak hours. Therefore, you can't hardcode a number for this. The value has to be specific to your actual traffic patterns.&lt;br&gt;
Every second, the detector records how many requests came in, keeping a rolling 30-minute history of these per-second counts. Every 60 seconds, it recalculates two things:&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;Mean: the average requests per second over the last 30 minutes&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;ul&gt;
&lt;li&gt;Standard deviation: how much the rates typically varies from the mean
It also maintains per hour slots, so that peak times are separate from quiet times. 
Also, the baseline mean never drops below 1.0, to prevent floor division by zero. This means that the baseline needs time, but after that period, it gets what normal looks like for a server.&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Detection Logic Decision&lt;/strong&gt;: From the above two, we have a current rate, a mean, and a standard deviation. The detector calculates a z-score:&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;The z-score answers how many standard deviations above normal, a particular rate is. A z-score of 1.0 means it is slightly above average. 3.0 means this happens by random chance less than 0.3% of the time. 10.0 means something is very wrong.&lt;br&gt;
When something is wrong, the detector takes action by banning the source IP, and a notification is sent on the system (slack in this case).&lt;br&gt;
There is also an error surge detector. If an IP is getting errors much higher than normal, its detection thresholds tighten automatically. This is to catch attackers who might not send high volumes of requests.&lt;/p&gt;
&lt;h2&gt;
  
  
  How iptables Blocks an IP
&lt;/h2&gt;

&lt;p&gt;Whenever an IP is flagged, the detector runs the command:&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 1.2.3.4 -j DROP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;iptables&lt;/code&gt; is Linux's built-in firewall&lt;br&gt;
&lt;code&gt;-A INPUT&lt;/code&gt; adds a rule to the input chain i.e. incoming traffic&lt;br&gt;
&lt;code&gt;-s 1.2.3.4&lt;/code&gt; selects traffic coming from the source IP that is given as argument to the flag.&lt;br&gt;
&lt;code&gt;-j DROP&lt;/code&gt; discards the packet. &lt;/p&gt;

&lt;p&gt;With this, the attacker's requests never reach Nginx; Linux drops them at the lowest level, before the application code runs.&lt;/p&gt;

&lt;p&gt;Bans are not permanent by default. The&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
 follows a backoff schedule.
First ban is for 10 minutes, second for 30 minutes, third for two hours, and fourth permanent. This means repeat offenders get permanently blocked.

## Personal Takeaway:
My personal takeaway from this project was the logic used to build adaptive thresholds. I love the math in that, and how it makes sure - to a good extent - that the thresholds adjust very well to different traffic patterns. It adds the time dimension to the volume of requests, which is what accurate systems need: **CONTEXT**.

Here is the github repository: **https://github.com/OWK50GA/ddos-attack-protection-engine**
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>devops</category>
      <category>sre</category>
      <category>nginx</category>
      <category>python</category>
    </item>
    <item>
      <title>Bitcoin Dust Attacks: What They Are and How to Defend Against Them</title>
      <dc:creator>Wilfrid Okorie</dc:creator>
      <pubDate>Wed, 15 Apr 2026 06:04:54 +0000</pubDate>
      <link>https://dev.to/wilfridk/bitcoin-dust-attacks-what-they-are-and-how-to-defend-against-them-46dh</link>
      <guid>https://dev.to/wilfridk/bitcoin-dust-attacks-what-they-are-and-how-to-defend-against-them-46dh</guid>
      <description>&lt;p&gt;In the context of blockchain, dust is referred to as tiny amounts of any cryptocurrency that is uneconomical to spend. &lt;br&gt;
An amount is considered uneconomical to spend when the transaction cost it incurs is greater than its value.&lt;/p&gt;

&lt;p&gt;An attack is a malicious-intent action that attempts to do things like steal funds, exploit loopholes in rules, or disrupt the network. There are different types of attacks in Bitcoin. One of them is the &lt;strong&gt;Dust Attack&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  How addresses/keys today in Bitcoin work:
&lt;/h2&gt;

&lt;p&gt;Bitcoin uses Elliptic Curve Cyptography, which is a form of asymmetric cryptography involving a keypair instead of a key, where there is the private key, and there is the public key. Public keys are derived from private keys. They are secured by the discrete-log problem in math, which ensures that it is extremely difficult to work your way back from a public key to a private key. There are different forms of spend, but the basic rule is simple: public keys are associated with bitcoins recorded on the blockchain, and the controller of the keys (the holders of the corresponding private key) can spend the bitcoins, by addressing them to some other public key. Addresses are derived from public keys.&lt;br&gt;
Now, the blockchain is a public network, implying that transactions there are completely visible, so that when a transaction happens, you can see what addresses are involved in the transactions, and the amounts spent. &lt;br&gt;
Bitcoiners love their privacy however, so a natural solution is to control more than one keypair, so that you can send different addresses for different transactions, and make it more difficult for addresses to be traced to you.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfsi18vr3pl63tt7v6at.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fkfsi18vr3pl63tt7v6at.png" alt="Deterministic Key Generation: Tree of wallet keys generated from a single seed" width="800" height="549"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Modern wallets have a clever way of doing this whereby from a seed, a private key can be derived, and a whole tree of child keys (private and public) can be derived. The whole idea is that each user has a master key, that gives birth to descriptors, that are used to deterministically derive  new keypairs, to evolve into a whole tree of keys, so that any key in the tree can be parent to other keys, and there is no limit on the depth of the tree. &lt;a href="https://github.com/bitcoinbook/bitcoinbook/blob/develop/ch05_wallets.adoc" rel="noopener noreferrer"&gt;More on this can be seen on the Bitcoin Book&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What This Implies
&lt;/h2&gt;

&lt;p&gt;This means that for every transaction you receive coins from, you could actually publish a different address, so that the coins could not be traced to you. There is also the concept of internal and external descriptors, where internal descriptors derive wallet addresses you never share - also called change descriptors, because you use them to make change for yourself in transactions. External descriptors derive addresses that are shared, for you to receive coins from other people.&lt;br&gt;
The main point of descriptors and many addresses is privacy through non-reuse. Having these many addresses breaks transaction links, helps hide which output from a transaction is your change (since to the unknowing eye, it is not addressed to your wallet), it limits damages from key leak, so that if a private key gets leaked, just that address and its UTXOs are in danger, not your entire wallet. &lt;/p&gt;

&lt;p&gt;Note: As you continue down this article, know that Bitcoin doesn't use the arithmetic balance model, but the UTXO model for amounts, so that you have units of cryptocurrency, instead of a constant you just subtract from when you want to spend. It is like different units of money in a physical wallet. Each unit is called an Unspent Transaction Output (UTXO)&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Dust Attacks
&lt;/h2&gt;

&lt;p&gt;In dust attacks, an attacker is not directly after your coins. Instead, they want to uncover your identity, by plotting your &lt;strong&gt;identity graph&lt;/strong&gt; - a map of which Bitcoin addresses belong to the same wallet. This can be used to deanonymize a person, so that you find out their total holdings. Knowledge of who holds how many bitcoins in the past has greatly led to threats, physical attakcs, abuse, torture, etc., so you want to protect yourself. &lt;br&gt;
The knowledge can also be used to link a pseudonymous identity - yours, to a real-world activity.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Dust Attacks Happen:
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzgrt2jelw5hqe1x5i0a.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frzgrt2jelw5hqe1x5i0a.png" alt="How Dust Attacks Happen" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 1: Target Selection:&lt;/strong&gt;&lt;br&gt;
First of all, the attacker identifies the address(es) they want to deanonymize. This is done by looking at the blockchain directly, since transactions are public. They could get the address from anywhere at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2: Dust Delivery:&lt;/strong&gt;&lt;br&gt;
Next, the attacker builds the dust delivery transaction. They can do this anyway, but to be economical, it is usually a single transaction, that sends tiny amounts (dust) to all target addresses simultaneously. This amount is chosen so carefully, it is most likely very very tiny, but not so tiny as it wouldn't make sense to spend economically, and above the relay floor, so that even if the victims might not notice it, nodes could relay it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3: Waiting/Monitoring:&lt;/strong&gt;&lt;br&gt;
Next, the attacker waits for the wallet owner to spend the outputs they sent. This could be a gamble, depending on how long it takes for the user to spend the coins. If somehow, the victim's wallet's coin selection algorithm picks any of these UTXOs, alongside a real UTXO, it becomes part of the graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 4: Clustering:&lt;/strong&gt;&lt;br&gt;
When the user spends this UTXO, the inputs reveal co-ownership, since all inputs to a transaction &lt;strong&gt;usually&lt;/strong&gt; belong to one controller. The attacker now knows that these addresses in the transaction input belong to the target.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 5: Graph Expansion:&lt;/strong&gt;&lt;br&gt;
The attacker can now expand their knowledge graph of you, trace forward or backward, expand the graph, and get closer to whatever goal it is they have.&lt;/p&gt;

&lt;h2&gt;
  
  
  How To Combat dust attacks:
&lt;/h2&gt;

&lt;p&gt;From the steps above as regards the &lt;strong&gt;How&lt;/strong&gt; of dust attacks, the way to avoid dust attacks is simple: &lt;strong&gt;Do not spend the dust&lt;/strong&gt;. More accurately, do not spend the dust on a canonical transaction. Instead, you can arrange transactions with no output to spend them, so that the entire dust goes into network fees and the trail ends there, or you could mix the dust through a coinjoin with other users' dust. Here it gets mixed with hundreds other dust UTXOs, and the link breaks.&lt;/p&gt;

&lt;p&gt;I built a tool using Rust called &lt;strong&gt;Hoover&lt;/strong&gt; that acts to identify dust attack UTXOs, and construct Partially Signed Bitcoin Transactions (PSBTs) that safely spend them to fees, and safely remove them from the users' wallets. &lt;/p&gt;

&lt;h2&gt;
  
  
  Hoover
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8o3ffmept2zl2yuiobzu.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8o3ffmept2zl2yuiobzu.png" alt="Hoover: a tool that identifies dust attacks and sweeps" width="300" height="168"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/OWK50GA/hoover" rel="noopener noreferrer"&gt;Hoover&lt;/a&gt; is one of many dust attack tools that have been created for this purpose, and they all have one general idea. &lt;br&gt;
These dust attack tools take in your descriptors, internal and/or external, and register them. Then, on your command, it scans your wallet for dust UTXOs, showing them to you. Promptly, it constructs PSBTs for you to spend these transactions, with OP_RETURN output, so that there is no UTXO output. These PSBTs can then be taken and signed however you may, and broadcasted to the network as regular signed transactions. &lt;/p&gt;

&lt;p&gt;One critical part of creating the PSBTs is that you do not create a PSBT that contains dust from multiple addresses. The consolidation must be per address, else it compromises the privacy it aims to protect.&lt;/p&gt;

&lt;p&gt;Of course, these steps/processes differ from tool to tool, with different tools making innovations in different parts of the process, such as the nature of the PSBT.&lt;br&gt;
In the same light, the part of the process Hoover does different is in the dust identification. &lt;/p&gt;

&lt;p&gt;Usually, the tool would just check all the UTXOs to see which one is below or sometimes equal to the relay floor for that script type. Optionally, a user may configure their minimum number of sats for a UTXO to be considered non-dust, so that for a relay floor of 546 sats, a user may choose to remove every UTXO under 600 sats.&lt;br&gt;
However, a user may by coincidence have a lot of UTXOs that are not economically unspendable, but might be dust attacks, and in this case, depending on how many of them they have, sweeping all from their wallet may be considered waste.&lt;br&gt;
Another case is that depending on how sophisticated the attacker is, the attacker could send as high as 700 sats so that the user is much more likely to spend, and less likely to suspect as a dust attack UTXO, which increase the attacker's chances of succeeding in that attack. &lt;br&gt;
Here is what Hoover does different: Hoover divides dust into different types. UTXOs under the relay floor for a particular script-type are automatically counted as dust, and since they are economically unspendable, they are added to the "psbt staging area". &lt;br&gt;
Hoover goes further to inspect UTXOs as high as a configurable amount by the user, trying to detect dishonest patterns from the source, and marks the UTXO in the list by suspicion level. This results in a user knowing their UTXO may be from a dust attack, even if it is not canonical dust. Some of the flags used to detect these dishonest patterns are:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Dust UTXOs received on a change address: this could be dust, or it could be a bit more than a dust UTXO, but your change addresses should not receive UTXOs from external parties in the first place. A person should only know your change address if they have been monitoring your transactions on the blockchain. This particular flag is highly suspicious.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Multiple of your addresses receiving dust from same txid: this is also almost certainly from a dust attack, and the attacker has actually done their research well and almost accurately, and at this point, doing anything might help finally achieve their goal. The suspicion on this flag is also very high.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64n9wlpcixwi4hjitwbz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F64n9wlpcixwi4hjitwbz.png" alt="Red flags in a transaction that indicate dust attack patters" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Number of outputs of sending tx: this is another fingerprint of a dust attack. The outputs of the sending transaction are inspected. If a high ratio of these outputs are dust, as opposed to not, it is almost certainly a dust attack. The suspicion flag is also high, but not as high as the previous two.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Suspicious round value: in case this wasn't caught by first type of dust, UTXOs with their values too close to, or exactly at the relay floor have to be inspected to be dust attack UTXOs. Other methods are used to check, but the fact that it is too close to the relay floor adds a suspicion weight as well to the UTXO.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Same address sending dust in more than one tx: Catching dust from  the same address more than once, even when not in the same transaction is also a bad flag.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There are more flags Hoover uses for detection (some still being implemented), but the idea is the same: In what context did my wallet receive this UTXO? That will tell whether or not the UTXO is a dust-attack UTXO, even for UTXOs above the canonical dust threshold.&lt;/p&gt;

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

&lt;p&gt;When dust attacks succeed, an attacker successfully clusters your address. The attacker now knows you hold significant amounts in bitcoin, and you become a target for extortion; kidnapping or ransom has happened to known Bitcoin holders. For an individual, transaction history exposure could expose salary, spending habits, political donations. Every transaction you ever made/will ever make becomes readable in the context of your identity. Social engineering is also possible with this information in the hands of bad actors. They know when you received large amounts, which services you use, and can do terrible things, such as timely impersonation of such services.&lt;/p&gt;

&lt;p&gt;What are some other flags you think would mean UTXOs probably are part of a dust attack campaign?&lt;/p&gt;

</description>
      <category>bitcoin</category>
      <category>blockchain</category>
      <category>tooling</category>
      <category>rust</category>
    </item>
  </channel>
</rss>
