InfraRunBook
    Back to articles

    Diagnosing a DDoS Attack on Your Server and Mitigation Steps

    Cyber Security for Servers
    Published: Aug 25, 2026
    Updated: Aug 25, 2026

    A practical runbook for confirming a DDoS attack against a Linux server, identifying the specific attack pattern behind it, and applying mitigation steps that actually reduce load instead of just masking symptoms.

    Diagnosing a DDoS Attack on Your Server and Mitigation Steps

    Symptoms

    The pager goes off, or worse, a customer tells you before your monitoring does. The usual pattern looks like this: response times on sw-infrarunbook-01 spike from 80ms to several seconds, then the box stops responding to health checks entirely. SSH sessions hang after login, sometimes for thirty or forty seconds before you get a shell. Load average climbs but CPU usage looks oddly low, or conversely CPU is pegged at 100% on a single nginx worker. Outbound bandwidth graphs on your provider's panel show a wall of traffic that doesn't match your normal diurnal pattern. In my experience, the first tell is almost always that

    ss -s
    shows an absurd number of connections in a state that's normally rare, like SYN-RECV or TIME-WAIT.

    Here's what a healthy box looks like versus one under attack:

    ## Normal baseline
    root@sw-infrarunbook-01:~# ss -s
    Total: 187
    TCP:   142 (estab 128, closed 6, orphaned 0, timewait 4)
    
    ## Under attack
    root@sw-infrarunbook-01:~# ss -s
    Total: 48213
    TCP:   48190 (estab 12, closed 3, orphaned 1, timewait 88)
    

    That gap between 142 total connections and 48,190 is the kind of number that tells you this isn't a slow API call or a bad deploy. It's an attack, and now the job is figuring out which kind, because the fix for a SYN flood is not the fix for an HTTP flood, and applying the wrong one wastes time you don't have.

    Root Cause 1: TCP SYN Flood

    A SYN flood works by sending a huge volume of TCP SYN packets, often with spoofed source IPs, and never completing the three-way handshake. Each half-open connection consumes a slot in the kernel's backlog queue until it times out. Once that queue fills, legitimate SYNs get dropped silently, and your server looks "down" even though the process behind the listening socket is perfectly healthy.

    You identify it by looking at connection states directly:

    root@sw-infrarunbook-01:~# netstat -ant | awk '{print $6}' | sort | uniq -c | sort -rn
      47812 SYN_RECV
        140 ESTABLISHED
         88 TIME_WAIT
          3 LISTEN
    

    Forty-seven thousand connections stuck in SYN_RECV with almost nothing making it to ESTABLISHED is the signature. A tcpdump capture will usually show a flood of SYNs from a wide spread of source IPs, many of them clearly spoofed (private ranges appearing on a public interface, or sequential IPs that no real client population would produce).

    root@sw-infrarunbook-01:~# tcpdump -nn -i eth0 'tcp[tcpflags] & tcp-syn != 0 and tcp[tcpflags] & tcp-ack == 0' -c 20
    10:14:02.881211 IP 192.0.2.44.51201 > 203.0.113.10.443: Flags [S], seq 118823, win 65535
    10:14:02.881233 IP 192.0.2.91.33221 > 203.0.113.10.443: Flags [S], seq 992104, win 65535
    10:14:02.881250 IP 192.0.2.12.60112 > 203.0.113.10.443: Flags [S], seq 552013, win 65535

    To fix this on the box itself, enable SYN cookies immediately (many distros ship with this off or set conservatively) and tighten the backlog handling:

    root@sw-infrarunbook-01:~# sysctl -w net.ipv4.tcp_syncookies=1
    root@sw-infrarunbook-01:~# sysctl -w net.ipv4.tcp_max_syn_backlog=8192
    root@sw-infrarunbook-01:~# sysctl -w net.ipv4.tcp_synack_retries=2

    SYN cookies let the kernel avoid allocating state for a connection until the handshake actually completes, which neutralizes most of the damage from a spoofed flood. This is a mitigation, not a cure — if the volume is large enough to saturate your NIC or upstream link, you need scrubbing at the network edge, which I'll get to in the prevention section.

    Root Cause 2: UDP or ICMP Volumetric Flood

    Volumetric floods aim to exhaust bandwidth rather than connection state. UDP floods are common because they're easy to spoof and easy to amplify (DNS, NTP, and memcached reflection attacks all fall in this bucket). The giveaway is bandwidth saturation with the CPU mostly idle — the box isn't struggling to process the packets, the pipe in front of it is just full.

    root@sw-infrarunbook-01:~# vnstat -l
    Rx: 940.12 Mbit/s   1,824,552 p/s
    Tx:  12.44 Mbit/s      18,204 p/s

    1.8 million packets per second inbound on a server that normally sees a few thousand is not organic traffic. Confirm the protocol breakdown with tcpdump so you're not guessing:

    root@sw-infrarunbook-01:~# tcpdump -nn -i eth0 udp -c 5000 2>/dev/null | awk '{print $NF}' | sort | uniq -c | sort -rn | head
       4890 53
         78 123
         32 other

    Source port 53 dominating means this is DNS reflection — attackers spoofed your IP as the source in queries sent to open resolvers, and the responses are now flooding you. There's no host-level fix that stops this cleanly; you cannot out-firewall bandwidth exhaustion on your own NIC. The only real mitigation is upstream — null-routing or scrubbing at your provider or a DDoS protection service before traffic reaches your uplink. What you can do locally is drop the traffic as early as possible to protect CPU for legitimate handling, and get on the phone with your hosting provider (in my experience, the sooner you open that ticket the faster they can null-route or blackhole at the edge):

    root@sw-infrarunbook-01:~# iptables -A INPUT -p udp --sport 53 -j DROP

    Root Cause 3: Application-Layer (HTTP) Flood

    This is the one that looks the least like an "attack" at first glance, because the connections complete normally and nginx or Apache logs fill with what look like legitimate GET requests. The difference is volume and pattern — thousands of requests per second hitting the same few endpoints, often the login page or a search form, from a botnet with rotating user agents.

    root@sw-infrarunbook-01:~# tail -5000 /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head
        812 198.51.100.23
        790 198.51.100.87
        765 198.51.100.14
        ...

    If you see hundreds of requests per second from individual IPs, or a huge spread of IPs all hitting

    /wp-login.php
    or
    /api/search
    in the same second-by-second pattern, that's your L7 flood. Check nginx's own perspective on worker load too:

    root@sw-infrarunbook-01:~# nginx -V 2>&1 | grep -o with-http_stub_status_module
    root@sw-infrarunbook-01:~# curl -s http://127.0.0.1/nginx_status
    Active connections: 8412
    server accepts handled requests
     902341 902341 4021884
    Reading: 120 Writing: 6203 Waiting: 2089

    Writing at 6203 means thousands of workers are stuck sending responses, usually because they're serving the same expensive endpoint over and over. Mitigate with rate limiting at the nginx layer, which is fast to deploy and doesn't need a restart if you use a reload:

    http {
        limit_req_zone $binary_remote_addr zone=perip:10m rate=10r/s;
        server {
            location /api/search {
                limit_req zone=perip burst=20 nodelay;
            }
        }
    }
    root@sw-infrarunbook-01:~# nginx -t && nginx -s reload

    Root Cause 4: Slowloris-Style Connection Exhaustion

    Slowloris and its variants don't send much traffic at all — they open connections and trickle partial HTTP headers just fast enough to avoid the server's timeout, tying up a worker thread indefinitely. This is nasty because bandwidth graphs look completely normal while the server is effectively unreachable, since every worker is pinned holding an incomplete request.

    You catch this by looking at how long connections have been open relative to how little data they've sent:

    root@sw-infrarunbook-01:~# ss -tnp state established '( dport = :80 or dport = :443 )' | wc -l
    1024
    root@sw-infrarunbook-01:~# ss -tni | grep -A1 ESTAB | grep -i 'rto\|rtt' | head -3
       rto:204 rtt:1.2/0.4 ato:40

    If your worker count matches your max_clients setting exactly and has held steady for ten minutes with barely any bytes transferred, that's the pattern. The fix is aggressive timeout tuning:

    root@sw-infrarunbook-01:~# grep -E 'client_header_timeout|client_body_timeout' /etc/nginx/nginx.conf
    client_header_timeout 10s;
    client_body_timeout 10s;

    Apache users should install and enable mod_reqtimeout or mod_qos. nginx is inherently more resistant to Slowloris because of how it handles connections asynchronously, and in my experience migrating a legacy Apache box under repeated Slowloris hits to nginx-as-reverse-proxy has ended the problem outright rather than just reducing it.

    Root Cause 5: Botnet-Driven Distributed Sources (No Single IP to Block)

    Sometimes the root cause isn't a specific protocol quirk, it's scale of distribution. You'll see a flood where no single source IP accounts for more than 0.1% of traffic, which means IP-based blocking is useless — you'd need to block thousands of addresses and new ones keep appearing.

    root@sw-infrarunbook-01:~# tail -20000 /var/log/nginx/access.log | awk '{print $1}' | sort -u | wc -l
    14822

    Fourteen thousand distinct IPs in twenty thousand requests tells you this is a real botnet, not a handful of misbehaving clients. At this point local mitigation buys time but doesn't solve the problem — you need a reverse proxy or CDN layer in front of the origin (a service like Cloudflare, or your provider's own scrubbing tier) that can absorb and filter traffic before it reaches sw-infrarunbook-01 at all. Locally, you can at least reduce load by serving cached responses instead of hitting the app server for every request:

    location / {
        proxy_cache mycache;
        proxy_cache_valid 200 10s;
        proxy_pass http://127.0.0.1:8080;
    }

    Root Cause 6: Misconfigured Fail2Ban or Rate Limiting Making Things Worse

    I've seen this one bite people during an actual attack: fail2ban or an overly aggressive iptables rule set starts consuming more CPU than the attack itself, because every packet triggers a chain traversal through hundreds of dynamically added rules. Check how large your chains have gotten:

    root@sw-infrarunbook-01:~# iptables -L f2b-nginx-req -n | wc -l
    8402

    Eight thousand rules in a single fail2ban chain will slow down every packet that hits the box, attack traffic or not. Use ipset instead of raw iptables rules for large ban lists — ipset does hash-based lookups instead of linear rule traversal, and the difference under load is dramatic:

    root@sw-infrarunbook-01:~# ipset create banlist hash:ip timeout 3600
    root@sw-infrarunbook-01:~# iptables -I INPUT -m set --match-set banlist src -j DROP

    Root Cause 7: DNS Amplification Targeting Your Own Resolver

    If your server also runs an open or semi-open DNS resolver, attackers can abuse it as part of a reflection attack against a third party, and you'll see your own outbound bandwidth spike even though you're the amplifier, not the ultimate victim. Check for this:

    root@sw-infrarunbook-01:~# dig @203.0.113.10 ANY solvethenetwork.com +short
    ;; connection timed out

    Test whether your resolver answers queries from arbitrary source IPs it shouldn't. If it does, lock it down:

    root@sw-infrarunbook-01:~# cat /etc/bind/named.conf.options | grep -A3 allow-query
    allow-query { 10.0.0.0/8; 172.16.0.0/12; };

    Restricting allow-query to your internal RFC 1918 ranges only takes a resolver out of the amplification pool entirely, and it's worth checking even if you don't think of your box as a "DNS server" — plenty of default installs leave this wide open.

    Prevention

    None of the above steps make a determined, well-resourced DDoS go away permanently — they buy time and reduce collateral damage. The real prevention work happens before the attack: put a CDN or scrubbing provider in front of anything public-facing, so volumetric floods get absorbed upstream instead of hitting your uplink directly. Keep sysctl hardening (syncookies, backlog sizes, conntrack limits) as your default configuration rather than something you scramble to apply mid-incident. Baseline your normal traffic with something like vnstat or a Grafana dashboard fed from node_exporter, because you can't recognize "abnormal" if you don't know what normal looks like on sw-infrarunbook-01 at 3am on a Tuesday versus during a marketing push. Rate limit at every layer — nginx, application, and firewall — rather than relying on one choke point. And keep a written runbook with your provider's abuse/DDoS escalation contact and ticket process ready before you need it, because during an actual attack is the worst time to be hunting for the right phone number.

    Frequently Asked Questions

    How can I tell the difference between a DDoS attack and a legitimate traffic spike?

    Check the distribution and pattern of source IPs and request paths. A legitimate spike usually shows a normal spread of endpoints being hit and referrers that make sense (a marketing campaign, a viral post). A DDoS typically hammers one or two endpoints, comes with connection states like SYN_RECV piling up, or shows thousands of distinct IPs each making an unnaturally uniform request pattern, as shown by commands like ss -s and tail on nginx access logs in this article.

    Can iptables alone stop a large volumetric DDoS attack?

    No. iptables runs on your server, which means the attack traffic has already consumed your bandwidth and reached your NIC before iptables can drop it. For volumetric floods you need upstream mitigation from your hosting provider or a dedicated DDoS scrubbing service — iptables is useful for application-layer and connection-exhaustion attacks, not for saturating your uplink.

    Why does my server show low CPU usage but still feel unreachable during an attack?

    This usually points to a SYN flood or Slowloris-style attack, where the bottleneck is connection state or worker availability rather than raw processing power. The kernel's backlog queue or your web server's worker pool fills up with half-open or trickling connections, leaving no room for legitimate traffic even though the CPU itself is barely working.

    Is fail2ban enough to protect against an HTTP flood?

    It helps for smaller, less distributed floods, but fail2ban relies on iptables rules per banned IP, which doesn't scale well against thousands of unique source addresses and can itself become a performance bottleneck. Pairing it with ipset for the ban list, plus nginx-level rate limiting, holds up much better under real attack volume.

    Related Articles