Skip to content
Published on

[DevOps] Nginx TLS Configuration Complete Guide: PEM/Key Setup and Security Optimization

Authors

1. Understanding PEM, Key, and CRT File Formats

1.1 PEM (Privacy Enhanced Mail)

PEM is a Base64-encoded certificate/key format that can be opened with a text editor.

-----BEGIN CERTIFICATE-----
MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw
TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh
... (Base64 encoded data)
-----END CERTIFICATE-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7o4qne60TB3pO
... (Base64 encoded data)
-----END PRIVATE KEY-----

1.2 DER (Distinguished Encoding Rules)

Binary format. Equivalent to PEM with Base64 encoding removed.

1.3 Key Types

AlgorithmKey SizePerformanceSecurityRecommendation
RSA 20482048 bitSlowSufficientCompatibility-focused
RSA 40964096 bitVery slowHighHigh security needed
ECDSA P-256256 bitFastSufficientRecommended (Modern)
ECDSA P-384384 bitFastHighHigh security + performance

ECDSA has much smaller key sizes than RSA at equivalent security levels, resulting in superior TLS handshake performance.

1.4 Let's Encrypt File Mapping

/etc/letsencrypt/live/example.com/
  fullchain.pem  = Server cert + Intermediate CA cert
  privkey.pem    = Private key
  chain.pem      = Intermediate CA cert only
  cert.pem       = Server cert only

Files used by Nginx:
  ssl_certificate     -> fullchain.pem
  ssl_certificate_key -> privkey.pem

2. Basic Nginx TLS Configuration

2.1 Minimal Setup

server {
    listen 443 ssl;
    server_name example.com;

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

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

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

2.2 HTTP to HTTPS Redirect

server {
    listen 80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

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

2.3 Complete Production Configuration

# /etc/nginx/conf.d/ssl-params.conf
ssl_protocols TLSv1.2 TLSv1.3;
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_dhparam /etc/nginx/ssl/dhparam.pem;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

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

add_header X-Frame-Options DENY 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;
# /etc/nginx/sites-available/example.com
server {
    listen 80;
    server_name example.com www.example.com;

    location /.well-known/acme-challenge/ {
        root /var/www/certbot;
    }

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

server {
    listen 443 ssl http2;
    server_name example.com www.example.com;

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

    include /etc/nginx/conf.d/ssl-params.conf;

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

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

    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff2)$ {
        expires 30d;
        add_header Cache-Control "public, immutable";
    }
}

3. TLS Version Configuration

3.1 TLS Version Security Status

VersionStatusRecommendation
SSL 2.0Deprecated (1996)Never use
SSL 3.0Deprecated (POODLE)Never use
TLS 1.0Deprecated (2021)Disable
TLS 1.1Deprecated (2021)Disable
TLS 1.2Current standardUse
TLS 1.3Latest standardUse (recommended)

3.2 Allow Only TLS 1.2/1.3

ssl_protocols TLSv1.2 TLSv1.3;

3.3 TLS 1.3 Only (Modern)

ssl_protocols TLSv1.3;

TLS 1.3 advantages:

  • Reduced handshake round trips (1-RTT, 0-RTT)
  • Removed vulnerable legacy cipher suites
  • Forward Secrecy enabled by default
  • Encrypted handshake (including server certificate)

4. Cipher Suite Configuration

4.1 Mozilla SSL Configuration Generator Profiles

Mozilla provides three configuration profiles.

Modern (TLS 1.3 only):

ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers off;

Intermediate (TLS 1.2 + 1.3, recommended):

ssl_protocols TLSv1.2 TLSv1.3;
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;

4.2 Cipher Suite Name Breakdown

ECDHE-RSA-AES256-GCM-SHA384 meaning:

ECDHE  = Key exchange (Elliptic Curve Diffie-Hellman Ephemeral)
         -> Provides Forward Secrecy
RSA    = Authentication algorithm (server certificate signing)
AES256 = Symmetric encryption (256-bit AES)
GCM    = Encryption mode (Galois/Counter Mode, AEAD)
SHA384 = Hash function (integrity verification)

5. DH Parameters

Parameters used for Diffie-Hellman key exchange. Required when using DHE cipher suites.

# Generate DH parameters (2048-bit minimum recommended)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048

# Stronger 4096-bit (takes longer to generate)
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
ssl_dhparam /etc/nginx/ssl/dhparam.pem;

Note: TLS 1.3 uses only ECDHE instead of DHE, so DH parameters are not needed. Removing DHE cipher suites from TLS 1.2 also eliminates the need.


6. HSTS (HTTP Strict Transport Security)

6.1 What Is HSTS

HSTS is an HTTP header that instructs browsers to always connect to the domain using HTTPS.

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

6.2 HSTS Preload List

Registering in the HSTS Preload List forces HTTPS from the very first browser visit.

Requirements:

  • Valid TLS certificate
  • Redirect from port 80 to 443
  • HTTPS applied to all subdomains
  • HSTS header with min max-age of 31536000 (1 year), includeSubDomains, preload

Registration site: hstspreload.org

6.3 Caution

  • HSTS is hard to undo (browser blocks HTTP for max-age duration)
  • Start with a small max-age (e.g., 300 seconds) and gradually increase
  • Verify all subdomains support HTTPS before adding includeSubDomains

7. OCSP Stapling

7.1 What Is OCSP Stapling

OCSP (Online Certificate Status Protocol) checks whether a certificate has been revoked. OCSP Stapling has the server pre-fetch the OCSP response from the CA and deliver it to the client during the TLS handshake.

Without OCSP Stapling:
Client -> Server (TLS handshake)
Client -> CA OCSP Server (certificate validity check) <- additional delay

With OCSP Stapling:
Server -> CA OCSP Server (periodically refreshes OCSP response)
Client -> Server (TLS handshake + OCSP response included) <- faster

7.2 Configuration

ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;

7.3 Verify OCSP Stapling

openssl s_client -connect example.com:443 -servername example.com -status < /dev/null 2>/dev/null | grep -A 20 "OCSP Response"

8. SSL Session Caching

# Shared memory cache (shared across worker processes)
# 10MB cache = approximately 40,000 sessions
ssl_session_cache shared:SSL:10m;

ssl_session_timeout 1d;

# Disable session tickets (to ensure Forward Secrecy)
ssl_session_tickets off;

Why disable Session Tickets: The ticket encryption key controls the security of all sessions. If key rotation is not properly managed, Forward Secrecy breaks. TLS 1.3 offers 0-RTT reconnection as an alternative.


9. HTTP/2 Configuration

9.1 Enable HTTP/2

server {
    listen 443 ssl http2;
    server_name example.com;
    # ... SSL config ...
}

HTTP/2 advantages:

  • Multiplexing: Handle multiple requests over a single TCP connection
  • Header compression: HPACK compresses HTTP headers
  • Server push: Send resources before client requests them
  • Priority: Set per-resource transmission priority

9.2 HTTP/3 (QUIC) Preparation

# Nginx 1.25.0+ (or nginx-quic)
server {
    listen 443 ssl;
    listen 443 quic reuseport;
    http2 on;
    http3 on;

    server_name example.com;

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

    add_header Alt-Svc 'h3=":443"; ma=86400' always;
}

10. mTLS (Mutual TLS) Configuration

10.1 What Is mTLS

Regular TLS has only the server present a certificate, but mTLS requires the client to also present a certificate for mutual authentication.

Regular TLS:
Client ---- verify server cert ----> Server

Mutual TLS:
Client <--- verify server cert ----> Server
Client ---- client certificate ----> Server (server verifies client cert)

Use cases:

  • Inter-microservice communication (service mesh)
  • API authentication (API Gateway)
  • IoT device authentication
  • Internal admin tool access control

10.2 Issuing Client Certificates with CA

# 1. Generate CA private key
openssl genrsa -out ca.key 4096

# 2. Generate CA certificate (self-signed)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
  -subj "/C=US/ST=California/O=MyOrg/CN=Internal CA"

# 3. Generate client private key
openssl genrsa -out client.key 2048

# 4. Generate client CSR
openssl req -new -key client.key -out client.csr \
  -subj "/C=US/ST=California/O=MyOrg/CN=service-a"

# 5. Sign client certificate with CA
openssl x509 -req -days 365 -in client.csr \
  -CA ca.crt -CAkey ca.key -CAcreateserial \
  -out client.crt

# 6. Create PFX/PKCS12 bundle (for browser import)
openssl pkcs12 -export -out client.pfx \
  -inkey client.key -in client.crt -certfile ca.crt

10.3 Nginx mTLS Configuration

server {
    listen 443 ssl http2;
    server_name api.example.com;

    ssl_certificate     /etc/nginx/ssl/server.fullchain.pem;
    ssl_certificate_key /etc/nginx/ssl/server.privkey.pem;

    ssl_client_certificate /etc/nginx/ssl/ca.crt;
    ssl_verify_client on;
    ssl_verify_depth 2;

    location / {
        proxy_pass http://backend;
        proxy_set_header X-SSL-Client-Cert $ssl_client_cert;
        proxy_set_header X-SSL-Client-S-DN $ssl_client_s_dn;
        proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
        proxy_set_header X-SSL-Client-Serial $ssl_client_serial;
    }
}

10.4 ssl_verify_client Options

# on: Client certificate required (400 error without one)
ssl_verify_client on;

# optional: Certificate optional (access allowed without, verified if present)
ssl_verify_client optional;

# optional_no_ca: No verification, just forward (backend verifies)
ssl_verify_client optional_no_ca;

10.5 mTLS Testing

# Request with client certificate
curl --cert client.crt --key client.key \
  --cacert ca.crt \
  https://api.example.com/data

# Request without certificate (fails if ssl_verify_client on)
curl --cacert ca.crt https://api.example.com/data
# 400 Bad Request - No required SSL certificate was sent

11. Self-Signed Certificates (Development)

11.1 Single Domain

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -subj "/C=US/ST=California/O=Dev/CN=localhost"

11.2 With SAN (Subject Alternative Name)

cat > san.cnf << 'SANEOF'
[req]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = v3_req

[dn]
C = US
ST = California
O = Dev
CN = localhost

[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = localhost
DNS.2 = myapp.local
DNS.3 = *.myapp.local
IP.1 = 127.0.0.1
IP.2 = 192.168.1.100
SANEOF

openssl req -x509 -newkey rsa:2048 -nodes \
  -keyout selfsigned.key \
  -out selfsigned.crt \
  -days 365 \
  -config san.cnf \
  -extensions v3_req

12. Certificate Conversion

12.1 PEM to DER

openssl x509 -in cert.pem -outform DER -out cert.der
openssl rsa -in key.pem -outform DER -out key.der

12.2 DER to PEM

openssl x509 -in cert.der -inform DER -outform PEM -out cert.pem

12.3 PFX/PKCS12 to PEM

openssl pkcs12 -in certificate.pfx -clcerts -nokeys -out cert.pem
openssl pkcs12 -in certificate.pfx -nocerts -nodes -out key.pem
openssl pkcs12 -in certificate.pfx -cacerts -nokeys -out chain.pem

12.4 PEM to PFX/PKCS12

openssl pkcs12 -export \
  -out certificate.pfx \
  -inkey key.pem \
  -in cert.pem \
  -certfile chain.pem

13. Certificate Verification Commands

13.1 Check Certificate Contents

openssl x509 -in cert.pem -text -noout
openssl x509 -in cert.pem -issuer -noout
openssl x509 -in cert.pem -subject -noout
openssl x509 -in cert.pem -enddate -noout
openssl x509 -in cert.pem -noout -ext subjectAltName

13.2 Check Remote Server Certificate

openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  openssl x509 -text -noout

# Check certificate chain
openssl s_client -connect example.com:443 -servername example.com -showcerts < /dev/null 2>/dev/null

# Check TLS version and cipher
openssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null | \
  grep -E "Protocol|Cipher"

13.3 Verify Certificate-Key Matching

# Compare modulus of certificate and key (must match)
openssl x509 -noout -modulus -in cert.pem | openssl md5
openssl rsa -noout -modulus -in key.pem | openssl md5

14. Security Audit Tools

14.1 SSL Labs

Online TLS configuration audit service.

https://www.ssllabs.com/ssltest/analyze.html?d=example.com

Grade criteria:
A+  = Excellent (HSTS + strong configuration)
A   = Good
B   = Fair (improvements recommended)
C   = Poor (security risk)
F   = Dangerous (immediate action required)

14.2 testssl.sh

Open-source command-line TLS audit tool.

git clone --depth 1 https://github.com/drwetter/testssl.sh.git

./testssl.sh/testssl.sh example.com

./testssl.sh/testssl.sh --protocols example.com
./testssl.sh/testssl.sh --ciphers example.com
./testssl.sh/testssl.sh --headers example.com
./testssl.sh/testssl.sh --vulnerabilities example.com

14.3 Nginx Configuration Validation

sudo nginx -t
sudo nginx -T

# Gixy (Nginx security analyzer)
pip install gixy
gixy /etc/nginx/nginx.conf

15. Let's Encrypt Auto-Renewal Integration

15.1 Certbot + Nginx Reload Hook

sudo certbot renew --deploy-hook "systemctl reload nginx"

15.2 Auto-Renewal in Docker Compose

version: '3.8'
services:
  nginx:
    image: nginx:latest
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    restart: unless-stopped
    command: '/bin/sh -c ''while :; do sleep 6h & wait; nginx -s reload; done & nginx -g "daemon off;"'''

  certbot:
    image: certbot/certbot
    volumes:
      - ./certbot/conf:/etc/letsencrypt
      - ./certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait; done'"

16. Configuration Checklist

Items to verify before production deployment:

ItemCommand / CheckStandard
TLS versionssl_protocolsTLSv1.2 TLSv1.3 only
Certificate chainUse fullchain.pemIncludes intermediate CA
HSTSCheck headermax-age 1 year+
OCSP Staplingopenssl s_client -statusCert Status: good
HTTP redirectcurl -I http://domain301 to HTTPS
SSL Labs gradessllabs.comTarget A+
Private key permissionsls -la privkey.pem600 (root only)
Auto renewalcertbot renew --dry-runSuccess
DH parameters2048-bit minimumGenerated
Session TicketsoffForward Secrecy

17. Conclusion

Nginx TLS configuration goes beyond simply applying certificates -- it requires comprehensive security optimization. Key takeaways:

  • Use fullchain.pem + privkey.pem to complete the certificate chain
  • Allow only TLS 1.2/1.3 and disable legacy protocols
  • Use Mozilla Intermediate profile cipher suites
  • Enforce HTTPS with HSTS, improve performance with OCSP Stapling
  • Reduce handshake overhead with SSL session caching
  • Optimize web performance with HTTP/2
  • Implement mutual authentication between microservices with mTLS
  • Target A+ grade on SSL Labs
  • Always configure Let's Encrypt certificate auto-renewal