Overview
SSL/TLS certificates are the foundation of encrypted communications โ HTTPS web traffic, VPN authentication, API security, and even network device management all depend on valid, correctly deployed certificates. Certificate mismanagement is one of the most common causes of outages: expired certs, mismatched CNs, incomplete chains, and forgotten renewals regularly take down production services.
This guide covers the full certificate lifecycle from requesting to deployment to automated renewal, plus systematic troubleshooting for the most common failure modes.
Certificate Lifecycle
Certificate Types
Domain Validation (DV)
- Validates only domain ownership (DNS or HTTP challenge)
- Issued in minutes โ suitable for internal tools, APIs, and non-public-facing services
- Let's Encrypt issues DV certs free of charge via ACME protocol
Organization Validation (OV)
- Validates domain ownership + legal organization identity
- Issued in 1โ3 business days โ recommended for corporate websites and customer portals
- Browser padlock shows organization name in certificate details
Extended Validation (EV)
- Full legal and organizational vetting
- Highest trust level โ used by financial institutions, e-commerce
- Note: modern browsers no longer show the green bar but EV still provides stronger identity assurance
Wildcard and SAN Certificates
- Wildcard (
*.example.com) covers all first-level subdomains โ cannot cover sub-subdomains - SAN (Subject Alternative Name) covers multiple specific domains in one certificate โ preferred for multi-service deployments
Generating a CSR
OpenSSL โ Standard CSR
# Generate private key (2048-bit RSA minimum; 4096-bit recommended for long-lived certs)openssl genrsa -out example.com.key 4096# Generate CSR interactivelyopenssl req -new -key example.com.key -out example.com.csr# Or generate CSR non-interactively with a config fileopenssl req -new -key example.com.key \ -subj "/C=PH/ST=Cebu/L=Cebu City/O=Example Corp/CN=example.com" \ -out example.com.csr# Verify CSR contents before submittingopenssl req -text -noout -verify -in example.com.csrOpenSSL โ CSR with SAN (Subject Alternative Names)
# Create OpenSSL config with SAN extensioncat > san.cnf << EOF[req]default_bits = 4096prompt = nodistinguished_name = dnreq_extensions = req_ext[dn]C = PHST = CebuL = Cebu CityO = Example CorpCN = example.com[req_ext]subjectAltName = @alt_names[alt_names]DNS.1 = example.comDNS.2 = www.example.comDNS.3 = api.example.comDNS.4 = portal.example.comEOFopenssl req -new -key example.com.key -config san.cnf -out example.com-san.csropenssl req -text -noout -in example.com-san.csr | grep -A5 "Subject Alternative"Deployment by Platform
Apache HTTPD
# Install cert, key, and CA chainSSLCertificateFile /etc/ssl/certs/example.com.crtSSLCertificateKeyFile /etc/ssl/private/example.com.keySSLCertificateChainFile /etc/ssl/certs/ca-bundle.crt# Test configuration before reloadapachectl configtest# Reload without dropping connectionssystemctl reload apache2# Verify certificate served by Apacheopenssl s_client -connect example.com:443 -servername example.com < /dev/null 2>/dev/null \ | openssl x509 -noout -dates -subject -issuerNginx
# Nginx expects cert + chain concatenated into one filecat example.com.crt ca-bundle.crt > example.com-fullchain.crt# Nginx configssl_certificate /etc/nginx/ssl/example.com-fullchain.crt;ssl_certificate_key /etc/nginx/ssl/example.com.key;# Recommended TLS hardeningssl_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;ssl_prefer_server_ciphers off;ssl_session_timeout 1d;ssl_session_cache shared:SSL:10m;# Test and reloadnginx -t && systemctl reload nginxWindows IIS
# Import PFX (cert + key bundled) into Windows certificate storeImport-PfxCertificate -FilePath "example.com.pfx" ` -CertStoreLocation Cert:\LocalMachine\My ` -Password (ConvertTo-SecureString -String "pfxpassword" -Force -AsPlainText)# Bind certificate to IIS site (replace thumbprint with actual)$cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -match "example.com"}New-WebBinding -Name "Default Web Site" -Protocol https -Port 443 -HostHeader "example.com"(Get-WebBinding -Name "Default Web Site" -Protocol https).AddSslCertificate($cert.Thumbprint, "My")Cisco ASA
! Step 1 โ Import the certificate chaincrypto ca trustpoint EXAMPLE-CA enrollment terminal fqdn example.com subject-name CN=example.com,OU=IT,O=Example Corp,C=PH keypair EXAMPLE-KEY crl configure! Step 2 โ Import CA certificatecrypto ca authenticate EXAMPLE-CA! Paste PEM-encoded CA cert, end with "quit"! Step 3 โ Import signed certificatecrypto ca import EXAMPLE-CA certificate! Paste PEM-encoded signed cert, end with "quit"! Step 4 โ Bind to SSL VPN or ASDMssl trust-point EXAMPLE-CA outsidehttp server enablehttp redirect outside 80 443Cisco IOS / IOS-XE (HTTPS Management)
! Generate RSA key if not already presentcrypto key generate rsa modulus 4096 label MGMT-KEY! Import certificate via SCEP or manual pastecrypto pki trustpoint MGMT-TP enrollment terminal subject-name CN=router.example.com,O=Example Corp revocation-check none rsakeypair MGMT-KEYcrypto pki authenticate MGMT-TP! Paste CA cert PEMcrypto pki enroll MGMT-TP! Copy CSR, get it signed, then importcrypto pki import MGMT-TP certificate! Paste signed cert PEM! Enable HTTPS with trustpointip http secure-serverip http secure-trustpoint MGMT-TPAutomated Renewal with Let's Encrypt / Certbot
Install and Issue (Standalone Mode)
# Install certbotapt install certbot python3-certbot-nginx # Debian/Ubuntuyum install certbot python3-certbot-nginx # RHEL/CentOS# Issue certificate (standalone โ temporarily binds port 80)certbot certonly --standalone -d example.com -d www.example.com# Or issue and auto-configure Nginxcertbot --nginx -d example.com -d www.example.com# Certificates are stored at:# /etc/letsencrypt/live/example.com/fullchain.pem (cert + chain)# /etc/letsencrypt/live/example.com/privkey.pem (private key)Automated Renewal
# Test renewal without actually renewingcertbot renew --dry-run# Certbot auto-installs a systemd timer (check it)systemctl status certbot.timersystemctl list-timers | grep certbot# Manual renewal with nginx reload hookcertbot renew --deploy-hook "systemctl reload nginx"# Or add to crontab (runs twice daily โ certbot only renews if <30 days remain)echo "0 0,12 * * * root certbot renew --quiet --deploy-hook 'systemctl reload nginx'" \ >> /etc/crontabTroubleshooting Common Certificate Errors
ERR_CERT_COMMON_NAME_INVALID / SSL_ERROR_BAD_CERT_DOMAIN
The CN or SAN does not match the hostname being accessed.
# Check what names the cert is valid foropenssl s_client -connect example.com:443 < /dev/null 2>/dev/null \ | openssl x509 -noout -text | grep -A1 "Subject Alternative Name"# Also check CNopenssl s_client -connect example.com:443 < /dev/null 2>/dev/null \ | openssl x509 -noout -subjectFix: Reissue with correct CN/SAN. For an apex domain, always include both example.com and www.example.com as SANs.
UNABLE_TO_VERIFY_LEAF_SIGNATURE / Incomplete Chain
The intermediate CA certificate is missing from the chain.
# Check chain completenessopenssl s_client -connect example.com:443 -showcerts < /dev/null 2>/dev/null \ | grep "subject\|issuer"# The last cert's issuer should be a well-known Root CA# If subject == issuer on an intermediate, the chain is broken# Fix: download and concatenate the intermediate certcat server.crt intermediate.crt > fullchain.crtCertificate Expired
# Check expiry dateopenssl s_client -connect example.com:443 < /dev/null 2>/dev/null \ | openssl x509 -noout -enddate# Check a local cert fileopenssl x509 -noout -dates -in example.com.crt# Check expiry in days (useful in scripts/monitoring)python3 -c "import ssl, socket, datetimectx = ssl.create_default_context()with ctx.wrap_socket(socket.socket(), server_hostname='example.com') as s: s.connect(('example.com', 443)) cert = s.getpeercert() exp = datetime.datetime.strptime(cert['notAfter'], '%b %d %H:%M:%S %Y %Z') print(f'Expires: {exp} ({(exp - datetime.datetime.utcnow()).days} days)')"Private Key Mismatch
The certificate and private key are not a matched pair โ common after reissuing a cert without updating the key.
# Compare modulus of cert and key โ they must match exactlyopenssl x509 -noout -modulus -in example.com.crt | md5sumopenssl rsa -noout -modulus -in example.com.key | md5sum# Both lines must produce the same MD5 hashTLS Handshake Failures / Protocol Mismatch
# Test specific TLS versionopenssl s_client -connect example.com:443 -tls1_2 < /dev/nullopenssl s_client -connect example.com:443 -tls1_3 < /dev/null# Check what ciphers the server acceptsnmap --script ssl-enum-ciphers -p 443 example.com# Test from a specific cipheropenssl s_client -connect example.com:443 \ -cipher ECDHE-RSA-AES256-GCM-SHA384 < /dev/nullCertificate Monitoring
Simple Bash Expiry Monitor
#!/bin/bash# cert-check.sh โ Check expiry for a list of domains# Usage: ./cert-check.sh domains.txtWARN_DAYS=30while IFS= read -r domain; do expiry=$(echo | openssl s_client -connect "${domain}:443" \ -servername "$domain" 2>/dev/null \ | openssl x509 -noout -enddate 2>/dev/null \ | cut -d= -f2) if [ -z "$expiry" ]; then echo "ERROR $domain โ could not retrieve cert" continue fi exp_epoch=$(date -d "$expiry" +%s 2>/dev/null || date -jf "%b %d %H:%M:%S %Y %Z" "$expiry" +%s) now_epoch=$(date +%s) days_left=$(( (exp_epoch - now_epoch) / 86400 )) if [ "$days_left" -lt "$WARN_DAYS" ]; then echo "WARN $domain โ expires in ${days_left} days ($expiry)" else echo "OK $domain โ expires in ${days_left} days" fidone < "$1"SSL/TLS Hardening Checklist
- Use TLS 1.2 minimum โ disable TLS 1.0 and 1.1 on all servers
- Use 2048-bit RSA minimum; prefer 4096-bit for certificates with long validity periods
- Always include the full certificate chain (cert + intermediates) in deployment
- SAN must include all hostnames that will access the service โ wildcard is
*.example.comnotexample.com - Private key file permissions:
chmod 600โ readable only by the web server process - Never reuse private keys across certificate renewals โ generate a new key each time
- Set up automated expiry monitoring โ alert at 30 days, escalate at 14 days
- Use HSTS (
Strict-Transport-Security) to prevent protocol downgrade attacks - Enable OCSP Stapling on web servers to improve TLS handshake performance
- For internal PKI: ensure CRL Distribution Points and OCSP URLs are reachable from all clients