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

- Name
- Youngju Kim
- @fjvbn20031
- 1. Understanding PEM, Key, and CRT File Formats
- 2. Basic Nginx TLS Configuration
- 3. TLS Version Configuration
- 4. Cipher Suite Configuration
- 5. DH Parameters
- 6. HSTS (HTTP Strict Transport Security)
- 7. OCSP Stapling
- 8. SSL Session Caching
- 9. HTTP/2 Configuration
- 10. mTLS (Mutual TLS) Configuration
- 11. Self-Signed Certificates (Development)
- 12. Certificate Conversion
- 13. Certificate Verification Commands
- 14. Security Audit Tools
- 15. Let's Encrypt Auto-Renewal Integration
- 16. Configuration Checklist
- 17. Conclusion
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
| Algorithm | Key Size | Performance | Security | Recommendation |
|---|---|---|---|---|
| RSA 2048 | 2048 bit | Slow | Sufficient | Compatibility-focused |
| RSA 4096 | 4096 bit | Very slow | High | High security needed |
| ECDSA P-256 | 256 bit | Fast | Sufficient | Recommended (Modern) |
| ECDSA P-384 | 384 bit | Fast | High | High 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
| Version | Status | Recommendation |
|---|---|---|
| SSL 2.0 | Deprecated (1996) | Never use |
| SSL 3.0 | Deprecated (POODLE) | Never use |
| TLS 1.0 | Deprecated (2021) | Disable |
| TLS 1.1 | Deprecated (2021) | Disable |
| TLS 1.2 | Current standard | Use |
| TLS 1.3 | Latest standard | Use (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:
| Item | Command / Check | Standard |
|---|---|---|
| TLS version | ssl_protocols | TLSv1.2 TLSv1.3 only |
| Certificate chain | Use fullchain.pem | Includes intermediate CA |
| HSTS | Check header | max-age 1 year+ |
| OCSP Stapling | openssl s_client -status | Cert Status: good |
| HTTP redirect | curl -I http://domain | 301 to HTTPS |
| SSL Labs grade | ssllabs.com | Target A+ |
| Private key permissions | ls -la privkey.pem | 600 (root only) |
| Auto renewal | certbot renew --dry-run | Success |
| DH parameters | 2048-bit minimum | Generated |
| Session Tickets | off | Forward 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