DoS Protection #
On the public internet, our server doesn’t only face human users browsing normally, but also the threat of Denial of Service (DoS) attacks. The main goal of a DoS attack is to cripple the server by exhausting all available resources — like RAM, CPU capacity, network bandwidth, and connection queue slots (connection sockets) — so legitimate users can no longer access our services.
Nginx is globally known for its event-driven architecture that’s very resilient in handling thousands of simultaneous connections with very little memory. However, without proper hardening configuration, our Nginx can still be crippled by application-layer DoS attacks. In this article, we’ll dissect the difference between Nginx mitigation vs external DDoS solutions, limiting simultaneous connections with limit_conn, fending off slow attacks (Slow HTTP Attacks) via timeout tuning, limiting file upload sizes, and doing bandwidth throttling.
Nginx’s Limits vs Dedicated DDoS Protection Solutions #
Before we do any configuration, we must be realistic about Nginx’s defense capacity.
- Volumetric DDoS Attacks: These attacks work at the network level (Layers 3 & 4) by sending massive traffic (tens of Gigabytes to Terabytes per second, e.g., via NTP/DNS Amplification or SYN Flood) to flood the server’s inbound bandwidth pipe. Nginx cannot handle this type of attack. Our server’s bandwidth pipe will be saturated before the data packets even reach the Nginx software. For this mitigation, we must use external solutions like Cloudflare, AWS Shield, or DDoS Scrubbing Centers in front of our infrastructure.
- Application-Layer DoS (Layer 7): These attacks target processing weaknesses at the application level with relatively small but resource-hungry traffic. For example, slow-opening connection attacks (Slowloris), database search query floods, or large file download connection floods. Nginx is very effective and in the best position to absorb this type of attack before requests touch our backend servers.
limit_conn: Limiting Simultaneous Connections per IP Address #
While rate limiting (limit_req) limits the rate of requests per second, the limit_conn directive is used to limit the number of simultaneously active connections from a single IP.
This scenario is important to prevent:
- A single user opening dozens of browser tabs or running parallel download scripts that monopolize server connections.
- Connection flooding attacks trying to exhaust our operating system’s TCP sockets.
Step 1: Define a Connection Zone at the http Context
#
Just like rate limiting, we must create a shared memory zone at the http block level:
http {
# Zone A: Track the connection count by client IP (10m = 160,000 IPs)
limit_conn_zone $binary_remote_addr zone=conn_per_ip:10m;
# Zone B: Track total connections to a specific virtual server
limit_conn_zone $server_name zone=conn_per_server:10m;
# Customize the response when the limit is exceeded (Use HTTP 429)
limit_conn_status 429;
limit_conn_log_level warn;
}
Step 2: Apply limit_conn on the Appropriate Location #
We can apply this restriction globally at the server block or specifically on certain folder locations (e.g., a file download directory):
server {
listen 443 ssl;
server_name example.com;
# Maximum 15 simultaneously active connections per IP for the whole website
limit_conn conn_per_ip 15;
# Maximum 2000 total connections allowed into this virtual server
# (To protect the server from exceeding internal capacity)
limit_conn conn_per_server 2000;
location / {
try_files $uri $uri/ =404;
}
# Stricter rules for the large file download directory
location /downloads/ {
# Restrict extremely: only 2 simultaneous connections per IP allowed
# (Prevents users from using multi-connection download managers)
limit_conn conn_per_ip 2;
root /var/www/data;
}
}
Fending Off Slow Attacks (Slow HTTP Attacks / Slowloris) #
One of the most lethal application attack techniques for traditional web servers is Slowloris (or Slow HTTP GET/POST attack).
How Slowloris works is simple yet clever:
- The attacker opens hundreds of TCP connections to our server.
- The attacker sends HTTP request headers, but deliberately sends them very slowly (e.g., only sending one header line every 9 seconds).
- Because the request isn’t fully sent, Nginx is forced to keep those connections open in memory waiting for the remaining data.
- If the attacker does this on thousands of simultaneous connections, all Nginx worker sockets get fully occupied just waiting, so real users can’t connect at all.
Here’s an illustration of how timeout settings in Nginx can proactively cut off an attacker’s slow connections:
sequenceDiagram
autonumber
actor Attacker as Attacker (Slowloris)
actor Nginx as Nginx Web Server
Attacker->>Nginx: TCP Handshake (Connection Open)
Attacker->>Nginx: Send Partial Header: "User-Agent: Mozilla"
Note over Nginx: client_header_timeout starts running (10 seconds)
Note over Attacker: Waits 9 seconds before sending the next line...
Attacker->>Nginx: Send Partial Header: "Accept: text/html"
Note over Nginx: client_header_timeout timer refreshed/reset
Note over Attacker: Waits 15 seconds (Exceeding the timeout limit!)...
Note over Nginx: client_header_timeout limit (10s) exceeded!
Nginx->>Attacker: HTTP 408 Request Timeout / Close Connection
Note over Nginx: Socket freed again to serve legitimate usersTimeout Configuration for Slow Attack Mitigation #
To fend off these attacks, we must tighten the timeout limits in Nginx so unproductive connections are cut as quickly as possible:
http {
# 1. Timeout for reading request headers from the client (Default: 60s, recommended: 10s)
# If the client doesn't finish sending headers within 10 seconds, Nginx closes the connection.
client_header_timeout 10s;
# 2. Timeout for reading the request body (Default: 60s, recommended: 10s)
# Applies to upload or POST form processes deliberately hung slowly.
client_body_timeout 10s;
# 3. Timeout for sending the response back to the client (Default: 60s, recommended: 10s)
# If the client deliberately reads data slowly (Slow Read Attack).
send_timeout 10s;
# 4. Idle keep-alive connection timeout (Recommended: 65s)
keepalive_timeout 65s;
# 5. Maximum number of requests in one keep-alive connection
keepalive_requests 1000;
}
With the 10-second timeout settings above, our Nginx becomes very responsive in discarding junk connections deliberately hung by attackers, without disturbing legitimate users on slightly slower mobile internet connections.
Limiting Request Sizes (Client Request Body Limits) #
Another DoS attack scenario is Buffer Overflow or Resource Exhaustion by sending requests with a gigantic body (e.g., trying to upload hundreds of Gigabytes of junk files) to fill temporary disk storage or eat up our server’s RAM.
Nginx provides an easy way to limit incoming request sizes:
http {
# 1. Limit the maximum request body size (Nginx default: 1m, recommended: 10m)
# Adjust to your application's maximum upload limit (e.g., 10 Megabytes)
# If a user uploads a file > 10m, Nginx immediately rejects it with status 413 Payload Too Large
client_max_body_size 10m;
# 2. Set the memory buffer size for reading the request body
# If the request body is under 128k, it's read directly in RAM (very fast).
# If larger, Nginx writes it to a temporary file on disk (avoiding OOM).
client_body_buffer_size 128k;
# 3. Limit the buffer for client request headers
# Prevents Header Flooding attacks (sending gigantic headers)
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
}
Bandwidth Throttling #
When our server serves large static files (like annual report PDFs, software ZIP files, or demo videos), attackers can run many bots to download those files repeatedly. This can exhaust the server’s monthly bandwidth quota (bandwidth exhaustion) and slow down the site for other users.
We can control data transfer speed using the combination of the limit_rate and limit_rate_after directives:
server {
listen 443 ssl;
server_name example.com;
location /static/downloads/ {
# Limit bandwidth per connection (e.g., maximum 500 Kilobytes per second)
limit_rate 500k;
# Bandwidth throttling only activates after the download passes the first 2 Megabytes
# (Allows small files to download fast without limits, but large files get reined in)
limit_rate_after 2m;
root /var/www/html;
}
}
This technique ensures that for the first 2MB of data, users’ browsers download at their full network speed (improving initial responsiveness). Once the download size exceeds 2MB, Nginx automatically lowers the transfer speed to a constant 500 KB/s for the rest of the file.
Linux TCP/IP Kernel Tuning (System-Level Hardening) #
Nginx runs on the Linux operating system, which means Nginx’s resilience against DoS attacks heavily depends on the performance of the Linux kernel’s TCP/IP stack underneath. During a connection flood attack, Nginx might still have enough memory, but the Linux kernel could already be giving up because the TCP socket queue is exhausted.
To strengthen our defenses, we can tune Linux kernel parameters by editing the /etc/sysctl.conf file and loading it with the sudo sysctl -p command:
# /etc/sysctl.conf
# 1. Enable TCP SYN Cookies (Very Important for SYN Flood Mitigation)
# When the SYN queue is full, the kernel starts using SYN cookies to validate
# connections without needing to allocate memory resources for SYN_RECV state.
net.ipv4.tcp_syncookies = 1
# 2. Increase the new unfinished connection queue capacity (backlog)
net.ipv4.tcp_max_syn_backlog = 2048
# 3. Increase the maximum queued connections limit by the OS (somaxconn)
# Must be adjusted to the listen backlog value in Nginx
net.core.somaxconn = 1024
# 4. Reduce the disconnect timeout for already-closed connections (FIN timeout)
# Helps clean up TIME_WAIT sockets from memory faster
net.ipv4.tcp_fin_timeout = 15
# 5. Enable TIME_WAIT socket reuse for new connections
net.ipv4.tcp_tw_reuse = 1
By combining the kernel optimizations above with Nginx timeout parameters, our server becomes much more resilient against connection flood attacks trying to cripple the system’s network stack.
Protecting Upstreams from HTTP Floods (Reverse Proxy Hardening) #
When acting as a Reverse Proxy, one of the most damaging DoS types is when attackers flood the server with format-valid requests that force the backend application to run very heavy database queries (heavy HTTP flood). Nginx might be safe, but backend servers (like Node.js, PHP-FPM, or Java) will run out of memory or lock up.
Besides using limit_req, we must secure the connection between Nginx and the Upstream Backend so Nginx doesn’t get locked up too when the backend slows down:
upstream backend_app {
server 127.0.0.1:8080;
# Reuses connections to the upstream (saves local TCP socket ports greatly)
keepalive 32;
}
server {
listen 443 ssl;
server_name app.example.com;
location / {
proxy_pass http://backend_app;
# 1. Limit the connection establishment timeout to the backend (Default: 60s)
# If the backend is down, don't let Nginx hang client connections
proxy_connect_timeout 5s;
# 2. Limit the data send timeout to the backend
proxy_send_timeout 10s;
# 3. Limit the response read timeout from the backend
# If a backend DB query takes > 10 seconds, cut it off & return status 504
proxy_read_timeout 10s;
# HTTP/1.1 keepalive configuration for the upstream
proxy_http_version 1.1;
proxy_set_header Connection "";
}
}
The proxy_read_timeout 10s setting ensures that if our backend server is down or locked up by a database query flood attack, Nginx immediately cuts that internal connection within 5 seconds and returns an HTTP 504 Gateway Timeout error response to the client instantly. This prevents connection queues from piling up in Nginx memory.
Integration with Fail2ban for Automatic Blocking #
Even though Nginx successfully rejects DoS or brute force requests with 429/503 status, those requests still reach our web server and use CPU power to process the rejection. For maximum defense efficiency, we should block attacker IPs at the operating system firewall level (iptables/nftables) so data packets are dropped before touching Nginx.
The industry-standard way to do this automatically is using Fail2ban. Fail2ban is a background service that monitors Nginx log files, detects IPs triggering repeated blocks, then dynamically blocks those IPs in the Linux firewall.
Step 1: Install Fail2ban #
sudo apt update
sudo apt install fail2ban -y
Step 2: Create a Custom Nginx Filter #
Create a new filter file at /etc/fail2ban/filter.d/nginx-limit-req.conf to detect Nginx rate-limiting logs:
# /etc/fail2ban/filter.d/nginx-limit-req.conf
[Definition]
# Match Nginx error logs triggering "limiting requests"
failregex = limiting requests, excess:.* client: <HOST>
ignoreregex =
Step 3: Configure a Jail in Fail2ban #
Add a jail rule in the /etc/fail2ban/jail.local file to enable blocking:
# /etc/fail2ban/jail.local
[nginx-limit-req]
enabled = true
port = http,https
filter = nginx-limit-req
logpath = /var/log/nginx/error.log
# If an IP is detected triggering the rate limit 5 times within 10 minutes (600s)
maxretry = 5
findtime = 600
# Block that IP for 2 hours (7200s) at the firewall
bantime = 7200
Start and enable the Fail2ban service:
sudo systemctl restart fail2ban
sudo systemctl enable fail2ban
With this configuration, if a rogue bot keeps hitting the Nginx rate limit 5 times, Fail2ban immediately detects it from the error log, invokes iptables commands to fully block that IP for 2 hours, and drops all of that bot’s data packets at the system kernel level.
Summary #
- Use Cloudflare for Volumetric DDoS: Nginx cannot handle large-scale network-level traffic flood attacks. Use an external CDN/DDoS Protection service to filter volumetric traffic before it reaches the server.
- Limit Connections per IP: Install the
limit_conndirective on sensitive locations (like download files) to prevent connection socket monopolization by download managers or attacker bots.- Tighten Control Timeouts: Set
client_header_timeoutandclient_body_timeoutto low values (10 seconds) to proactively cut hanging connections from Slowloris attacks.- Lock Down
client_max_body_size: Don’t let attackers upload unlimited-size files that can trigger server memory exhaustion; set a logical maximum limit for our application system.- Apply Bandwidth Throttling: Use
limit_rateto protect our server’s network capacity from mass exploitation of giant file downloads.