What's Actually Happening
Nginx serves SSL certificate without complete chain. Clients receive certificate validation errors, browsers show security warnings.
The Error You'll See
```bash $ openssl s_client -connect example.com:443 -showcerts
Verify return code: 21 (unable to verify the first certificate) ```
Browser warning:
Your connection is not fully secure
This site is missing a valid, trusted certificate (net::ERR_CERT_AUTHORITY_INVALID)curl error:
```bash $ curl https://example.com
curl: (60) SSL certificate problem: unable to get local issuer certificate ```
Certificate chain depth error:
depth=0 CN = example.com
verify error:num=20:unable to get local issuer certificate
verify return:20Why This Happens
- 1.Missing intermediate certificates - Intermediate CA certificates not included
- 2.Wrong certificate order - Certificates in wrong order in bundle
- 3.Incorrect certificate file - Using leaf certificate only
- 4.Certificate bundle not concatenated - Multiple files not combined
- 5.CA bundle missing - CA certificate bundle not configured
- 6.Self-signed root in chain - Self-signed root included unnecessarily
Step 1: Check Current Certificate Chain
```bash # Check certificate chain from server: openssl s_client -connect example.com:443 -showcerts
# Output should show: # Certificate chain # 0 s:/CN=example.com # i:/CN=Intermediate CA # 1 s:/CN=Intermediate CA # i:/CN=Root CA
# If only one certificate shown, chain is incomplete
# Check certificate details: openssl s_client -connect example.com:443 -showcerts 2>/dev/null | openssl x509 -noout -text
# Check certificate issuer: openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -issuer
# Verify certificate: openssl verify -CAfile /etc/ssl/certs/ca-certificates.crt /path/to/cert.pem ```
Step 2: Obtain Intermediate Certificates
```bash # Download intermediate certificate from CA: # For Let's Encrypt: wget https://letsencrypt.org/certs/lets-encrypt-r3.pem
# For DigiCert: wget https://www.digicert.com/CACerts/DigiCertSHA2SecureServerCA.pem
# For Cloudflare: wget https://developers.cloudflare.com/ssl/static/origin_ca_rsa_root.pem
# Check your CA's website for intermediate certificates
# Extract from existing certificate: openssl x509 -in cert.pem -noout -text | grep "CA Issuers - URI"
# Download from CA Issuers URL: wget -O intermediate.der "http://URL-from-certificate" openssl x509 -inform DER -in intermediate.der -out intermediate.pem
# Get from certificate bundle: # Many CAs provide full chain bundle (fullchain.pem) ```
Step 3: Concatenate Certificate Chain
```bash # Create full chain file: # Order: leaf certificate, intermediate(s), root (optional)
cat cert.pem intermediate.pem > fullchain.pem
# For multiple intermediates: cat cert.pem intermediate1.pem intermediate2.pem > fullchain.pem
# Let's Encrypt already provides fullchain: # cert.pem - Leaf only # fullchain.pem - Leaf + intermediate (use this!) # chain.pem - Intermediate only
# Check file contents: openssl x509 -in fullchain.pem -noout -text | head -20 openssl x509 -in fullchain.pem -noout -issuer -subject
# Verify chain order: # First certificate should be your domain certificate # Following should be intermediates leading to root
# View all certificates in bundle: awk 'BEGIN {c=0} /BEGIN CERTIFICATE/ {c++} {print > "cert" c ".pem"}' fullchain.pem for f in cert*.pem; do openssl x509 -in $f -noout -subject -issuer done rm cert*.pem ```
Step 4: Configure Nginx Certificate
```nginx # In nginx config: server { listen 443 ssl; server_name example.com;
# Use full chain certificate: ssl_certificate /etc/nginx/ssl/fullchain.pem;
# Private key: ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# Optional: CA bundle for client verification: # ssl_client_certificate /etc/nginx/ssl/ca-bundle.pem;
# SSL settings: ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256; ssl_prefer_server_ciphers on; }
# Check config: nginx -t
# Reload Nginx: systemctl reload nginx ```
Step 5: Verify Chain Configuration
```bash # Test certificate chain: openssl s_client -connect example.com:443 -showcerts
# Should show: # depth=2 C = US, O = DigiCert, CN = DigiCert Global Root CA # verify return:1 # depth=1 C = US, O = DigiCert, CN = DigiCert SHA2 Secure Server CA # verify return:1 # depth=0 CN = example.com # verify return:1
# Verify no errors: openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | grep "Verify return" # Should show: Verify return code: 0 (ok)
# Check with specific CA: openssl s_client -connect example.com:443 -CAfile /etc/ssl/certs/ca-certificates.crt
# Online SSL test: # https://www.ssllabs.com/ssltest/analyze.html?d=example.com ```
Step 6: Fix Certificate Order
```bash # If certificates are in wrong order:
# Check current order: openssl crl2pkcs7 -nocrl -certfile fullchain.pem | openssl pkcs7 -print_certs -noout
# Correct order should be: # 1. Server/Leaf certificate (your domain) # 2. Intermediate certificate(s) # 3. Root certificate (optional, often not needed)
# Reorder certificates: # Identify each certificate: for i in {0..N}; do awk "NR==$i" fullchain.pem # Extract each cert done
# Create properly ordered bundle: cat domain.pem intermediate.pem root.pem > fullchain_ordered.pem
# Note: Root certificate typically not needed # Clients usually have root certificates built-in ```
Step 7: Check Let's Encrypt Certificates
```bash # For Let's Encrypt/Certbot:
# List certificates: certbot certificates
# Check certificate locations: # /etc/letsencrypt/live/example.com/ # - cert.pem (leaf only) # - chain.pem (intermediate) # - fullchain.pem (leaf + intermediate) - USE THIS # - privkey.pem (private key)
# Configure Nginx with fullchain: ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Renew certificates: certbot renew --dry-run
# Force renewal: certbot renew --force-renewal
# Check renewal hooks: certbot renew --post-hook "systemctl reload nginx" ```
Step 8: Use Online Certificate Checker
```bash # Use SSL Labs: # https://www.ssllabs.com/ssltest/
# Check chain issues: # "Chain issues: Incomplete" means missing intermediate
# Use check-certificates tool: curl https://example.com -v 2>&1 | grep -E "SSL|certificate"
# Use testssl.sh: git clone https://github.com/drwetter/testssl.sh ./testssl.sh example.com
# Use cert-chain-resolver: # https://github.com/trimstray/cert-chain-resolver cert-chain-resolver -i cert.pem -o fullchain.pem ```
Step 9: Handle Self-Signed Certificates
```bash # For self-signed certificates (development only):
# Generate self-signed with proper chain: openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes
# For self-signed CA + server cert: # 1. Create CA: openssl genrsa -out ca.key 2048 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem
# 2. Create server cert: openssl genrsa -out server.key 2048 openssl req -new -key server.key -out server.csr openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 365 -sha256
# 3. Create chain: cat server.pem ca.pem > fullchain.pem
# Note: Clients need to trust your CA certificate ```
Step 10: Nginx SSL Certificate Verification Script
```bash # Create verification script: cat << 'EOF' > /usr/local/bin/check-nginx-ssl.sh #!/bin/bash
DOMAIN=$1
if [ -z "$DOMAIN" ]; then echo "Usage: $0 domain.com" exit 1 fi
echo "=== Certificate Chain ===" openssl s_client -connect $DOMAIN:443 -showcerts -servername $DOMAIN 2>/dev/null | sed -n '/Certificate chain/,/---/p'
echo "" echo "=== Verification Result ===" openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | grep "Verify return"
echo "" echo "=== Certificate Details ===" openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | openssl x509 -noout -subject -issuer -dates
echo "" echo "=== Chain Depth ===" openssl s_client -connect $DOMAIN:443 -servername $DOMAIN -showcerts 2>/dev/null | grep -E "depth=[0-9]"
echo "" echo "=== TLS Version ===" openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | grep "Protocol"
echo "" echo "=== Cipher Used ===" openssl s_client -connect $DOMAIN:443 -servername $DOMAIN 2>/dev/null | grep "Cipher"
echo "" echo "=== OCSP Status ===" openssl s_client -connect $DOMAIN:443 -servername $DOMAIN -status 2>/dev/null | grep -A10 "OCSP response" EOF
chmod +x /usr/local/bin/check-nginx-ssl.sh
# Usage: /usr/local/bin/check-nginx-ssl.sh example.com
# Quick check: alias ssl-check='openssl s_client -connect' ```
Nginx SSL Certificate Chain Checklist
| Check | Command | Expected |
|---|---|---|
| Chain depth | openssl s_client | depth >= 1 |
| Verify return | openssl s_client | 0 (ok) |
| Certificate file | nginx config | fullchain.pem |
| Chain order | openssl x509 | leaf, intermediate, root |
| CA bundle | ssl_client_certificate | If client cert required |
Verify the Fix
```bash # After fixing certificate chain
# 1. Check verification openssl s_client -connect example.com:443 | grep "Verify return" // 0 (ok)
# 2. Check chain depth openssl s_client -connect example.com:443 -showcerts | grep "depth" // depth >= 1 for intermediate
# 3. Test with curl curl https://example.com // No SSL error
# 4. Check browser // No warning, green lock icon
# 5. SSL Labs test // No chain issues reported
# 6. Check Nginx config nginx -t // Syntax OK ```
Related Issues
- [Fix Nginx SSL Certificate Not Trusted](/articles/fix-nginx-ssl-certificate-not-trusted)
- [Fix SSL Certificate Expired](/articles/fix-ssl-certificate-expired)
- [Fix Nginx HTTPS Not Working](/articles/fix-nginx-https-not-working)