[ HOME ]    [ RADIO ]    [ COMPUTERS ]    [ FIELD STATION ]    [ ABOUT ]    [ MODERN MODE ]
=== NGINX HARDENING GUIDE: REVERSE PROXY SECURITY, SSL/TLS, AND BEST PRACTICES ===

Nginx is the backbone of modern web infrastructure, serving as a high-performance web server, load balancer, mail proxy, and reverse proxy. When exposing internal container services or self-hosted applications to local or external networks, hardening your Nginx configuration is essential to prevent information disclosure, SSL stripping, and cross-site scripting.


1. Core Capabilities: What Nginx Can Do

Nginx is highly versatile. Beyond serving standard static web pages, it handles several critical roles across infrastructure setups:

  • Reverse Proxy / SSL Terminator: Sits at the edge of your network, terminating incoming HTTPS traffic, managing TLS certificates, and securely forwarding requests to backend containers or services (e.g., Jellyfin, Nextcloud, Mealie).
  • Static Asset Server: Delivers compiled static sites (such as Hugo or Astro) with ultra-low latency and minimal RAM overhead.
  • Load Balancer: Distributes incoming HTTP/HTTPS traffic across multiple backend nodes using algorithms like round-robin, least-connections, or IP-hash.
  • Content Cache (Microcaching): Caches dynamic backend responses (from Node.js, Python, or PHP-FPM) in memory or disk for short intervals, drastically mitigating load spikes.
  • Rate Limiting & Traffic Shaping: Limits request rates per IP address to safeguard sensitive endpoints against brute-force attacks and abuse.

2. Installation and Directory Layout

Setting up Nginx with a clean directory hierarchy prevents configuration clutter as services scale.

Basic Installation

On Debian or Ubuntu-based distributions, install Nginx from the official package manager:

sudo apt update
sudo apt install nginx
sudo systemctl enable --now nginx

Configuration Architecture

Avoid placing all virtual hosts into the global /etc/nginx/nginx.conf file. Follow the modular structure:

  • /etc/nginx/nginx.conf: Global settings, worker process counts, global logging formats, and rate-limiting zones.
  • /etc/nginx/sites-available/: Individual virtual host (server block) configuration files.
  • /etc/nginx/sites-enabled/: Active site configurations, linked via symbolic links from sites-available.

Enabling a site configuration:

sudo ln -s /etc/nginx/sites-available/my-app.conf /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx

3. Hardened Production Nginx Template

Below is a production-ready template combining version masking, modern TLS protocols, strong cipher suites, HSTS, essential security headers, and rate limiting:

http {
    # 1. Hide Nginx version details in server headers and error pages
    server_tokens off;

    # Performance optimizations
    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;

    # MIME types
    include /etc/nginx/mime.types;
    default_type application/octet-stream;

    # Custom security log format
    log_format secure_format '$remote_addr - $remote_user [$time_local] "$request" '
                             '$status $body_bytes_sent "$http_referer" '
                             '"$http_user_agent" "$http_x_forwarded_for"';
    access_log /var/log/nginx/access.log secure_format;

    # Rate Limiting Zone: 10 requests/second limit per IP
    limit_req_zone $binary_remote_addr zone=app_limit:10m rate=10r/s;

    # Redirect all HTTP traffic to HTTPS
    server {
        listen 80;
        listen [::]:80;
        server_name w4dxr.us www.w4dxr.us;

        return 301 https://$host$request_uri;
    }

    # Main HTTPS Server Block
    server {
        listen 443 ssl;
        listen [::]:443 ssl;
        http2 on;
        server_name w4dxr.us www.w4dxr.us;

        # SSL/TLS Certificate paths (Let's Encrypt / Certbot)
        ssl_certificate /etc/letsencrypt/live/w4dxr.us/fullchain.pem;
        ssl_certificate_key /etc/letsencrypt/live/w4dxr.us/privkey.pem;

        # 2. Enforce Modern TLS Protocols (Disable legacy SSLv3, TLS 1.0, TLS 1.1)
        ssl_protocols TLSv1.2 TLSv1.3;

        # 3. Strong Cipher Suites
        ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
        ssl_prefer_server_ciphers off;

        # SSL Session Caching
        ssl_session_cache shared:SSL:10m;
        ssl_session_timeout 1d;

        # 4. HTTP Strict Transport Security (HSTS)
        add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

        # 5. Essential Security Headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;
        add_header Referrer-Policy "strict-origin-when-cross-origin" always;

        # Static site root
        root /var/www/w4dxr/public;
        index index.html index.htm;

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

        # Reverse Proxy for internal services
        location /app/ {
            # Apply rate limiting
            limit_req zone=app_limit burst=20 nodelay;

            proxy_pass http://127.0.0.1:8080/;
            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-Proto $scheme;

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

4. Production Application Proxy Examples

Different self-hosted apps require tailored Nginx directives. Below are ready-to-use virtual host blocks for common applications.

A. Nextcloud Configuration

Nextcloud requires high upload limits, WebDAV redirects for CalDAV/CardDAV syncing, and memory-buffering adjustments.

server {
    listen 443 ssl;
    http2 on;
    server_name cloud.example.com;

    ssl_certificate /etc/letsencrypt/live/cloud.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/cloud.example.com/privkey.pem;

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-XSS-Protection "1; mode=block" always;

    # Nextcloud DAV service discovery redirects
    location = /.well-known/carddav { return 301 $scheme://$host/remote.php/dav; }
    location = /.well-known/caldav  { return 301 $scheme://$host/remote.php/dav; }

    # Increase body size for file uploads (e.g., 10 Gigabytes)
    client_max_body_size 10G;
    client_body_timeout 300s;
    fastcgi_buffers 64 4K;

    location / {
        proxy_pass http://127.0.0.1:8080;
        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-Proto $scheme;

        # Disable response buffering for large file transfers
        proxy_buffering off;
        proxy_max_temp_file_size 0;
    }
}

B. Jellyfin / Media Server Configuration

Jellyfin needs WebSockets enabled for live dashboard sync and video player control, long timeouts for video streaming, and disabled proxy buffering to prevent stream stalling.

server {
    listen 443 ssl;
    http2 on;
    server_name media.example.com;

    ssl_certificate /etc/letsencrypt/live/media.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/media.example.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8096;
        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-Proto $scheme;

        # Disable buffering for live video streaming
        proxy_buffering off;
    }

    # WebSocket location block for real-time player updates
    location /socket {
        proxy_pass http://127.0.0.1:8096;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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-Proto $scheme;

        # Extend read/send timeouts for persistent WebSocket connections
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
    }
}

C. Vaultwarden / Bitwarden Configuration

Vaultwarden relies on a separate WebSocket endpoint (/notifications/hub) for live password vault updates across browser extensions and mobile apps.

server {
    listen 443 ssl;
    http2 on;
    server_name vault.example.com;

    ssl_certificate /etc/letsencrypt/live/vault.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/vault.example.com/privkey.pem;

    client_max_body_size 525M; # Allows up to 500MB attachment uploads

    # Standard Web Vault
    location / {
        proxy_pass http://127.0.0.1:8000;
        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-Proto $scheme;
    }

    # Vaultwarden Notifications WebSocket
    location /notifications/hub {
        proxy_pass http://127.0.0.1:3012;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        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-Proto $scheme;
    }
}

5. Key Hardening Checklist

  • Disable Server Tokens: Set server_tokens off; to hide version strings from potential vulnerability scans.
  • Header Inheritance Rule: If you declare an add_header directive inside a child location block, Nginx completely ignores all add_header directives set in the parent server block. Re-declare all security headers inside location blocks that specify custom headers.
  • Isolate Backend Services: Avoid binding Docker container ports to 0.0.0.0 (e.g., -p 8080:8080). Bind them explicitly to 127.0.0.1 (-p 127.0.0.1:8080:8080) so they are accessible only through Nginx.
  • Always Test Changes: Run sudo nginx -t before issuing a reload command to ensure configuration syntax is valid.

6. Common Problems and Solutions

502 Bad Gateway

  • Cause: Nginx cannot establish a connection with the upstream backend application.

  • Fix: Verify the backend service is running (systemctl status <service> or docker ps). Confirm the IP address and port match your proxy_pass setting. On SELinux-enabled systems (RHEL/Fedora), enable network connections for HTTP servers:

    sudo setsebool -P httpd_can_network_connect 1
    

403 Forbidden

  • Cause: Nginx lacks read or execute permissions for the requested files or directories.

  • Fix: Update permissions for the Nginx worker user (www-data or nginx):

    sudo chown -R www-data:www-data /var/www/w4dxr/public
    sudo chmod -R 755 /var/www/w4dxr/public
    

    Ensure an index.html or index.php file exists in the directory if directory listing (autoindex) is disabled.

Error: Address already in use

  • Cause: Port 80 or 443 is already bound by another process (e.g., Apache, Docker, or an orphan Nginx process).

  • Fix: Identify the conflicting process and stop it:

    sudo ss -tulpn | grep -E ':80|:443'
    sudo systemctl stop apache2
    

Security Headers Missing on Specific Endpoints

  • Cause: A nested location block contains its own add_header directive, overriding the server block headers.
  • Fix: Copy all global security headers directly into the specific location block that introduces extra custom headers.

413 Request Entity Too Large

  • Cause: Client request or file upload exceeds the default Nginx limit (1MB).

  • Fix: Increase the client body size limit in the http, server, or location block:

    client_max_body_size 50M;
    



W4DXR Retro Terminal | Station Log & Technical Notebook