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:

bash
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:

bash
depth=0 CN = example.com
verify error:num=20:unable to get local issuer certificate
verify return:20

Why This Happens

  1. 1.Missing intermediate certificates - Intermediate CA certificates not included
  2. 2.Wrong certificate order - Certificates in wrong order in bundle
  3. 3.Incorrect certificate file - Using leaf certificate only
  4. 4.Certificate bundle not concatenated - Multiple files not combined
  5. 5.CA bundle missing - CA certificate bundle not configured
  6. 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

CheckCommandExpected
Chain depthopenssl s_clientdepth >= 1
Verify returnopenssl s_client0 (ok)
Certificate filenginx configfullchain.pem
Chain orderopenssl x509leaf, intermediate, root
CA bundlessl_client_certificateIf 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 ```

  • [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)