Table of Contents

This post serves as a personal reminder and a guide for others on how to configure an Nginx virtual host with SSL support that achieves an A+ rating and an exceptional security score.

You can test your own site’s security at SSL Labs.

Global Configuration: nginx.conf

This global setup ensures high-performance TLS settings and correctly handles real IP addresses from Cloudflare and Podman.

user  nginx;
worker_processes  auto;
error_log  /var/log/nginx/error.log notice;
pid        /run/nginx.pid;

events {
    worker_connections  1024;
}

http {
    server_tokens off; # Security: Hide Nginx version
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    sendfile        on;
    keepalive_timeout  65;

    # === TLS GLOBAL SETTINGS ===
    # Disable old protocols (TLS 1.0, 1.1)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ecdh_curve X25519:prime256v1:secp384r1;

    # Strong Cipher Suite
    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:DHE-RSA-CHACHA20-POLY1305;
    
    ssl_prefer_server_ciphers off; # Let the client choose best available cipher

    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;
    ssl_session_tickets off;

    # Diffie-Hellman Parameters (Generate with: openssl dhparam -out dhparam.pem 4096)
    ssl_dhparam /etc/letsencrypt/dhparam.pem;

    # OCSP Stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # Secure Resolvers
    resolver 185.222.222.222 45.11.45.11 [2a09::] [2a11::] valid=300s;
    resolver_timeout 5s;

    # Podman Real IP Headers
    set_real_ip_from 10.43.0.0/24;
    real_ip_header    X-Forwarded-For;
    real_ip_recursive on;

    include /etc/nginx/conf.d/*.conf;
}

Virtual Host Configuration (e.g., WordPress)

# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name example.com www.example.com;

    # Allow ACME challenge for Let's Encrypt
    location /.well-known/acme-challenge/ {
        proxy_pass http://host.containers.internal:8080;
        proxy_set_header Host $host;
    }

    location / {
        return 301 https://example.com$request_uri;
    }
}

# Redirect www to non-www (HTTPS)
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.com;

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

    return 301 https://example.com$request_uri;
}

# Primary HTTPS Server
server {
    listen 443 ssl;
    listen [::]:443 ssl;
    http2 on;
    server_name example.com;

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

    # Security Headers
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
    add_header X-Frame-Options DENY;
    add_header X-Content-Type-Options nosniff;

    location / {
        proxy_pass http://xxx_app:80;
        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;

        proxy_connect_timeout 90;
        proxy_send_timeout 90;
        proxy_read_timeout 90;
        proxy_buffers 32 4k;
        client_max_body_size 64M;
    }
}

Categorized in:

Tutorials,

Tagged in:

, , ,