---
title: "Nginx Web Server"
description: "A guide on installing and configuring an Nginx web server."
---

# Nginx Web Server

This guide was written for Ubuntu Server 26.04 LTS (Noble Numbat), Nginx 1.28.3, OpenSSL 3.5.5, and Certbot 5.6.0.

## Nginx

Nginx (pronounced "engine x") is the world's most popular web server, primarily used for serving static files or acting as a reverse proxy.

### Installation

Check for package updates and install `nginx`:

```shellsession
sudo apt update && sudo apt install nginx
```

### Enable a site

1. Create a new configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

2. Copy and paste the following configuration into the file, replacing `example.com` with your domain name:

   ```nginx title="/etc/nginx/sites-available/example.com" "example.com"
   server {
       listen 80;
       listen [::]:80;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }
   }
   ```

   Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

3. Enable a site by creating a symbolic link from `sites-available` to `sites-enabled`, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
   ```

4. Check the configuration for syntax errors:

   ```shellsession
   sudo nginx -t
   ```

5. Reload `nginx` to apply the changes:

   ```shellsession
   sudo systemctl reload nginx
   ```

6. Check the status of `nginx` to ensure it's running:

   ```shellsession
   sudo systemctl status nginx
   ```

7. Open your web browser and visit `http://example.com` to confirm that the site is working.

### Allow traffic through the firewall

1. List available application profiles:

   ```shellsession
   sudo ufw app list
   ```

2. Allow `nginx` through the firewall:

   `nginx` comes installed with pre-configured UFW application profiles that allow traffic on port 80 (`Nginx HTTP`), port 443 (`Nginx HTTPS`), or both (`Nginx Full`).

   Replace `Nginx Full` with `Nginx HTTP` or `Nginx HTTPS` as needed:

   ```shellsession "Nginx Full"
   sudo ufw allow "Nginx Full"
   ```

- **Optional:** Limit access to specific IP addresses:

  ```shellsession "192.0.2.1" "Nginx Full"
  sudo ufw allow from 192.0.2.1 to any app "Nginx Full" comment "Allow from specific IP"
  ```

  See [Cloudflare](#cloudflare) for a specific use case.

## Certbot

Certbot is a free, open-source command line tool that automates the process of obtaining, installing, and renewing SSL/TLS certificates from Let's Encrypt to enable secure HTTPS connections.

### Installation

1.  Install Certbot:

    ```shellsession
    sudo snap install --classic certbot
    ```

2.  Enable the `certbot` command:

    ```shellsession
    sudo ln -s /snap/bin/certbot /usr/bin/certbot
    ```

3.  Confirm the plugin containment level:

    ```shellsession
    sudo snap set certbot trust-plugin-with-root=ok
    ```

4.  Install the appropriate DNS plugin, replacing `cloudflare` with the name of your DNS provider:

    ```shellsession "cloudflare"
    sudo snap install certbot-dns-cloudflare
    ```

### Configuration

You'll need to set up DNS credentials.

Follow the steps for the previously installed [DNS plugin](https://eff-certbot.readthedocs.io/en/stable/using.html).

For example, if your DNS provider is Cloudflare, you'd run the following commands:

1. Create a credentials file:

   ```shellsession
   mkdir -p ~/.secrets/certbot
   touch ~/.secrets/certbot/cloudflare.ini
   ```

2. Open the file in a text editor:

   ```shellsession
   nano ~/.secrets/certbot/cloudflare.ini
   ```

3. Copy and paste the following code into the text editor:

   ```ini title="~/.secrets/certbot/cloudflare.ini" "0123456789abcdef0123456789abcdef01234567"
   # Cloudflare API token used by Certbot
   dns_cloudflare_api_token = 0123456789abcdef0123456789abcdef01234567
   ```

4. Visit the [Cloudflare API Tokens](https://dash.cloudflare.com/profile/api-tokens) page and create a new User API Token using the API token template `Edit zone DNS`. Replace the example token with the generated token. If you accidentally leave the page, you'll need to roll a new token.

5. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

6. Secure the new directories and file:

   ```shellsession
   chmod 700 ~/.secrets ~/.secrets/certbot
   chmod 600 ~/.secrets/certbot/cloudflare.ini
   ```

### Obtain a certificate

Run one of the following commands, replacing `user` with your Linux user and `example.com` with your domain name:

- Domain and www subdomain

  ```shellsession title="Domain and www subdomain" "user" "example.com"
  certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /home/user/.secrets/certbot/cloudflare.ini \
    -d example.com \
    -d www.example.com
  ```

- Domain and all subdomains

  ```shellsession title="Domain and all subdomains" "user" "example.com"
  certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /home/user/.secrets/certbot/cloudflare.ini \
    -d example.com \
    -d *.example.com
  ```

- Subdomain only

  ```shellsession title="Subdomain only" "user" "example.com"
  certbot certonly \
    --dns-cloudflare \
    --dns-cloudflare-credentials /home/user/.secrets/certbot/cloudflare.ini \
    -d app.example.com
  ```

The official documentation gives `~/.secrets/certbot/cloudflare.ini` as the example path to the credentials file, but I've found that leads to issues with automatic renewals.

### Test automatic renewal

The Certbot packages on your system come with a cron job or systemd timer that will renew your certificates automatically before they expire. You will not need to run Certbot again, unless you change your configuration. You can test automatic renewal for your certificates by running this command:

```shellsession
sudo certbot renew --dry-run
```

The command to renew certbot is installed in one of the following locations:

- `/etc/crontab/`
- `/etc/cron.*/*`
- `systemctl list-timers`

### Install the certificate

1. Open the configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

2. Enable HTTPS by changing the `listen` port to 443, add `ssl` parameter, and include the SSL certificate paths. This is also a good opportunity to enable HTTP/2.

   ```diff lang="nginx" title="/etc/nginx/sites-available/example.com" "example.com"
   server {
   -    listen 80;
   -    listen [::]:80;
   +    listen 443 ssl;
   +    listen [::]:443 ssl;

   + http2 on;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }

   +    # SSL certificates
   +    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
   +    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
   }
   ```

   Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

3. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

### Confirm that Certbot worked

To confirm that your site is set up properly, visit `https://example.com/` in your browser and look for the lock icon in the URL bar.

### Harden SSL configuration

#### Global settings

The following settings will enhance SSL performance and protect Perfect Forward Secrecy (PFS).

1. Open the configuration file:

   ```shellsession
   sudo nano /etc/nginx/nginx.conf
   ```

2. Add the following lines:

   ```diff lang="nginx" title="/etc/nginx/nginx.conf"
   ##
   # SSL Settings
   ##

   ssl_protocols TLSv1.2 TLSv1.3; # Dropping SSLv3 (POODLE), TLS 1.0, 1.1
   ssl_prefer_server_ciphers off; # Don't force server cipher order.

   +# TLS session resumption settings (works with TLS 1.2 and 1.3)
   +ssl_session_cache shared:SSL:10m;
   +ssl_session_timeout 1d;
   +ssl_session_tickets off;
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

#### Per-site settings

Use the [Mozilla SSL Configuration Generator](https://configurator.tlsref.org/#server=nginx&version=1.28.3&config=modern&openssl=3.5.5&hsts&guideline=6.0) to generate a strong SSL configuration for your server. The following example uses the "Modern" configuration for Nginx 1.28.3 and OpenSSL 3.5.5.

Use the "Intermediate" configuration if you are not running behind a Cloudflare proxy to make sure you support all clients. Do not disable TLS 1.2 on Cloudflare. Things will break like social media platforms or other apps previewing links to your websites.

1. Verify `nginx` and `openssl` versions:

   ```shellsession
   nginx -v; openssl version
   ```

2. Open the configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

3. Add the following lines to the server block, replacing `example.com` with your domain name:

   ```diff lang="nginx" title="/etc/nginx/sites-available/example.com" "example.com"
   server {
       listen 443 ssl;
       listen [::]:443 ssl;

       http2 on;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }

       # SSL certificates
       ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

   +    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
   +    add_header Strict-Transport-Security "max-age=63072000" always;

   +    # Modern configuration
   +    ssl_protocols TLSv1.3;
   +    ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1:secp384r1;
   +    ssl_prefer_server_ciphers off;
   }
   ```

   HSTS is a security feature that tells browsers to only connect to your site using HTTPS. Once you enable HSTS, you cannot disable it for 63072000 seconds (2 years). Make sure your site is fully functional over HTTPS before enabling HSTS.

4. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

5. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

## Cloudflare

### Restrict HTTPS traffic through the firewall

If you're using Cloudflare as a proxy server for your website, you can restrict access to your origin server to only allow requests originating from Cloudflare's IP addresses.

#### Remove existing rules

If you have existing rules that allow traffic from all IP addresses, you may want to remove them before adding the new rules.

1. List the current UFW rules as a numbered list:

   ```shellsession
   sudo ufw status numbered
   ```

2. Either delete a rule by the rule name or number:

- Delete a rule by the name:

  ```shellsession "Nginx Full"
  sudo ufw delete allow "Nginx Full"
  ```

- Delete a rule by the number:

  ```shellsession "NUM"
  sudo ufw delete NUM
  ```

  **Note:** The numbered list will change after a rule is added or deleted.

#### Manually allow Cloudflare IPs

Run the following command to allow traffic from Cloudflare's IP addresses through the firewall. Replace `192.0.2.1` with each IP address listed on the Cloudflare [IP Ranges](https://www.cloudflare.com/ips/) page.

```shellsession "192.0.2.1"
sudo ufw allow from 192.0.2.1 to any app "Nginx HTTPS" comment "Allow from specific IP"
```

#### Using a script

You may automate the process of adding Cloudflare's IP addresses to UFW by using a script. The following script fetches the latest IP addresses from Cloudflare's API and adds them to UFW.

1. Create a file named `add_cloudflare_ufw_rules.sh`:

   ```shellsession
   nano add_cloudflare_ufw_rules.sh
   ```

2. Copy and paste the following code into the file:

   ```shellscript title="add_cloudflare_ufw_rules.sh"
   #!/bin/bash
   # Generated by ChatGPT

   # Cloudflare API URL
   API_URL="https://api.cloudflare.com/client/v4/ips"

   # Function to fetch IPs from Cloudflare API and apply UFW rules
   apply_ufw_rules() {
       echo "Fetching IP list from Cloudflare API..."

       # Fetch the JSON response
       response=$(curl -s "$API_URL")

       # Extract IPv4 and IPv6 ranges using grep, sed, and awk
       ipv4_list=$(echo "$response" | grep -o '"ipv4_cidrs":\[[^]]*' | sed 's/"ipv4_cidrs":\[//' | tr -d '"' | tr ',' '\n')
       ipv6_list=$(echo "$response" | grep -o '"ipv6_cidrs":\[[^]]*' | sed 's/"ipv6_cidrs":\[//' | tr -d '"' | tr ',' '\n')

       # Apply UFW rules for IPv4
       echo "Applying IPv4 rules..."
       for ip in $ipv4_list; do
           if [[ ! -z "$ip" ]]; then
               echo "Adding UFW rule for $ip..."
               sudo ufw allow from "$ip" to any app "Nginx HTTPS" comment "Cloudflare"
           fi
       done

       # Apply UFW rules for IPv6
       echo "Applying IPv6 rules..."
       for ip in $ipv6_list; do
           if [[ ! -z "$ip" ]]; then
               echo "Adding UFW rule for $ip..."
               sudo ufw allow from "$ip" to any app "Nginx HTTPS" comment "Cloudflare"
           fi
       done
   }

   # Apply rules
   apply_ufw_rules

   echo "All UFW rules have been added."
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Make the script executable and run it:

   ```shellsession
   chmod +x add_cloudflare_ufw_rules.sh && ./add_cloudflare_ufw_rules.sh
   ```

5. Verify that the rules were added successfully:

   ```shellsession
   sudo ufw status
   ```

   You should see something similar to the following output:

   ```shellsession
   Status: active

   To                         Action      From
   --                         ------      ----
   OpenSSH                    LIMIT       Anywhere                   # SSH (Rate Limited)
   Nginx HTTPS                ALLOW       173.245.48.0/20            # Cloudflare
   Nginx HTTPS                ALLOW       103.21.244.0/22            # Cloudflare
   Nginx HTTPS                ALLOW       103.22.200.0/22            # Cloudflare
   Nginx HTTPS                ALLOW       103.31.4.0/22              # Cloudflare
   Nginx HTTPS                ALLOW       141.101.64.0/18            # Cloudflare
   Nginx HTTPS                ALLOW       108.162.192.0/18           # Cloudflare
   Nginx HTTPS                ALLOW       190.93.240.0/20            # Cloudflare
   Nginx HTTPS                ALLOW       188.114.96.0/20            # Cloudflare
   Nginx HTTPS                ALLOW       197.234.240.0/22           # Cloudflare
   Nginx HTTPS                ALLOW       198.41.128.0/17            # Cloudflare
   Nginx HTTPS                ALLOW       162.158.0.0/15             # Cloudflare
   Nginx HTTPS                ALLOW       104.16.0.0/13              # Cloudflare
   Nginx HTTPS                ALLOW       104.24.0.0/14              # Cloudflare
   Nginx HTTPS                ALLOW       172.64.0.0/13              # Cloudflare
   Nginx HTTPS                ALLOW       131.0.72.0/22              # Cloudflare
   OpenSSH (v6)               LIMIT       Anywhere (v6)              # SSH (Rate Limited)
   Nginx HTTPS (v6)           ALLOW       2400:cb00::/32             # Cloudflare
   Nginx HTTPS (v6)           ALLOW       2606:4700::/32             # Cloudflare
   Nginx HTTPS (v6)           ALLOW       2803:f800::/32             # Cloudflare
   Nginx HTTPS (v6)           ALLOW       2405:b500::/32             # Cloudflare
   Nginx HTTPS (v6)           ALLOW       2405:8100::/32             # Cloudflare
   Nginx HTTPS (v6)           ALLOW       2a06:98c0::/29             # Cloudflare
   Nginx HTTPS (v6)           ALLOW       2c0f:f248::/32             # Cloudflare
   ```

### Authenticated Origin Pulls

Authenticated Origin Pulls (AOP) helps ensure requests to your origin server come from the Cloudflare network, which provides an additional layer of security on top of Full or Full (strict) encryption modes.

This authentication becomes particularly important with the Cloudflare Web Application Firewall (WAF). Together with the WAF, you can make sure that **all traffic** is evaluated before receiving a response from your origin server.

1. Download the Cloudflare certificate and move it to the `/etc/nginx/certs` directory:

   ```shellsession
   wget https://developers.cloudflare.com/ssl/static/authenticated_origin_pull_ca.pem
   sudo mkdir -p /etc/nginx/certs
   sudo mv authenticated_origin_pull_ca.pem /etc/nginx/certs/cloudflare.crt
   ```

2. Open your configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

3. Add the following lines to the server block, replacing `example.com` with your domain name:

   ```diff lang="nginx" title="/etc/nginx/sites-available/example.com" "example.com"
   server {
       listen 443 ssl;
       listen [::]:443 ssl;

       http2 on;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }

       # SSL certificates
       ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

       # HSTS (ngx_http_headers_module is required) (63072000 seconds)
       add_header Strict-Transport-Security "max-age=63072000" always;

       # Modern configuration
       ssl_protocols TLSv1.3;
       ssl_ecdh_curve X25519:prime256v1:secp384r1;
       ssl_prefer_server_ciphers off;

   +    # Authenticated Origin Pulls (mTLS)
   +    ssl_verify_client on;
   +    ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
   }
   ```

4. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

5. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

### Restoring original visitor IPs

When your website traffic is routed through the Cloudflare network, they act as a reverse proxy. This allows Cloudflare to speed up page load time by routing packets more efficiently and caching static resources (images, JavaScript, CSS, etc.). As a result, when responding to requests and logging them, your origin server returns a [Cloudflare IP address](https://www.cloudflare.com/ips/).

To restore the original visitor IP addresses in your Nginx logs, you can use the `ngx_http_realip_module` to set the correct client IP address based on the `CF-Connecting-IP` header added by Cloudflare.

1. Open your Nginx configuration file:

   ```shellsession
   sudo nano /etc/nginx/nginx.conf
   ```

2. Add the following lines within the `http` block to set the real IP from Cloudflare:

   ```diff lang="nginx" title="/etc/nginx/nginx.conf"
   ##
   # Logging Settings
   ##

   +set_real_ip_from 173.245.48.0/20;
   +set_real_ip_from 103.21.244.0/22;
   +set_real_ip_from 103.22.200.0/22;
   +set_real_ip_from 103.31.4.0/22;
   +set_real_ip_from 141.101.64.0/18;
   +set_real_ip_from 108.162.192.0/18;
   +set_real_ip_from 190.93.240.0/20;
   +set_real_ip_from 188.114.96.0/20;
   +set_real_ip_from 197.234.240.0/22;
   +set_real_ip_from 198.41.128.0/17;
   +set_real_ip_from 162.158.0.0/15;
   +set_real_ip_from 104.16.0.0/13;
   +set_real_ip_from 104.24.0.0/14;
   +set_real_ip_from 172.64.0.0/13;
   +set_real_ip_from 131.0.72.0/22;
   +set_real_ip_from 2400:cb00::/32;
   +set_real_ip_from 2606:4700::/32;
   +set_real_ip_from 2803:f800::/32;
   +set_real_ip_from 2405:b500::/32;
   +set_real_ip_from 2405:8100::/32;
   +set_real_ip_from 2a06:98c0::/29;
   +set_real_ip_from 2c0f:f248::/32;

   +real_ip_header CF-Connecting-IP;
   +#real_ip_header X-Forwarded-For;

   +log_format custom '$remote_addr - $remote_user [$time_local] '
   +                  '"$request" $status $body_bytes_sent '
   +                  '"$http_referer" "$http_user_agent" '
   +                  '$http_cf_connecting_ip "$http_x_forwarded_for"';

   -access_log /var/log/nginx/access.log;
   +access_log /var/log/nginx/access.log custom;
   error_log  /var/log/nginx/error.log warn;
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

## Nginx, part 2

### Prevent processing requests with undefined server names

When Nginx receives a request that doesn't match any defined server names, it defaults to the first server block defined in the configuration. This can lead to unintended behavior, such as serving content from the wrong site or exposing sensitive information. To prevent this, you can create a catch-all server block that rejects requests with undefined server names.

If you're using Cloudflare, you should block all traffic that doesn't originate from [Cloudflare IPs](#allow-cloudflare-ips-through-the-firewall).

```nginx
server {
    # Match all HTTP requests on port 80 and close the connection
    listen 80 default_server;
    listen [::]:80 default_server;

    # Catch-all server name
    server_name _;

    # Close the connection without sending a response
    return 444;
}

server {
    # Match all HTTPS requests on port 443 and reject the SSL handshake
    listen 443 ssl default_server;
    listen [::]:443 ssl default_server;

    http2 on;

    # Catch-all server name
    server_name _;

    # Provide a dummy SSL certificate and key for the handshake
    # These can be self-signed or a generic certificate.
    include snippets/snakeoil.conf;

    # Reject the SSL handshake immediately
    ssl_reject_handshake on;
}
```

### Disable server tokens

To enhance the security of your Nginx server, you can disable server tokens, which prevents Nginx from revealing its version number in error pages and HTTP headers.

1. Open your Nginx configuration file:

   ```shellsession
   sudo nano /etc/nginx/nginx.conf
   ```

2. Find the `server_tokens` directive within the `http` block and set it to `off`:

   ```diff lang="nginx" title="/etc/nginx/nginx.conf"
   http {

   ##
   # Basic Settings
   ##

   sendfile on;
   tcp_nopush on;
   types_hash_max_size 2048;
   -# server_tokens off;
   +server_tokens off;
   ...
   }
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
    sudo nginx -t && sudo systemctl reload nginx
   ```

### Add common security headers

You can enhance the security of your web server by adding common security headers to your Nginx configuration.

Some of these may be added automatically if you use [Cloudflare](#cloudflare), but some of the ones added are depreciated and they're still missing the more modern ones, so it's still a good practice to include all of them in your server configuration.

1. Open your configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

2. Add the following lines to the `server` block, replacing `example.com` with your domain name:

   ```diff lang="nginx" title="/etc/nginx/sites-available/example.com" "example.com"
   server {
       listen 443 ssl;
       listen [::]:443 ssl;

       http2 on;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }

   +    # Common security headers
   +    add_header X-Content-Type-Options "nosniff" always;
   +    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
   +    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
   +    add_header Cross-Origin-Opener-Policy "same-origin" always;
   +    add_header Cross-Origin-Embedder-Policy "require-corp" always;
   +    add_header Cross-Origin-Resource-Policy "same-origin" always;

       # SSL certificates
       ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

       # HSTS (ngx_http_headers_module is required) (63072000 seconds)
       add_header Strict-Transport-Security "max-age=63072000" always;

       # Modern configuration
       ssl_protocols TLSv1.3;
       ssl_ecdh_curve X25519:prime256v1:secp384r1;
       ssl_prefer_server_ciphers off;

       # Authenticated Origin Pulls (mTLS)
       ssl_verify_client on;
       ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
   }
   ```

   `Content-Security-Policy` is not included in this list because it requires careful configuration based on your website's specific needs. An incorrect CSP can break functionality on your website. Consider implementing CSP separately after thorough testing.

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

For more information on these headers, please visit [HTTP headers - HTTP MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers).

### Use as a reverse proxy

You may use Nginx as a reverse proxy to terminate HTTPS requests and forward to an insecure application server (e.g., a web application running on a different port or in a Docker container).

1. Create a new configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

2. Copy and paste the following configuration into the file, replacing `example.com` with your domain name and `http://localhost:8080` with the address of your application server:

   ```nginx title="/etc/nginx/sites-available/example.com" "example.com" "http://localhost:8080"
   server {
       listen 443 ssl;
       listen [::]:443 ssl;

       http2 on;

       server_name example.com;

       location / {
           proxy_pass http://localhost:8080;

           # Preserve client information
           proxy_set_header Host $host;
           proxy_set_header X-Real-IP $remote_addr;
           proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
           proxy_set_header X-Forwarded-Host  $host;
           proxy_set_header X-Forwarded-Proto $scheme;

           # RFC7239 Forwarded header (for both IPv4 and IPv6)
           proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme";

           # Optional WebSocket support
           proxy_http_version 1.1;
           proxy_set_header Upgrade $http_upgrade;
           proxy_set_header Connection "upgrade";
       }

       # SSL certificates
       ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

       # HSTS (ngx_http_headers_module is required) (63072000 seconds)
       add_header Strict-Transport-Security "max-age=63072000" always;

       # Modern configuration
       ssl_protocols TLSv1.3;
       ssl_ecdh_curve X25519:prime256v1:secp384r1;
       ssl_prefer_server_ciphers off;

       # Authenticated Origin Pulls (mTLS)
       ssl_verify_client on;
       ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
   }
   ```

   `X-Forwarded-For`, `X-Forwarded-Host`, and `X-Forwarded-Proto` will eventually be replaced by the `Forwarded` header as per [RFC 7239](https://datatracker.ietf.org/doc/html/rfc7239), but many applications and frameworks still rely on the older headers for compatibility.

   `X-Real-IP` is non-standard, but widely used by many applications to obtain the client's real IP address.

   Always consult your application server's documentation for the recommended proxy settings.

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

For more information on these headers, please visit [HTTP headers - HTTP MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers).

### Have files downloaded as attachments

By default, most web browsers will attempt to open any plain text files as web pages. To have them download everything as an attachment instead:

1. Open your configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession "example.com"
   sudo nano /etc/nginx/sites-available/example.com
   ```

2. Add the following lines to the `server` block, replacing `example.com` with your domain name:

   ```diff lang="nginx" title="/etc/nginx/sites-available/example.com" "example.com"
   server {
       listen 443 ssl;
       listen [::]:443 ssl;

       http2 on;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }

   +    location ^~ /downloads/ {
   +        # Your existing root / alias / try_files etc. stay the same
   +        add_header Content-Disposition 'attachment';
   +
   +        # Optional but recommended – prevents some browsers from still trying to render it
   +        default_type application/octet-stream;
   +    }

       # SSL certificates
       ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
       ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

       # HSTS (ngx_http_headers_module is required) (63072000 seconds)
       add_header Strict-Transport-Security "max-age=63072000" always;

       # Modern configuration
       ssl_protocols TLSv1.3;
       ssl_ecdh_curve X25519:prime256v1:secp384r1;
       ssl_prefer_server_ciphers off;

       # Authenticated Origin Pulls (mTLS)
       ssl_verify_client on;
       ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
   }
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

### Use snippets

Nginx snippets are reusable configuration files that can be included in multiple server blocks or locations. They help to keep your configuration DRY (Don't Repeat Yourself) and make it easier to manage.

#### Proxy headers snippet

1. Create a new snippet file for proxy headers:

   ```shellsession
   sudo nano /etc/nginx/snippets/proxy-headers.conf
   ```

2. Copy and paste the following lines into the file:

   ```nginx title="/etc/nginx/snippets/proxy-headers.conf"
   # Preserve client information
   proxy_set_header Host $host;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
   proxy_set_header X-Forwarded-Host  $host;
   proxy_set_header X-Forwarded-Proto $scheme;

   # RFC7239 Forwarded header (for both IPv4 and IPv6)
   proxy_set_header Forwarded "for=$remote_addr;host=$host;proto=$scheme";

   # Optional WebSocket support
   proxy_http_version 1.1;
   proxy_set_header Upgrade $http_upgrade;
   proxy_set_header Connection "upgrade";
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

#### Security headers snippet

1. Create a new snippet file for security headers:

   ```shellsession
   sudo nano /etc/nginx/snippets/security-headers.conf
   ```

2. Copy and paste the following lines into the file:

   ```nginx title="/etc/nginx/snippets/security-headers.conf"
   # Common security headers
   add_header X-Content-Type-Options "nosniff" always;
   add_header Referrer-Policy "strict-origin-when-cross-origin" always;
   add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
   add_header Cross-Origin-Opener-Policy "same-origin" always;
   add_header Cross-Origin-Embedder-Policy "require-corp" always;
   add_header Cross-Origin-Resource-Policy "same-origin" always;
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

#### SSL snippet

1. Create a new snippet file for SSL configuration:

   ```shellsession
   sudo nano /etc/nginx/snippets/ssl.conf
   ```

2. Copy and paste the following lines into the file, replacing `example.com` with your domain name:

   ```nginx title="/etc/nginx/snippets/ssl.conf" "example.com"
   # SSL certificates
   ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
   ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

   # HSTS (ngx_http_headers_module is required) (63072000 seconds)
   add_header Strict-Transport-Security "max-age=63072000" always;

   # Modern configuration
   ssl_protocols TLSv1.3;
   ssl_ecdh_curve X25519:prime256v1:secp384r1;
   ssl_prefer_server_ciphers off;

   # Authenticated Origin Pulls (mTLS)
   ssl_verify_client on;
   ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

#### Update site configuration

1. Open your configuration file for your site, replacing `example.com` with your domain name:

   ```shellsession
   sudo nano /etc/nginx/sites-available/example.com
   ```

2. Make the following changes in the file, replacing `example.com` with your domain name:

   ```diff lang="nginx" title="/etc/nginx/sites-available/example.com" "example.com"
   server {
       listen 443 ssl;
       listen [::]:443 ssl;

       http2 on;

       server_name example.com;

       root /var/www/example.com;
       index index.html;

       location / {
           try_files $uri $uri/ =404;
       }

       # Common security headers
   -    add_header X-Content-Type-Options "nosniff" always;
   -    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
   -    add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;
   -    add_header Cross-Origin-Opener-Policy "same-origin" always;
   -    add_header Cross-Origin-Embedder-Policy "require-corp" always;
   -    add_header Cross-Origin-Resource-Policy "same-origin" always;
   +    # Security headers snippet
   +    include snippets/security-headers.conf;


   -    # SSL certificates
   -    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
   -    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

   -    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
   -    add_header Strict-Transport-Security "max-age=63072000" always;

   -    # Modern configuration
   -    ssl_protocols TLSv1.3;
   -    ssl_ecdh_curve X25519:prime256v1:secp384r1;
   -    ssl_prefer_server_ciphers off;

   -    # Authenticated Origin Pulls (mTLS)
   -    ssl_verify_client on;
   -    ssl_client_certificate /etc/nginx/certs/cloudflare.crt;
   +    # SSL snippet
   +    include snippets/ssl.conf;
   }
   ```

3. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

4. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

5. Repeat these steps for any other sites using the same configuration.

### Enable compression

#### Gzip

1. Open your Nginx configuration file:

   ```shellsession
   sudo nano /etc/nginx/nginx.conf
   ```

2. Add the following line within the `http` block to serve pre-compressed gzip files:

   ```diff lang="nginx" title="/etc/nginx/nginx.conf"
   ##
   # Gzip Settings
   ##

   gzip on;
   +gzip_static on;
   ```

3. Uncomment and adjust the `gzip_comp_level` and `gzip_types` directives to optimize compression for your specific use case:

   ```diff lang="nginx" title="/etc/nginx/nginx.conf"
   ##
   # Gzip Settings
   ##

   gzip on;
   gzip_static on;

   # gzip_vary on;
   # gzip_proxied any;
   +gzip_comp_level 6;
   # gzip_buffers 16 8k;
   # gzip_http_version 1.1;
   +gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
   ```

4. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

5. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

#### Brotli

1. Install the Brotli modules for Nginx and verify that they're installed correctly:

   ```shellsession
   sudo apt install libnginx-mod-http-brotli-filter libnginx-mod-http-brotli-static
   sudo ls /usr/lib/nginx/modules/ | grep brotli
   sudo nginx -t && sudo service nginx restart
   ```

2. Open your Nginx configuration file:

   ```shellsession
   sudo nano /etc/nginx/nginx.conf
   ```

3. Add the following line within the `http` block to enable Brotli compression and serve pre-compressed Brotli files:

   ```diff lang="nginx" title="/etc/nginx/nginx.conf"
   ##
   # Gzip Settings
   ##

   gzip on;
   gzip_static on;

   # gzip_vary on;
   # gzip_proxied any;
   gzip_comp_level 6;
   # gzip_buffers 16 8k;
   # gzip_http_version 1.1;
   gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss>

   +##
   +# Brotli Settings
   +##

   +brotli on;
   +brotli_static on;
   ```

4. Press <kbd>Ctrl + O</kbd> to save the file and <kbd>Ctrl + X</kbd> to exit.

5. Check the configuration and reload `nginx` to apply the changes:

   ```shellsession
   sudo nginx -t && sudo systemctl reload nginx
   ```

## References

The following sources were used while writing this guide:

- [Nginx Documentation](https://nginx.org/en/docs/)
- [Certbot Instructions](https://certbot.eff.org/instructions?ws=nginx&os=snap&tab=wildcard)
- [Certbot Documentation](https://eff-certbot.readthedocs.io/en/stable/)
- [Certbot Cloudflare DNS Plugin](https://certbot-dns-cloudflare.readthedocs.io/en/stable/)
- [Mozilla SSL Configuration Generator](https://ssl-config.mozilla.org/)
- [Cloudflare IP Ranges](https://www.cloudflare.com/ips/)
- [Authenticated Origin Pulls (mTLS)](https://developers.cloudflare.com/ssl/origin-configuration/authenticated-origin-pull/)
- [Restoring original visitor IPs](https://developers.cloudflare.com/support/troubleshooting/restoring-visitor-ips/restoring-original-visitor-ips/)
- [How nginx processes a request](https://nginx.org/en/docs/http/request_processing.html)
- [HTTP headers - HTTP MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers)
