Security

DDoS protection: what the network filters and what is left to you

Contents

For administrators and managers who want to know what the DDoS protection included with each of our servers actually does. By the end you will know where its limits are and have your own safeguards on the server: closed ports, request limits in nginx, a cache and fail2ban.

Attack layers in plain terms#

DDoS is an attempt to make a service unavailable by flooding it with traffic from many addresses at once. The layer the attack works at determines who can stop it.

SYN flood
The server receives a stream of requests to open a TCP connection (SYN packets), often from forged addresses. The queue of new connections fills up and real clients cannot connect.
UDP amplification
The attacker sends short requests to other people’s open services (DNS, NTP) with your address forged as the sender, and they answer you with much larger packets that clog the link.
HTTP flood
Thousands of infected devices open real connections and ask the site for its heaviest pages: search, basket, login.

The first two are network-layer attacks (L3/L4): the packets can be filtered out in the data-centre network before they reach the server. The third is an application-layer attack (L7): to the network it is ordinary encrypted HTTPS traffic, so only the server’s settings or an external service that inspects HTTP can stop it.

What is included with the server#

  • Protection against network-layer attacks (L3/L4) is included with every server, in every line and every country: Poland, Germany, France and the United Kingdom.
  • It switches on automatically when the network detects an attack on the server’s address.
  • There is no extra charge for attack volume or duration.

Two caveats:

  • Filtering starts once an attack has been detected, so in the first seconds junk traffic may reach the server.
  • The filter discards packets and does not watch your application, so it does not guarantee that a site or accounting system stays available.

What this protection does not cover#

  • Application-layer attacks (L7): HTTP floods, slow requests, a stream of API calls.
  • Password guessing against SSH, RDP, mail or a site’s admin panel. See the articles on SSH keys and secure RDP.
  • Vulnerabilities: an outdated CMS or plugin, missing updates.
  • Overload from heavy requests: a report that scans the whole database, a crawler on filter pages, your own script in a loop.

What you will need#

  • SSH access with sudo rights. The examples are for Debian 12–13 and Ubuntu 24.04–26.04 with nginx from the distribution package; on Windows Server only the principles of steps 1 and 5 apply.
  • Your office’s fixed address (203.0.113.10 in the examples) and a second computer for checking from outside.

What to do yourself#

1. Close the ports you do not need#

See which services are listening on the network:

sudo ss -tulpn

A local address of 0.0.0.0, * or [::] means the service is open on all addresses. Only the ports your visitors need should be reachable from the internet — usually 80 and 443, plus SSH with key-only login. Do not expose the database or RDP: bind the database to 127.0.0.1 or to a private network address, and reach RDP and the accounting system through a VPN.

Warning. A mistake in the firewall rules can cut off your SSH access. Allow your SSH port first and keep a second session open. If access is lost anyway, the rules have to be fixed through the remote console, provided on request through support (not available on the Lite line). Ready-made rules are in the article on basic Linux server hardening.

2. Limit requests and connections in nginx#

The limit_req and limit_conn zones are declared in the http context. Create the file /etc/nginx/conf.d/limits.conf:

# addresses that are not limited (the office)
geo $limit_exempt {
    default       0;
    203.0.113.10  1;
}
map $limit_exempt $limit_key {
    0 $binary_remote_addr;
    1 "";
}

limit_req_zone  $limit_key zone=perip:10m rate=10r/s;
limit_req_zone  $limit_key zone=login:10m rate=30r/m;
limit_conn_zone $limit_key zone=addr:10m;

limit_req_status  429;
limit_conn_status 429;

Then switch the limits on in your site’s server block (on Debian and Ubuntu it is a file in /etc/nginx/sites-available/); replace /login with the path of your login page:

server {
    # ... listen, server_name, ssl_certificate ...

    limit_conn addr 40;

    location / {
        limit_req zone=perip burst=40 nodelay;
        # ... proxy_pass or try_files ...
    }

    location = /login {
        limit_req zone=login burst=5 nodelay;
        # ... the same handler as in location / ...
    }
}

rate is the average request rate from one address, burst is the allowance for a spike, and with nodelay requests within that allowance are served without delay. Anything beyond it gets code 429. limit_conn caps simultaneous connections from one address; with HTTP/2 and HTTP/3 every concurrent request counts as a separate connection.

These numbers are only a starting point. To find your own, temporarily add limit_req_dry_run on; and limit_conn_dry_run on; to server: nginx will limit nobody but will log to error.log whom it would have limited. Test the configuration and apply it:

sudo nginx -t
sudo systemctl reload nginx

3. Turn on a cache for anonymous pages#

The cheapest way to survive an HTTP flood is not to wake the application for every request. In nginx that is proxy_cache (for PHP-FPM, fastcgi_cache and the same directives with the fastcgi_ prefix): even a 10-second cache turns a thousand identical requests into one call to the application. Cache anonymous pages only: exclude requests with a session cookie or an Authorization header using proxy_cache_bypass and proxy_no_cache, otherwise a visitor may see someone else’s account or basket.

4. Block persistent offenders with fail2ban#

The fail2ban service reads the logs and temporarily blocks, at the firewall, addresses that keep breaking the rules. Install the packages:

sudo apt update
sudo apt install fail2ban nftables

The package enables the sshd jail straight away (fail2ban’s term for a set of rules), so add the office address to the exceptions for all jails. Create the file /etc/fail2ban/jail.d/nginx-limit-req.local:

[DEFAULT]
ignoreip = 127.0.0.1/8 ::1 203.0.113.10

[nginx-limit-req]
enabled   = true
backend   = auto
banaction = nftables
logpath   = /var/log/nginx/error.log
findtime  = 10m
maxretry  = 20
bantime   = 1h

backend and banaction are set explicitly because the package defaults differ between releases. If the site has its own error_log, put that file in logpath.

Debian 12 only. The sshd jail looks for /var/log/auth.log, which does not exist there by default, so the service fails to start with the error Have not found any log file for sshd jail. Add this section to the same file (it needs the python3-systemd package, normally installed with fail2ban):

[sshd]
backend = systemd

Test the configuration and restart the service:

sudo fail2ban-client -t
sudo systemctl restart fail2ban

The nginx-limit-req filter ships with the package and reacts to limit_req rejections in error.log (dry_run entries do not count): 20 rejections in 10 minutes mean a one-hour block on ports 80 and 443. Against a flood from thousands of addresses fail2ban helps little: it is a tool against individual stubborn sources.

5. For websites: external application-layer protection#

If the site earns money or has already been attacked, limits on one server will not hold against a large botnet. You need an external WAF or CDN service: it takes HTTP traffic on its own network, weeds out bots and passes only checked requests to the server. Then allow ports 80 and 443 only from that service’s networks, or the attack will go straight to the server’s address. Also set up the realip module in nginx (set_real_ip_from, real_ip_header), or all visitors will share one address for limit_req.

How to tell an attack from ordinary overload#

Look at the connections first:

ss -s
ss -Htn state syn-recv | wc -l
ss -Htn state established '( sport = :80 or sport = :443 )' | awk '{print $NF}' | sed 's/:[0-9]*$//' | sort | uniq -c | sort -rn | head

The first command shows the total number of connections, the second counts the half-open ones (SYN received, no acknowledgement), the third lists the addresses with the most web server connections. Then the web server log (combined format): the busiest addresses or, with $7 in place of $1, the most requested pages.

sudo tail -n 50000 /var/log/nginx/access.log | awk '{print $1}' | sort | uniq -c | sort -rn | head -n 20

If you have monitoring, compare the graphs of inbound traffic, packets per second, connections, CPU and disk on one timeline. Without it, sar -n DEV 1 5 from the sysstat package shows the current rate on the interfaces.

L3/L4 attack
A sharp jump in inbound traffic and packets, while the web server log shows no increase in requests. Many half-open connections; the server may not even answer a ping.
L7 attack
A flood of requests in the log for one or two pages from many addresses, and many established connections on ports 80 and 443. The CPU is busy with the web server, the application and the database, while traffic grows only moderately.
Ordinary overload
Traffic is as usual or grows smoothly, and requests come from familiar clients and crawlers. The disk or memory is saturated, or one process causes all the load, and the start coincides with an event: a report, a backup, a mailing.

What to tell support#

If it looks like a network attack, or the server is unreachable from outside, contact technical support, available 24/7: phone +38 044 206 08 08, email info@united.net.ua. State straight away:

  • the time the attack started (and ended, if it is already over), with the time zone;
  • the server’s IP address, and the port and protocol under attack — for example, 203.0.113.25, TCP 443;
  • the symptoms: the server is completely unreachable, packets are lost, the site is slow, only one service is down;
  • what you saw: the output of ss -s, a few log lines, a screenshot of a graph, an mtr trace from outside.

How to check the result#

  1. Remove the dry_run lines, reload nginx as in step 2 and, from a computer whose address is not in the exceptions, send a series of requests to the login page. The first few get a normal response, the rest get code 429.
    for i in $(seq 1 20); do curl -s -o /dev/null -w '%{http_code}\n' https://example.com/login; done | sort | uniq -c
  2. The rejections show up in error.log, and the Total failed counter in fail2ban grows. After several series the address is blocked on ports 80 and 443 only; SSH is not affected. The last command lifts the block — use the address you tested from.
    sudo grep 'limiting requests' /var/log/nginx/error.log | tail -n 5
    sudo fail2ban-client status nginx-limit-req
    sudo fail2ban-client set nginx-limit-req unbanip 198.51.100.7

Common mistakes#

  • A per-address limit that ignores NAT. A whole office, or a mobile operator’s subscribers, reach the internet from one address. Add your own addresses to geo and ignoreip, and tune the values in dry_run mode.
  • dry_run mode left switched on. In that mode nginx only writes to the log and limits nobody.

What next#