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:

  1. 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;
  1. Set the status code Nginx will return when a client exceeds the rate limit (444 in this case):
limit_req_status 444;
  1. Apply the rate limit to a server or location block. This example sets a burst limit of 20 and turns on the nodelay option, 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.

Nginx uses the ngx_http_geoip_module to implement Geo-blocking. This module uses MaxMind’s GeoIP databases to determine the origin of a connection.

To set this up:

  1. Install the geo ip list package.
# Via package
sudo apt-get install -y nginx libnginx-mod-http-geoip geoip-database
# Manually
cd /usr/share/GeoIP/
sudo wget http://geolite.maxmind.com/download/geoip/database/GeoLiteCountry/GeoIP.dat.gz
sudo gunzip GeoIP.dat.gz
  1. Load the required modules:
load_module modules/ngx_http_geoip_module.so;
load_module modules/ngx_stream_geoip_module.so;
  1. Specify the location of the GeoIP database:
geoip_country /usr/share/GeoIP/GeoIP.dat;
  1. Then, create a map that assigns a variable ($allowed_country in 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 $geoip_country_code $allowed_country {
    default no;
    SE yes;
    US yes;
    UK no;
}
  1. Finally, in your server block, use an if directive to deny access for all countries not allowed:
server {
    ...
    if ($allowed_country = no) {
        return 444;
    }
    ...
}

This will return a 444 error for any request originating from a country that is not on the allowed list.

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:

  1. 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;
  1. Set up a geo module to match the realip_remote_addr with the list of Cloudflare’s IPs:
geo $realip_remote_addr $cloudflare_ip {
    default 0;
    103.21.244.0/22 1;
    ...
}
  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":"$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":"$cf_iplatitude"}';

The log format of json is easily parsed by most if not all logging utilities such as filebeat or loki

Here is a loki config for managing the log above with promtail (so essentially a promtail config)

server:
  http_listen_port: 9080
  grpc_listen_port: 0

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: nginx
    static_configs:
      - targets:
          - localhost
        labels:
          job: nginx
          __path__: /var/log/nginx/access.log
    pipeline_stages:
      - json:
          expressions:
            timestamp: timestamp
            body_bytes_sent: body_bytes_sent
            host: 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_country: http_cf_country
            http_referer: http_referer
            http_cf_iplongitude: http_cf_iplongitude
            http_cf_iplatitude: http_cf_iplatitude
      - labels:
          host: ''
          status: ''
      - timestamp:
          source: timestamp
          format: '02/Jan/2006:15:04:05 -0700'
      - output:
          source: message

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:

  1. Install Fail2Ban on your server, if you haven’t already:

    sudo apt install fail2ban
    
  2. Create a jail for Nginx in Fail2Ban. Open the jail configuration file:

    sudo nano /etc/fail2ban/jail.local
    

    Add 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,https
    
  3. Restart Fail2Ban to enable the new configuration:

    sudo systemctl restart fail2ban
    

    Now, 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":"<HOST>", .*"status": 4[0-9][0-9],.*}$
ignoreregex =

This filter is designed to match the JSON log format for nginx, looking for 4xx status codes (e.g., 403 Forbidden, 404 Not Found), which might indicate malicious activities.

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.json
maxretry = 5

This jail configuration activates the nginx-json filter and monitors the NGINX JSON access log. After five failed attempts (maxretry), the IP is banned.

After modifying the configuration, restart Fail2Ban to apply the changes:

sudo systemctl restart fail2ban

Mitigating SYN/ACK Attacks

A SYN/ACK attack is a form of denial-of-service attack that exploits the three-way handshake process that TCP/IP networks use to establish a connection between a client and a server. An attacker sends a large number of SYN (synchronization) requests to a target server, but it does not respond to the subsequent ACK (acknowledgment) from the server, causing the server to consume resources waiting for responses that will never arrive.

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:

  1. 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=256
    
  2. net.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 to client_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 the Keep-Alive: timeout=time response header field.
  • send_timeout: If after this time the client has not received any data, the server closes the connection.


Buy Me a Coffee