SSL/TLS Certificate Deployment Guide: Nginx, Apache, and IIS
Deploying SSL/TLS certificates correctly is crucial for securing your web applications. This guide covers certificate installation on the three most popular web servers: Nginx, Apache, and IIS, including proper certificate chain configuration.
Understanding Certificate Chains
Before deployment, it’s essential to understand certificate chains.
What is a Certificate Chain?
A certificate chain (or chain of trust) is a sequence of certificates where each certificate is signed by the next certificate in the chain:
Root CA Certificate (trusted by browsers)
↓ (signs)
Intermediate CA Certificate
↓ (signs)
Your Server Certificate
Why Certificate Chains Matter
Browsers and clients need to verify that your certificate is trusted. They do this by:
- Checking your server certificate
- Following the chain to intermediate certificates
- Verifying the chain ends at a trusted root CA
If the chain is incomplete, users will see security warnings!
Certificate Chain Components
Server Certificate (leaf certificate)
- Your domain’s certificate
- Example:
example.com.crt
Intermediate Certificate(s)
- Issued by root CA to sign server certificates
- One or more certificates
- Example:
intermediate.crtorca-bundle.crt
Root Certificate
- Self-signed, trusted by browsers
- Usually NOT included in server configuration
- Already in browser/OS trust stores
Fullchain vs. Certificate + Chain
Fullchain:
- Single file with server cert + intermediates
- Example:
fullchain.pem - Most convenient for Nginx
Separate files:
- Server certificate:
example.com.crt - Intermediate chain:
ca-bundle.crtorintermediate.crt - Common with Apache
Nginx SSL Configuration
Nginx is one of the most popular web servers, known for performance and simplicity.
Basic SSL Configuration
File structure:
/etc/ssl/certs/
├── example.com.crt # Server certificate
├── intermediate.crt # Intermediate CA
└── fullchain.pem # Combined (cert + intermediate)
/etc/ssl/private/
└── example.com.key # Private key (keep secure!)
Option 1: Using fullchain.pem (Recommended)
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name example.com www.example.com;
# SSL certificate and key
ssl_certificate /etc/ssl/certs/fullchain.pem;
ssl_certificate_key /etc/ssl/private/example.com.key;
# SSL session settings
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:50m;
ssl_session_tickets off;
# Modern SSL configuration
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;
# HSTS (optional but recommended)
add_header Strict-Transport-Security "max-age=63072000" always;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
root /var/www/example.com;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
# HTTP to HTTPS redirect
server {
listen 80;
listen [::]:80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
Option 2: Separate certificate and chain
server {
listen 443 ssl http2;
server_name example.com;
ssl_certificate /etc/ssl/certs/example.com.crt;
ssl_certificate_key /etc/ssl/private/example.com.key;
# Optional: Explicitly specify intermediate certificates
# Nginx will read and include them automatically from ssl_certificate
# if it contains the full chain
# ... rest of configuration
}
Creating fullchain.pem
If you have separate files:
# Concatenate server cert + intermediate cert(s)
cat example.com.crt intermediate.crt > fullchain.pem
# Or with multiple intermediates
cat example.com.crt intermediate1.crt intermediate2.crt > fullchain.pem
# Verify the chain
openssl verify -CAfile root.crt -untrusted intermediate.crt example.com.crt
Let’s Encrypt with Certbot
# Install certbot
sudo apt-get install certbot python3-certbot-nginx
# Obtain certificate
sudo certbot --nginx -d example.com -d www.example.com
# Certbot automatically configures Nginx and creates:
# /etc/letsencrypt/live/example.com/fullchain.pem
# /etc/letsencrypt/live/example.com/privkey.pem
Testing Nginx Configuration
# Test configuration syntax
sudo nginx -t
# Reload Nginx
sudo systemctl reload nginx
# Check SSL configuration
openssl s_client -connect example.com:443 -servername example.com
# Verify certificate chain
openssl s_client -connect example.com:443 -showcerts
Apache SSL Configuration
Apache is another widely-used web server with powerful SSL capabilities.
Basic SSL Configuration
Enable SSL module:
# Debian/Ubuntu
sudo a2enmod ssl
sudo a2enmod headers
sudo systemctl restart apache2
File structure:
/etc/ssl/certs/
├── example.com.crt # Server certificate
├── intermediate.crt # Intermediate CA bundle
└── ca-bundle.crt # Alternative name
/etc/ssl/private/
└── example.com.key # Private key
Virtual Host Configuration (/etc/apache2/sites-available/example.com-ssl.conf):
<VirtualHost *:443>
ServerName example.com
ServerAlias www.example.com
DocumentRoot /var/www/example.com
# SSL Engine
SSLEngine on
# Certificate files
SSLCertificateFile /etc/ssl/certs/example.com.crt
SSLCertificateKeyFile /etc/ssl/private/example.com.key
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
# For Apache 2.4.8+, use SSLCACertificateFile instead of SSLCertificateChainFile
# SSLCACertificateFile /etc/ssl/certs/intermediate.crt
# SSL Protocol and Cipher settings
SSLProtocol all -SSLv3 -TLSv1 -TLSv1.1
SSLCipherSuite 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
SSLHonorCipherOrder off
SSLCompression off
SSLSessionTickets off
# HSTS (optional but recommended)
Header always set Strict-Transport-Security "max-age=63072000"
# Security headers
Header always set X-Frame-Options "SAMEORIGIN"
Header always set X-Content-Type-Options "nosniff"
Header always set X-XSS-Protection "1; mode=block"
# Logging
ErrorLog ${APACHE_LOG_DIR}/example.com-ssl-error.log
CustomLog ${APACHE_LOG_DIR}/example.com-ssl-access.log combined
</VirtualHost>
# HTTP to HTTPS redirect
<VirtualHost *:80>
ServerName example.com
ServerAlias www.example.com
Redirect permanent / https://example.com/
</VirtualHost>
Apache with fullchain.pem
If you have a fullchain.pem file:
<VirtualHost *:443>
ServerName example.com
SSLEngine on
# Use fullchain for both certificate and chain
SSLCertificateFile /etc/ssl/certs/fullchain.pem
SSLCertificateKeyFile /etc/ssl/private/example.com.key
# SSLCertificateChainFile not needed with fullchain
# ... rest of configuration
</VirtualHost>
Enable Site and Reload
# Enable SSL site
sudo a2ensite example.com-ssl.conf
# Test configuration
sudo apache2ctl configtest
# Reload Apache
sudo systemctl reload apache2
# Check SSL
openssl s_client -connect example.com:443 -servername example.com
Windows IIS SSL Configuration
IIS (Internet Information Services) is the web server for Windows environments.
Prerequisites
- Certificate in .pfx format (includes private key)
- Administrative access to IIS
Method 1: IIS Manager GUI
Import Certificate:
- Open IIS Manager
- Click on server name in left panel
- Double-click “Server Certificates”
- Click “Import…” in right panel
- Browse to .pfx file
- Enter password
- Click OK
Bind Certificate to Site:
- Expand “Sites” in left panel
- Right-click your site → “Edit Bindings…”
- Click “Add…”
- Select “Type: https”
- Port: 443
- Select SSL certificate from dropdown
- Click OK
Configure HTTPS Redirect (optional):
- Select your site
- Double-click “URL Rewrite” (install if not present)
- Add rule to redirect HTTP to HTTPS
Method 2: PowerShell
# Import certificate
$certPassword = ConvertTo-SecureString -String "YourPassword" -Force -AsPlainText
Import-PfxCertificate -FilePath "C:\Certs\example.com.pfx" `
-CertStoreLocation Cert:\LocalMachine\My `
-Password $certPassword
# Get certificate thumbprint
$cert = Get-ChildItem -Path Cert:\LocalMachine\My | Where-Object {$_.Subject -like "*example.com*"}
$thumbprint = $cert.Thumbprint
# Bind certificate to site
New-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443
$binding = Get-WebBinding -Name "Default Web Site" -Protocol "https" -Port 443
$binding.AddSslCertificate($thumbprint, "my")
# Force HTTPS redirect (if URL Rewrite is installed)
Add-WebConfigurationProperty -pspath 'MACHINE/WEBROOT/APPHOST' `
-filter "system.webServer/rewrite/rules" `
-name "." `
-value @{name='HTTP to HTTPS'; stopProcessing='True'}
Converting Certificates for IIS
If you have PEM files, convert to PFX:
# Linux/Mac
openssl pkcs12 -export -out certificate.pfx \
-inkey private.key \
-in certificate.crt \
-certfile ca-bundle.crt
# Then transfer .pfx to Windows server
PowerShell (Windows with OpenSSL):
& "C:\Program Files\OpenSSL\bin\openssl.exe" pkcs12 -export `
-out certificate.pfx `
-inkey private.key `
-in certificate.crt `
-certfile ca-bundle.crt
IIS-Specific Considerations
- Certificate Store: IIS uses Windows Certificate Store
- Private Key Permissions: Ensure IIS App Pool identity has read access
- SNI (Server Name Indication): Enable for multiple SSL sites on same IP
- Intermediate Certificates: Automatically included from Windows Certificate Store
Verifying SSL Installation
Online Tools
Command Line Testing
# Basic SSL connection test
openssl s_client -connect example.com:443 -servername example.com
# Check certificate expiration
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates
# Verify certificate chain
openssl s_client -connect example.com:443 -showcerts
# Check specific protocol version
openssl s_client -connect example.com:443 -tls1_2
openssl s_client -connect example.com:443 -tls1_3
Using curl
# Test HTTPS connection
curl -I https://example.com
# Show certificate details
curl -vI https://example.com
# Test certificate validation
curl --insecure https://example.com # Should show certificate error if invalid
Common Issues and Solutions
Issue: “Certificate chain incomplete”
Symptoms:
- Browser warnings on some devices
- Mobile devices can’t connect
- SSL Labs shows “Chain issues”
Solution:
# Ensure intermediate certificates are included
# Nginx: Use fullchain.pem
cat example.com.crt intermediate.crt > fullchain.pem
# Apache: Specify SSLCertificateChainFile
SSLCertificateChainFile /etc/ssl/certs/intermediate.crt
Issue: “NET::ERR_CERT_COMMON_NAME_INVALID”
Symptoms:
- Certificate valid but browser shows warning
- Name in certificate doesn’t match domain
Solution:
- Verify certificate CN (Common Name) matches domain
- Check Subject Alternative Names (SAN) include all domains
- Ensure www and non-www variants are covered
Issue: “SSL handshake failed”
Symptoms:
- Cannot establish SSL connection
- Timeout errors
Solution:
# Check certificate and key match
openssl x509 -noout -modulus -in certificate.crt | openssl md5
openssl rsa -noout -modulus -in private.key | openssl md5
# MD5 hashes should match
# Verify private key is valid
openssl rsa -in private.key -check
# Check certificate is valid
openssl x509 -in certificate.crt -text -noout
Issue: “Mixed content warnings”
Symptoms:
- Page loads but shows “Not Secure”
- Some resources loaded over HTTP
Solution:
- Update all internal links to use HTTPS or relative URLs
- Configure Content Security Policy
- Use HSTS to enforce HTTPS
Best Practices
Use Strong Cipher Suites
- TLS 1.2 and 1.3 only
- Disable SSLv3, TLS 1.0, TLS 1.1
- Use modern cipher suites
Enable HSTS
Strict-Transport-Security: max-age=63072000; includeSubDomains; preloadInclude Complete Certificate Chain
- Always include intermediate certificates
- Test on multiple browsers/devices
Regular Certificate Monitoring
- Track expiration dates
- Set up renewal reminders
- Use tools like CrtMgr
Secure Private Keys
- Restrict file permissions (chmod 600)
- Never commit to version control
- Use strong passphrases
Automate Renewal
- Use Let’s Encrypt with automatic renewal
- Set up monitoring for expiring certificates
- Test renewal process
Test Thoroughly
- SSL Labs scan
- Multiple browsers
- Mobile devices
- Certificate chain verification
Automation and Monitoring
Certbot (Let’s Encrypt)
# Auto-renewal test
sudo certbot renew --dry-run
# Set up automatic renewal (systemd)
sudo systemctl enable certbot.timer
sudo systemctl start certbot.timer
Certificate Monitoring
Use CrtMgr to:
- Monitor certificate expiration
- Get renewal alerts
- Track multiple domains
- Download certificates in any format
- Public certificate viewing
Deployment Automation
# Example deployment script
#!/bin/bash
DOMAIN="example.com"
CERT_PATH="/etc/letsencrypt/live/$DOMAIN"
# Obtain/renew certificate
certbot certonly --webroot -w /var/www/$DOMAIN -d $DOMAIN -d www.$DOMAIN
# Copy to nginx location
cp $CERT_PATH/fullchain.pem /etc/nginx/ssl/$DOMAIN.crt
cp $CERT_PATH/privkey.pem /etc/nginx/ssl/$DOMAIN.key
# Reload nginx
nginx -t && systemctl reload nginx
Conclusion
Proper SSL/TLS certificate deployment requires understanding certificate chains, server-specific configuration, and security best practices. Key takeaways:
- Always include intermediate certificates for complete chain of trust
- Use fullchain.pem for Nginx (simplest approach)
- Configure strong cipher suites and disable old protocols
- Enable HSTS for enhanced security
- Monitor expiration dates and automate renewal
- Test thoroughly with SSL Labs and multiple devices
Need help managing and monitoring your certificates? Try CrtMgr for automated certificate tracking, expiration alerts, and easy deployment!