NGINX SSL/TLS Configuration and HTTP/3 Setup Guide

Professional Server Configuration Documentation

NGINX SSL/TLS HTTP/3 QUIC

Prerequisites

Before beginning this configuration, ensure you have the following requirements met:

  • Root or sudo access to your server
  • NGINX installed and running
  • A registered domain name pointing to your server
  • Certbot installed for SSL certificate management

SSL Certificate Installation

Install Certbot

First, update your system and install Certbot with the Cloudflare DNS plugin:

sudo apt update
sudo apt install certbot python3-certbot-dns-cloudflare

Obtain SSL Certificate

Use Certbot to obtain an SSL certificate for your domain. Replace the paths and domain names with your actual values:

sudo certbot certonly --webroot -w /var/www/example.com/public_html/ -d example.com -d www.example.com

View Installed Certificates

To view all certificates installed on your server:

sudo certbot certificates

Certificate Management Commands

Renew certificates

sudo certbot renew

Force renewal

sudo certbot renew --force-renewal

Delete certificate

sudo certbot delete

Diffie-Hellman Parameters

The Diffie-Hellman parameters file enhances SSL/TLS security by enabling perfect forward secrecy. This ensures that even if your private key is compromised in the future, past communications remain secure.

Create SSL Directory

cd /etc/nginx/
sudo mkdir ssl/
cd ssl/

Generate DH Parameters File

Note: This process may take several minutes to complete. Please be patient.
sudo openssl dhparam -out dhparam.pem 2048

Verify Creation

ls

You should see dhparam.pem in the directory listing.

SSL Configuration Files

Site-Specific SSL Configuration

Create a configuration file for your specific domain's SSL certificates. This file contains the paths to your Let's Encrypt certificates.

sudo nano /etc/nginx/ssl/ssl_certs_example.com.conf

File contents:

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/chain.pem;
Important: Replace example.com with your actual domain name in both the filename and the certificate paths.

Verify File Path

readlink -f /etc/nginx/ssl/ssl_certs_example.com.conf

Copy this path to your text editor for later use.

Global SSL Configuration

Create a universal SSL configuration file that applies to all sites on your server. This file contains security settings that achieve an A+ rating on SSL Labs.

sudo nano /etc/nginx/ssl/ssl_all_sites.conf

File contents:

# CONFIGURATION RESULTS IN A+ RATING AT SSLLABS.COM
# WILL UPDATE DIRECTIVES TO MAINTAIN A+ RATING - CHECK DATE
# DATE: MAY 2025

# SSL CACHING AND PROTOCOLS
ssl_session_cache shared:SSL:20m;
ssl_session_timeout 180m;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers on;

# ssl_ciphers must be on a single line, do not split over multiple lines
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_dhparam /etc/nginx/ssl/dhparam.pem;

# LETS ENCRYPT REMOVED SUPPORT 7 MAY 2025 FOR SSL STAPLING
# REMOVE OR COMMENT BOTH ssl_stapling directives
#ssl_stapling on;
#ssl_stapling_verify on;

# resolver set to Cloudflare
# timeout can be set up to 30s
resolver 1.1.1.1 1.0.0.1;
resolver_timeout 15s;
ssl_session_tickets off;

# HSTS HEADERS
add_header Strict-Transport-Security "max-age=31536000;" always;
# After setting up ALL of your sub domains - comment the above and uncomment the directive below
# add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;" always;

# Enable QUIC and HTTP/3
ssl_early_data on;
add_header Alt-Svc 'h3=":$server_port"; ma=86400';
add_header x-quic 'H3';
quic_retry on;

NGINX Server Block Configuration

Configuration Architecture Diagram

┌─────────────────────────────────────────────────────────────┐ │ HTTP Request Flow │ └─────────────────────────────────────────────────────────────┘ Client Request │ ├─── HTTP (Port 80) ──────────────────────────────┐ │ │ │ ┌──────────────────────────────────┐ │ │ │ Non-Secure Server Block │ │ │ │ Listen: 80 │ │ │ │ Action: 301 Redirect │ │ │ └──────────────────────────────────┘ │ │ │ │ │ ↓ │ │ Return 301 https://domain │ │ │ │ └───────────────────┼──────────────────────────────┘ │ ┌───────────────────┘ │ ├─── HTTPS (Port 443) ─────────────────────────────┐ │ │ │ ┌──────────────────────────────────┐ │ │ │ Secure Server Block │ │ │ │ Listen: 443 ssl (HTTP/2) │ │ │ │ Listen: 443 quic (HTTP/3) │ │ │ │ SSL Certificates Loaded │ │ │ └──────────────────────────────────┘ │ │ │ │ │ ↓ │ │ Process Request │ │ │ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ PHP-FPM Handler │ │ │ │ or Static Content │ │ │ └─────────────────────┘ │ │ │ │ └───────────────────┼───────────────────────────────┘ │ ↓ Return Response

Open Your Site Configuration

cd /etc/nginx/sites-available/
sudo nano example.com.conf

Add Non-Secure Server Block (HTTP Redirect)

Add this server block at the top of your configuration file. This block will redirect all HTTP traffic to HTTPS:

server {
    listen 80;
    server_name example.com www.example.com;
    
    # 301 PERMANENT REDIRECT TO HTTPS
    return 301 https://example.com$request_uri;
}
Important Note: The redirect URL (https://example.com) must match your WordPress installation URL exactly. Do not attempt to change from www to non-www or vice versa at this stage, as it will cause a "too many redirects" error.

Modify Existing Server Block for HTTPS

Locate your existing server { block and modify the listen directives:

Original:

listen 80;

Change to (First Site):

listen 443 ssl;
http2 on;

listen 443 quic reuseport;
http3 on;

For Additional Sites (Second, Third, etc.):

listen 443 ssl;
http2 on;

listen 443 quic;
http3 on;
Note: Only the first site should include the reuseport option. All subsequent sites should omit it. This is because socket options can only be specified once for any given listening socket.

Add SSL Include Directives

Inside the secure server block (the one listening on port 443), add these include directives. Place them after the try_files directive:

include /etc/nginx/ssl/ssl_certs_example.com.conf;
include /etc/nginx/ssl/ssl_all_sites.conf;

Replace ssl_certs_example.com.conf with your actual site-specific SSL configuration filename.

Add HTTP/3 Testing Header

To test HTTP/3, temporarily add this directive inside the location / block:

location / {
    try_files $uri $uri/ /index.php?$args;
    
    # PREVENTS BROWSER CACHING SITE - USED TO TEST HTTP3
    # COMMENT OR REMOVE DIRECTIVE AFTER CONFIRMING HTTP3 IS ENABLED
    add_header Cache-Control 'no-cache,no-store';
}
Important: Remove this caching header after confirming HTTP/3 is working. Leaving it in production will prevent browser caching and negatively impact performance.

Fix HTTP/3 PHP Warning

To prevent "undefined array key HTTP_HOST" warnings in PHP logs when using HTTP/3, add this directive to your PHP processing block:

location ~ \.php$ {
    include snippets/fastcgi-php.conf;
    fastcgi_param HTTP_HOST $host;
    fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
    include /etc/nginx/includes/fastcgi_optimize.conf;
}

The key addition is:

fastcgi_param HTTP_HOST $host;

Complete Example Configuration

Here is a complete example of a properly configured NGINX server block with SSL/TLS and HTTP/3:

# HTTP to HTTPS redirect
server {
    listen 80;
    server_name example.com www.example.com;
    return 301 https://example.com$request_uri;
}

# Secure HTTPS server block
server {
    listen 443 ssl;
    http2 on;
    
    listen 443 quic reuseport;
    http3 on;
    
    server_name example.com www.example.com;
    root /var/www/example.com/public_html;
    index index.php index.html index.htm;
    
    # SSL Configuration
    include /etc/nginx/ssl/ssl_certs_example.com.conf;
    include /etc/nginx/ssl/ssl_all_sites.conf;
    
    location / {
        try_files $uri $uri/ /index.php?$args;
        # Temporary for HTTP/3 testing - remove after verification
        add_header Cache-Control 'no-cache,no-store';
    }
    
    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_param HTTP_HOST $host;
        fastcgi_pass unix:/run/php/php8.3-fpm-example.com.sock;
        include /etc/nginx/includes/fastcgi_optimize.conf;
    }
}

Test and Reload Configuration

Test NGINX syntax:

sudo nginx -t

Expected output:

nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

Reload NGINX:

sudo systemctl reload nginx

HTTP/3 and QUIC Setup

Protocol Hierarchy Diagram

┌──────────────────────────────────────────────────────────┐ │ Client Connection Negotiation │ └──────────────────────────────────────────────────────────┘ Client Request │ ├─── Client supports HTTP/3? ───┐ │ │ ├─── YES │ │ │ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ HTTP/3 (QUIC) │ │ │ │ Port: 443/UDP │ │ │ │ Fast, encrypted │ │ │ └─────────────────────┘ │ │ │ ├─── NO │ │ │ │ │ ↓ │ │ ┌─────────────────────┐ │ │ │ HTTP/2 (TCP) │ │ │ │ Port: 443/TCP │ │ │ │ Multiplexed │ │ │ └─────────────────────┘ │ │ │ └─────────────────────────────────┘

Key Differences

Feature HTTP/2 HTTP/3
Transport TCP UDP (QUIC)
Connection Requires handshake Faster 0-RTT
Head-of-line blocking Possible Eliminated
Encryption Optional Built-in
Performance Good Excellent
Understanding the Benefits:
  • 0-RTT Connection: HTTP/3 can resume connections without a handshake, reducing latency
  • No Head-of-line Blocking: Lost packets only affect the stream they belong to, not all streams
  • Built-in Encryption: QUIC has encryption built into the protocol, improving security
  • Better Mobile Performance: Handles network changes (WiFi to cellular) seamlessly

Testing and Verification

cURL Testing

Use cURL to test your redirects and SSL configuration:

Test HTTP redirect:

curl -I http://example.com

Test www redirect:

curl -I http://www.example.com

Test HTTPS with www:

curl -I https://www.example.com

Test HTTPS without www:

curl -I https://example.com

SSL Labs Testing

Visit SSL Labs and test your domain. With this configuration, you should receive an A+ rating.

What to expect: The SSL Labs test will check your certificate validity, protocol support, cipher strength, and various security headers. An A+ rating indicates that your configuration meets the highest security standards.

HTTP/3 Verification

Online Tool:

Visit https://http3check.net/ and enter your domain.

Browser Console Method:

  1. Open your website in Chrome or Firefox
  2. Press F12 to open Developer Tools
  3. Go to Network tab
  4. Refresh the page (Ctrl+F5 or Cmd+Shift+R)
  5. Right-click on column headers
  6. Ensure Protocol column is enabled
  7. Look for h3 in the Protocol column
Success Indicator: If you see h3 in the Protocol column, HTTP/3 is working correctly! You may also see h2 for HTTP/2 requests.

Automated Certificate Renewal

Setup Cron Job

Configure automatic certificate renewal to ensure your certificates never expire:

sudo crontab -e

Add these lines:

# Renew certificates on the 14th and 28th of each month at 1:00 AM
00 1 14,28 * * certbot renew --force-renewal

# Reload NGINX on the 14th and 28th of each month at 2:00 AM
00 2 14,28 * * systemctl reload nginx

This ensures your certificates are automatically renewed twice per month and NGINX is reloaded to use the new certificates.

Why twice per month? This schedule provides redundancy. If one renewal fails for any reason, you have a second chance before the certificate expires.

Troubleshooting

Common Issues

Issue: "Too Many Redirects" Error

Cause: Mismatch between WordPress URL and NGINX redirect URL, or conflicting redirect rules.

Solution:

  • Ensure the redirect URL in your NGINX configuration matches your WordPress installation URL exactly
  • Do not attempt to change from www to non-www (or vice versa) until WordPress is properly configured
  • Check for conflicting redirects in WordPress plugins or .htaccess files

Issue: HTTP/3 Not Working

Cause: Browser caching or UDP port 443 blocked by firewall.

Solution:

  • Clear browser cache completely (including SSL/TLS state)
  • Ensure the Cache-Control testing header is present during testing
  • Verify UDP port 443 is open:
sudo ufw allow 443/udp
  • Check your firewall logs for blocked UDP traffic
  • Verify your hosting provider doesn't block UDP traffic

Issue: PHP Warnings in Logs

Warning: PHP warning undefined array key HTTP_HOST

Solution:

Add this line to your PHP processing block:

fastcgi_param HTTP_HOST $host;

Issue: SSL Certificate Not Found

Error: nginx: [emerg] cannot load certificate

Solution:

  • Verify certificate paths:
sudo ls -la /etc/letsencrypt/live/example.com/
  • Ensure certificate files exist: fullchain.pem, privkey.pem, chain.pem
  • Check file permissions (certificates should be readable by the nginx user)
  • Verify the domain name in your configuration matches the certificate domain

Issue: 502 Bad Gateway Error

Cause: PHP-FPM not running or socket path incorrect.

Solution:

  • Check PHP-FPM status:
sudo systemctl status php8.3-fpm
  • Verify socket path exists:
ls -la /run/php/php8.3-fpm-example.com.sock
  • Check NGINX error logs for specific errors:
sudo tail -f /var/log/nginx/error.log

Security Best Practices

HSTS (HTTP Strict Transport Security)

The configuration includes HSTS headers to force browsers to use HTTPS:

add_header Strict-Transport-Security "max-age=31536000;" always;

After configuring all subdomains, enable HSTS for subdomains:

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains;" always;
Warning: Only enable includeSubDomains after ensuring ALL subdomains are properly configured with SSL certificates. This directive cannot be easily reversed.

Cipher Suite Security

The configuration uses only strong, modern cipher suites that provide:

  • Perfect Forward Secrecy (PFS): Protects past sessions against future compromises of secret keys
  • Protection against known attacks: Defends against BEAST, CRIME, and other SSL/TLS vulnerabilities
  • Compatibility with modern browsers: Supports all current browsers while maintaining security

TLS Protocol Versions

Only TLS 1.2 and TLS 1.3 are enabled, providing:

  • Strong encryption: Modern cryptographic algorithms
  • Modern security standards: Compliance with current best practices
  • Protection against protocol downgrade attacks: Prevents attackers from forcing older, vulnerable protocols
Note: TLS 1.0 and 1.1 are deprecated and should never be enabled. They contain known vulnerabilities and are not considered secure.

Additional Resources

Useful Commands

View NGINX error log:

sudo tail -f /var/log/nginx/error.log

View NGINX access log:

sudo tail -f /var/log/nginx/access.log

Check NGINX status:

sudo systemctl status nginx

Restart NGINX:

sudo systemctl restart nginx

Check open ports:

sudo netstat -tulpn | grep nginx

Test SSL configuration:

sudo nginx -t

Reload NGINX configuration:

sudo systemctl reload nginx

View certificate details:

sudo certbot certificates

Summary Checklist

Use this checklist to ensure you've completed all necessary steps:

  • Certbot installed and SSL certificates obtained
  • Diffie-Hellman parameters file created
  • Site-specific SSL configuration file created
  • Global SSL configuration file created
  • NGINX server block modified with HTTP redirect
  • NGINX server block modified for HTTPS
  • HTTP/2 and HTTP/3 enabled
  • fastcgi_param HTTP_HOST added to PHP block
  • Configuration tested with sudo nginx -t
  • NGINX reloaded
  • SSL Labs test shows A+ rating
  • HTTP/3 verified as working
  • Cache-Control testing header removed
  • Automated certificate renewal configured
Note: This guide is based on NGINX configuration best practices as of May 2025. Always refer to the latest NGINX and SSL/TLS documentation for updates and security advisories.