The difficulty of configuring SSL on Nginx is not in “getting it configured,” but in ensuring it works correctly for others. Most issues (incomplete certificate chains, old protocols not disabled, HTTP not redirecting) are not visible during configuration and only surface when accessed externally.
Below is a ready-to-use configuration, followed by a line-by-line explanation of its purpose and common errors.
Preparation: Two Files
| File | Content | Description |
|---|---|---|
fullchain.pem |
Server Certificate + Intermediate Certificates | This must be used in the configuration |
privkey.pem |
Private Key | Do not leak; set permissions to 600 |
# Place in a fixed location
mkdir -p /etc/nginx/ssl/example.com
cp fullchain.pem /etc/nginx/ssl/example.com/
cp privkey.pem /etc/nginx/ssl/example.com/
# Tighten private key permissions
chmod 600 /etc/nginx/ssl/example.com/privkey.pem
# Verify certificate content (check how many certificates are included to ensure chain completeness)
openssl crl2pkcs7 -nocrl -certfile fullchain.pem
| openssl pkcs7 -print_certs -noout
# Verify that the certificate and private key match (the three values must be consistent)
openssl x509 -noout -modulus -in fullchain.pem | openssl md5
openssl rsa -noout -modulus -in privkey.pem | openssl md5
Mismatched certificates and private keys are a common low-level error, manifesting as immediate Nginx startup failure. The two MD5 comparisons above can detect this before configuration.
Complete Configuration Block
# HTTP: Redirect all traffic to HTTPS
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
root /var/www/html;
index index.php index.html;
# ① Certificate: Must use fullchain, do not use single cert
ssl_certificate /etc/nginx/ssl/example.com/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/example.com/privkey.pem;
# ② Only allow TLS 1.2 / 1.3
ssl_protocols TLSv1.2 TLSv1.3;
# ③ Cipher suites
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;
# ④ Session reuse (improves performance)
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_session_tickets off;
# ⑤ HSTS (enable only after confirming long-term HTTPS usage)
add_header Strict-Transport-Security "max-age=31536000" always;
location / {
try_files $uri $uri/ /index.php?$args;
}
location ~ .php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
Line-by-Line Explanation and Common Errors
| Configuration Item | Purpose | Common Error |
|---|---|---|
ssl_certificate |
Specifies the certificate | Using cert instead of fullchain → Incomplete chain |
ssl_protocols |
Allowed protocols | Keeping TLSv1/TLSv1.1 results in penalty points from security scans |
ssl_ciphers |
Cipher suites | Copying outdated cipher lists |
ssl_session_cache |
Session reuse | Not configured, leading to high handshake overhead |
Strict-Transport-Security |
Force HTTPS | Enabling without confirmation locks HTTP services permanently |
return 301 |
HTTP redirect | Written as rewrite, or missing $request_uri causing all requests to redirect to the homepage |
HSTS Requires Careful Consideration
The effect of Strict-Transport-Security is: within the max-age period, the browser will force HTTPS access, and you cannot revert to HTTP.
Therefore:
- Enable only after confirming long-term HTTPS usage for the entire site
- Start with a short
max-age(e.g., 300 seconds) to test, then increase if no issues arise - If the site has subdomains or interfaces that must use HTTP, do not enable HSTS
Verification
# ① Syntax check (mandatory after configuration changes)
nginx -t
# ② Reload
nginx -s reload
# ③ Confirm HTTP redirects
curl -I http://example.com/ | head -3
# Should see 301 and Location: https://...
# ④ Confirm complete certificate chain (Critical: -verify_return_error)
openssl s_client -connect example.com:443 -servername example.com
-verify_return_error </dev/null 2>/dev/null | grep -i 'verify return'
# Must show Verify return code: 0 (ok)
# ⑤ Confirm protocol version
openssl s_client -connect example.com:443 -servername example.com -tls1_2 </dev/null
openssl s_client -connect example.com:443 -servername example.com -tls1_1 </dev/null
# The TLS 1.1 command should fail
# ⑥ Confirm validity period
openssl s_client -connect example.com:443 -servername example.com </dev/null 2>/dev/null
| openssl x509 -noout -dates
# ⑦ Confirm HSTS header
curl -I https://example.com/ | grep -i strict
Decision Tree
Post-configuration verification
├─ nginx -t fails
│ ├─ Indicates certificate/private key mismatch → Compare MD5 of both modulus values
│ └─ Indicates file not found → Check path and permissions
├─ Browser works normally, but external scan reports incomplete chain
│ └─ Used cert instead of fullchain → Switch to fullchain
├─ HTTP does not redirect
│ ├─ Missing server block for port 80 → Add it
│ └─ All requests redirect to homepage → Missing $request_uri in 301 redirect
├─ Old devices/clients report invalid certificate
│ ├─ verify_return_error is non-zero → Incomplete chain
│ └─ Protocol version → When TLS only allows 1.2/1.3, very old clients cannot connect
└─ Want to temporarily revert to HTTP but cannot
└─ HSTS enabled → Must wait for max-age to expire
Scenario Simulation: Certificate Just Purchased, Still Reported as Untrusted After Configuration
Symptom: A new certificate was purchased and deployed according to the guide.
Operations staff access via desktop browsers normally, but users gradually report “certificate untrusted,”
with stable errors on some mobile devices.
- When facing inconsistent conclusions, determine whose judgment to trust first → Desktop browsers automatically fill in missing intermediate certificates,
so their “normal” status cannot serve as evidence of a complete chain. This step shifts the baseline for judgment. - Re-verify using strict methods → Adding parameters to disable auto-completion returns a non-zero exit code,
confirming the chain is indeed incomplete. - Check how many certificates the server actually sends → Only one is present, indicating the configuration references only the site certificate itself,
and the intermediate certificate was not sent along. - Merge into a complete chain in the correct order → Site certificate first, intermediate certificate second; reversing the order also renders it unusable.
- Reload configuration and re-verify strictly → Exit code becomes 0, and the number of sent certificates increases.
- Re-verify on clients that do not auto-complete → Test both mobile devices and command-line tools,
as their behavior represents what real users encounter.
# Strict verification: Exit code must be 0
echo | openssl s_client -connect example.com:443 -servername example.com
-verify_return_error 2>&1 | grep -E 'Verify return code|depth'
# Check how many certificates the server actually sends
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null
| grep -c 'BEGIN CERTIFICATE'
# Concatenation order for complete chain: Site certificate first, intermediate certificate second
cat site.crt intermediate.crt > fullchain.pem
# Point configuration to the merged file, check syntax first, then reload
nginx -t && systemctl reload nginx
Root Cause: The configuration referenced the site certificate itself rather than the merged complete chain,
so the intermediate certificate was not sent during the handshake. Clients with strong completion capabilities were unaffected,
while those without auto-completion reported errors directly.
Two key takeaways:
- “Works fine for me” does not equal successful deployment; rely on strict verification results.
Browsers fill in missing parts for you, so their conclusions are naturally optimistic and cannot be used to judge certificate chains. - Final verification must be performed on clients that do not auto-complete.
The behavior of such clients represents the lower bound for real users—if any client type reports an error, the deployment is not truly complete.
8 Common Pitfalls
- Using cert instead of fullchain: The most common issue; browsers may not reveal it.
- Mismatched certificate and private key: Nginx fails to start; compare MD5s before configuration.
- Omitting $request_uri in 301 redirect: All pages redirect to the homepage.
- Keeping TLS 1.0/1.1: Security scans will deduct points.
- Enabling HSTS without confirmation: Unable to revert to HTTP later.
- Overly permissive private key permissions: Should be set to 600.
- Not reloading after changes:
nginx -tonly checks syntax; changes do not take effect. - Firewall not allowing port 443: Configuration is correct, but access fails.
Real Customer Feedback (From Cloud Market Reviews and Consultations for Similar Products)
The following excerpts are taken from real buyer reviews and consultations for similar operations services on Alibaba Cloud / Tencent Cloud / Huawei Cloud Cloud Markets (service providers include our store and competitors, provided solely for reference of genuine feedback, not fictional tickets from this site):
y****6 Rating: 5.0 Points 2023-09-13 16:30 k****0 Rating: 5.0 Points 2022-04-12 19:03 Mai****8 Rating: 5.0 Points 2021-06-23 11:45 Reply and processing speed is very fast 1
1****9 Rating: 5.0 Points 2023-12-22 14:48
a****8 Rating: 5.0 Points 2025-08-22 14:43 r****8 Rating: 5.0 Points 2025-05-22 15:34 t****c Rating: 5.0 Points 2025-04-18 13:58 Cai****Kang Rating: 5.0 Points 2024-08-31 12:05 2****2 Rating: 5.0 Points 2024-02-21 11:08