SSL/TLS certificates are the backbone of internet security, yet they remain one of the most common sources of production incidents. Whether you're dealing with expired certs at 3 AM, debugging chain verification failures, or automating certificate deployment — this guide covers everything a DevOps engineer needs to know.
Every HTTPS connection starts with a TLS handshake. Understanding this process is key to debugging SSL issues.
Client Server
| |
|--- ClientHello (supported ciphers) ->|
| |
|<- ServerHello + Certificate + Verify-|
| |
|--- Finished (encrypted) ------------>|
| |
|<========= Application Data =========>|
- ClientHello: The client sends supported TLS versions, cipher suites, and a random value
- ServerHello: The server picks a cipher suite and sends its certificate chain
- Certificate Verification: The client validates the certificate chain up to a trusted root CA
- Key Exchange: Both parties derive session keys using Diffie-Hellman
- Encrypted Communication: All subsequent data is encrypted with the session key
TLS 1.3 completes the handshake in 1 round trip (vs 2 in TLS 1.2), reducing latency by ~100ms.
| Type | Validation | Use Case | Cost |
|---|
| DV (Domain Validation) | Domain ownership only | Most websites, APIs | Free (Let's Encrypt) |
| OV (Organization Validation) | Domain + company identity | Business websites | $50-200/yr |
| EV (Extended Validation) | Extensive legal verification | Financial institutions | $200-500/yr |
For most DevOps use cases, DV certificates are sufficient. Let's Encrypt provides them for free with automated renewal.
- Single-domain: Covers exactly one FQDN (e.g.,
rawops.dev)
- Wildcard: Covers all first-level subdomains (
*.rawops.dev)
- Multi-domain (SAN): Covers a list of specific domains via Subject Alternative Names
- Self-signed: Not trusted by browsers, used for internal services and development
This is where most confusion happens. The same certificate data can be stored in different formats:
The most common format on Linux. Base64-encoded, ASCII text.
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ
RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD
...
-----END CERTIFICATE-----
- File extensions:
.pem, .crt, .cer, .key
- Can contain certificates, private keys, or both
- Can be concatenated (certificate chains)
- Used by: Nginx, Apache, most Linux tools
Binary format. Not human-readable.
- File extensions:
.der, .cer
- Single certificate only (no chaining)
- Used by: Java, Windows, some hardware appliances
Binary archive format. Contains certificate + private key + optionally intermediate certificates. Password-protected.
- File extensions:
.pfx, .p12
- Used by: Windows/IIS, code signing, S/MIME email, client certificates
- Import/export with OpenSSL or our PFX Decoder tool
openssl x509 -in cert.pem -outform der -out cert.der
openssl x509 -in cert.der -inform der -outform pem -out cert.pem
openssl pkcs12 -export -out cert.pfx -inkey key.pem -in cert.pem -certfile chain.pem
openssl pkcs12 -in cert.pfx -out all.pem -nodes
Tip: You can use the SSL Certificate Decoder tool to decode PFX files and inspect certificates without remembering OpenSSL flags.
SSL certificates form a chain of trust from your end-entity (server) certificate to a trusted root CA:
Root CA (self-signed, in OS/browser trust store)
└── Intermediate CA (signed by Root)
└── End-Entity Certificate (your server cert, signed by Intermediate)
Root CAs don't directly sign server certificates. They delegate to intermediates for security — if an intermediate is compromised, the root can revoke it without affecting all certificates.
Common mistake: Forgetting to include intermediate certificates in your server configuration. The server must send the full chain (end-entity + intermediates). The root CA is already in the client's trust store.
openssl verify -CAfile ca-bundle.crt server.crt
openssl s_client -connect rawops.dev:443 -showcerts
openssl verify -CAfile root.pem -untrusted intermediate.pem server.pem
Tip: Use the Chain Builder tool to automatically fetch and build the full certificate chain from a single end-entity certificate.
Here's a production-ready Nginx SSL configuration:
server {
listen 443 ssl http2;
server_name rawops.dev;
# Certificate and key
ssl_certificate /etc/nginx/ssl/fullchain.pem;
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
# TLS 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;
ssl_prefer_server_ciphers off;
# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/nginx/ssl/chain.pem;
# Security headers
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
add_header X-Content-Type-Options nosniff always;
add_header X-Frame-Options DENY always;
# ... your server configuration
}
# HTTP to HTTPS redirect
server {
listen 80;
server_name rawops.dev;
return 301 https://$server_name$request_uri;
}
Key points:
ssl_certificate should be the full chain (end-entity + intermediates), not just the server cert
- Disable TLS 1.0 and 1.1 — they have known vulnerabilities
- Enable OCSP stapling for faster TLS handshakes
- Set HSTS headers to prevent downgrade attacks
Tip: Use the Nginx Config Generator to build this config interactively.
Let's Encrypt provides free DV certificates with 90-day validity. The short lifetime encourages automation.
apt install certbot python3-certbot-nginx
certbot --nginx -d rawops.dev -d www.rawops.dev
certbot certonly --standalone -d rawops.dev
certbot renew --dry-run
/etc/letsencrypt/live/rawops.dev/
├── cert.pem # End-entity certificate only
├── chain.pem # Intermediate certificates
├── fullchain.pem # cert.pem + chain.pem (use this for Nginx ssl_certificate)
└── privkey.pem # Private key (use this for ssl_certificate_key)
Certbot installs a systemd timer by default:
systemctl status certbot.timer
certbot renew --dry-run
Always use fullchain.pem (not cert.pem) for your web server. Missing intermediates is the #1 cause of "untrusted certificate" errors on mobile devices.
- Always automate renewal — 90-day certs expire fast. Use certbot timers, Caddy (auto-TLS), or ACME clients
- Monitor certificate expiry — Set up alerts at 30, 14, and 7 days before expiration
- Use the full chain — Include intermediates in your server config. Test with
openssl s_client
- Keep private keys secure — File permissions should be
600 or 640, owned by root
- Pin nothing in production — Certificate pinning (HPKP) is deprecated and dangerous
- Test with multiple clients — A cert that works in Chrome might fail on Android or curl
- Use TLS 1.2+ only — Disable TLS 1.0/1.1 and SSLv3
- Enable HSTS — Prevents protocol downgrade attacks
- Rotate certificates before they expire — Not after
- Document your certificate inventory — Track which certs cover which domains, their CAs, and renewal dates
When things go wrong, these commands are your first line of defense:
openssl x509 -in cert.pem -noout -text
openssl x509 -in cert.pem -noout -enddate
openssl s_client -connect rawops.dev:443 -servername rawops.dev
curl -vI https://rawops.dev 2>&1 | grep -A6 "Server certificate"
openssl x509 -noout -modulus -in cert.pem | md5sum
openssl rsa -noout -modulus -in key.pem | md5sum
For a more visual approach, paste your certificate into the Certificate Viewer to inspect all fields, SANs, key usage, and validity.
SSL/TLS certificates are not complicated once you understand the fundamentals: the handshake establishes trust, the chain links your cert to a root CA, and automation keeps everything running. The most common issues — expired certs, missing intermediates, wrong file formats — are all preventable with good tooling and monitoring.
For hands-on practice: