When it comes to running a web server, security should always be your paramount concern. In this blog post, we’ll explore how you can secure your Nginx server by setting up access controls, only allowing Cloudflare IPs, and implementing rate limiting. We’ll provide numerous examples to illustrate the concepts and the necessary code snippets to implement these security measures.
Access Control with ‘allow’ and ‘deny’
Nginx allows you to restrict access to certain parts of your server using the allow and deny directives. Let’s look at some examples:
Example 1: Denying Access to All
To deny all visitors access to your server, add the following in your server configuration:
location / {
deny all;
}
Example 2: Allowing Specific IP Address or a Specific network
To allow a specific IP address, for example, ‘192.168.1.1’, use:
# Specific IP
location / {
allow 192.168.1.1;
deny all;
}
# Specific Network
location / {
allow 192.168.1.0/24;
deny all;
}
Rate Limiting in Nginx
Rate limiting can prevent your server from being overwhelmed by too many requests at once. Here’s how to set it up:
- First, define a rate limit zone. In this example, we’re allowing 10 requests per second:
limit_req_zone $binary_remote_addr zone=ip:1m rate=10r/s;
# the zone can be named whatever you think is most suitible as long you refer to it in your server config
limit_req_zone $binary_remote_addr zone=badasses:10m rate=1r/s;
- Set the status code Nginx will return when a client exceeds the rate limit (444 in this case):
limit_req_status 444;
- Apply the rate limit to a server or location block. This example sets a burst limit of 20 and turns on the
nodelayoption, which means that requests exceeding the limit will be processed immediately if the burst limit isn’t exceeded:
server {
...
limit_req zone=ip burst=20 nodelay;
...
}
Geo-blocking with Nginx
Geo-blocking is a method that restricts or allows access based on the geographic location of the client. This can be used to block traffic from specific countries or regions.
My original example used the legacy GeoIP module and GeoLite .dat database. On a current setup I use ngx_http_geoip2_module with a GeoLite2 .mmdb database, or use country data supplied by a trusted edge such as Cloudflare.
To set this up:
- Install the distribution’s GeoIP2 module and obtain a current MaxMind GeoLite2 Country database under its license.
sudo apt install nginx libnginx-mod-http-geoip2
- Load the required modules:
load_module modules/ngx_http_geoip2_module.so;
- Specify the location of the GeoIP database:
geoip2 /usr/share/GeoIP/GeoLite2-Country.mmdb {
auto_reload 1h;
$geoip2_country_code country iso_code;
}
- Then, create a map that assigns a variable (
$allowed_countryin this case) based on the country code. In this example, we are allowing traffic from Sweden (SE) ans US and denying traffic from (UK):
map $geoip2_country_code $allowed_country {
default no;
SE yes;
US yes;
GB no;
}
- Finally, in your server block, use an
ifdirective to deny access for all countries not allowed:
server {
...
if ($allowed_country = no) {
return 444;
}
...
}
This returns 444 for a country outside my allowlist. The ISO code for the United Kingdom is GB, not UK. GeoIP2 module syntax is documented by the official module project; Cloudflare documents its CF-IPCountry header for traffic that I have already verified came through Cloudflare.
Allowing Only Cloudflare IPs
You might want to only allow traffic coming through Cloudflare, a popular content delivery network (CDN). Here is how you can set this up:
- Check if there are new ips here CloudFlare IP Ranges
- Configure Nginx to understand the actual client IPs from Cloudflare’s network:
set_real_ip_from 103.21.244.0/22;
...
real_ip_header CF-Connecting-IP;
- Set up a geo module to match the
realip_remote_addrwith the list of Cloudflare’s IPs:
geo $realip_remote_addr $cloudflare_ip {
default 0;
103.21.244.0/22 1;
...
}
- Finally, in the server block, return a 444 error (which closes the connection without sending a response to the client) if the client IP isn’t on Cloudflare’s list:
server {
...
if ($cloudflare_ip != 1) {
return 444;
}
}
By incorporating these measures, you will significantly enhance your Nginx server’s security and resilience against potential threats. Remember, effective security is always a multi-layered approach, and these configurations are but a part of that broader picture.
Logging And Log Options
Create a custom log option, in the examle below we are logging access log as json and added cloudflare country, longitude and latitude
log_format my_log_in_json escape=json '{"timestamp": "$time_local", "body_bytes_sent": $body_bytes_sent, "host": "$http_host", "remote_addr": "$remote_addr", "request_length": $request_length, "request_method": "$request_method", "request_uri": "$request_uri", "status": $status, "http_user_agent": "$http_user_agent", "request_time": $request_time, "upstream_addr": "$upstream_addr", "http_cf_ipcountry": "$http_cf_ipcountry", "http_referer": "$http_referer","http_cf_iplongitude":"$http_cf_iplongitude","http_cf_iplatitude":"$http_cf_iplatitude"}';
access_log /var/log/nginx/access.log my_log_in_json;
### If the log line becomes too unmanageble in one line you can do a more clean setup like this, adding it all in single quotes
log_format my_log_in_json escape=json '{'
' "timestamp": "$time_local", "body_bytes_sent": $body_bytes_sent,'
' "host": "$http_host", "remote_addr": "$remote_addr",'
' "request_length": $request_length,"request_method": "$request_method",'
' "request_uri": "$request_uri", "status": $status,'
' "http_user_agent": "$http_user_agent", "request_time": $request_time,'
' "upstream_addr": "$upstream_addr", "http_cf_ipcountry": "$http_cf_ipcountry",'
' "http_referer": "$http_referer","http_cf_iplongitude":"$http_cf_iplongitude",'
'"http_cf_iplatitude":"$http_cf_iplatitude"}';
The log format of json is easily parsed by most if not all logging utilities such as filebeat or loki
Promtail reached end of life on 2026-03-02, so this is the same small pipeline in Grafana Alloy. Grafana Promtail deprecation
local.file_match "nginx" {
path_targets = [{"__path__" = "/var/log/nginx/access.log", "job" = "nginx"}]
}
loki.source.file "nginx" {
targets = local.file_match.nginx.targets
forward_to = [loki.process.nginx.receiver]
}
loki.process "nginx" {
stage.json { expressions = {host = "host", status = "status"} }
stage.labels { values = {host = "", status = ""} }
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint { url = "http://loki:3100/loki/api/v1/push" }
}
Notice that I have added 2 fields host and status as labels, even though they are empty they will be populated if the json is parsed correctly, you will only want to set labels for fields with limited number of possible values.
a good grafana dashboard for the above aformentioned extraction is Loki v2 Web Analytics Dashboard for NGINX
Nginx and Fail2Ban
Fail2Ban is a useful tool that scans log files for patterns indicating potential malicious activity, and bans IPs that show such patterns.
Nginx writes access and error logs that can be monitored by Fail2Ban to detect and block potential threats. Here’s how you can configure Fail2Ban for Nginx:
Install Fail2Ban on your server, if you haven’t already:
sudo apt install fail2banCreate a jail for Nginx in Fail2Ban. Open the jail configuration file:
sudo nano /etc/fail2ban/jail.localAdd the following lines to the file:
[nginx-http-auth] enabled = true filter = nginx-http-auth logpath = /var/log/nginx/error.log maxretry = 3 bantime = 3600 port = http,httpsRestart Fail2Ban to enable the new configuration:
sudo systemctl restart fail2banNow, Fail2Ban will monitor Nginx logs and block IPs that repeatedly fail authentication.
To read JSON logs, Fail2Ban requires a custom filter. This filter must be written to parse the JSON structure and identify activities based on your server’s log files.
First, let’s create a new filter file named nginx-json.conf in the Fail2Ban filter directory:
sudo nano /etc/fail2ban/filter.d/nginx-json.conf
Now, copy and paste the following into the newly created file:
[Definition]
failregex = ^.*"remote_addr"\s*:\s*"<HOST>".*"status"\s*:\s*(?:401|403).*$
ignoreregex =
This filter is designed to match the JSON format shown above, but only for repeated authentication/authorization failures (401 and 403). It deliberately does not ban every 4xx response: ordinary 404 and client mistakes are too broad a signal.
Save the file and exit the text editor.
Next, modify the Fail2Ban jail file to include our new filter. Open the Fail2Ban jail file:
sudo nano /etc/fail2ban/jail.local
Add a new jail configuration as follows:
[nginx-json]
enabled = true
filter = nginx-json
port = http,https
logpath = /var/log/nginx/access.log
maxretry = 5
This targets repeated authentication/authorization failures, not every 4xx response: ordinary 404s and client mistakes should not automatically become firewall bans. I test the filter against the actual JSON ordering with fail2ban-regex /var/log/nginx/access.log /etc/fail2ban/filter.d/nginx-json.conf.
After modifying the configuration, restart Fail2Ban to apply the changes:
sudo systemctl restart fail2ban
Mitigating SYN Floods
A SYN flood sends many SYN requests without completing the TCP handshake, leaving the server with half-open connections. The server’s SYN/ACK packets are responses, not the attack name.
Mitigation
One of the most effective ways to mitigate SYN/ACK attacks is to adjust the kernel parameters that control TCP/IP behavior. Two of the most important parameters are:
net.ipv4.tcp_max_syn_backlog: This parameter controls the maximum number of queued connection requests that have sent a SYN packet, but have not yet received the client’s ACK packet. Increasing this value can help your server handle a larger number of incomplete connections.# default for ubuntu 22.04 sudo sysctl -w net.ipv4.tcp_max_syn_backlog=256net.ipv4.tcp_synack_retries: This parameter controls the number of times the server will resend a SYN/ACK packet to a client that has sent a SYN packet. Reducing this value can help to free up resources more quickly when dealing with SYN/ACK attacks.sudo sysctl -w net.ipv4.tcp_synack_retries=3
Keep in mind that these are just examples, and you should adjust these values based on your server’s capabilities and requirements. It’s also important to note that these changes will only persist until the next reboot unless you add them to the /etc/sysctl.conf file.
Adjusting Timeouts
Setting appropriate timeout values when attacks occur is a crucial part of server configuration. Timeouts help to prevent your server from wasting resources on slow or stalled connections.
There are several directives related to timeout settings:
client_body_timeout 5s;
client_header_timeout 5s;
keepalive_timeout 5 5;
send_timeout 5;
client_body_timeout: This directive sets the read timeout for the request body. If the client does not transmit the entire request body within this time, the 408 (Request Time-out) error is returned to the client.client_header_timeout: Similar toclient_body_timeout, but this one sets the timeout for reading the client request header.keepalive_timeout: The first parameter sets a timeout during which a keep-alive client connection will stay open on the server side. The second parameter sets a value for theKeep-Alive: timeout=timeresponse header field.send_timeout: If after this time the client has not received any data, the server closes the connection.
Buy Me a Coffee