TLS and SSL Certificates Explained: A Developer's Guide to HTTPS, X.509, and Certificate Chains
Every developer has stared at NET::ERR_CERT_AUTHORITY_INVALID, x509: certificate has expired, or SSL_ERROR_BAD_CERT_DOMAIN at some point and silently hoped a redeploy would make it go away. Understanding what these errors actually mean — and how the certificate machinery works underneath — turns each of them from a mystery into a 30-second fix.
This guide walks through TLS and SSL from the ground up: what a certificate is, how the chain of trust works, what happens during the TLS handshake, and how to read certificates from the command line.
SSL vs TLS — A Note on Terminology
You'll hear "SSL certificate", "TLS certificate", and "SSL/TLS certificate" used interchangeably. They refer to the same thing: an X.509 certificate used by a Transport Layer Security handshake.
- SSL (Secure Sockets Layer) is the original protocol. SSL 1.0 (1995) was never released. SSL 2.0 and 3.0 are completely obsolete and insecure. Modern systems do not negotiate SSL.
- TLS (Transport Layer Security) is the IETF-standardised successor. TLS 1.0 and 1.1 are deprecated. TLS 1.2 (2008) is widely supported. TLS 1.3 (2018) is the current standard — faster handshake, better cryptography, fewer footguns.
Despite being deprecated for over a decade, "SSL certificate" remains the popular term simply because the marketing stuck. They are TLS certificates.
What Is an X.509 Certificate?
A certificate is a small file that says, in cryptographic terms: "I am the public key for <hostname>, and a certificate authority you trust has signed this claim."
Inside an X.509 v3 certificate:
| Field | Meaning |
|---|---|
| Subject | Who the certificate is for. Includes the Common Name (CN) and other identifying fields. |
| Subject Alternative Name (SAN) | The hostnames the certificate is valid for. This is what browsers actually check. |
| Issuer | The Certificate Authority (CA) that signed it. |
| Public Key | The server's public key (usually RSA 2048+ or ECDSA P-256/P-384). |
| Validity | Not Before and Not After timestamps. |
| Serial Number | Unique ID assigned by the CA. |
| Signature | The CA's cryptographic signature over everything above. |
| Extensions | Key usage, extended key usage, CRL/OCSP URLs, SCTs (Certificate Transparency). |
Common Name is dead. Modern browsers (Chrome since 2017, Firefox since 2019, Safari since iOS 13) ignore the CN field and only validate against the Subject Alternative Name. If your cert has the hostname in CN but no matching SAN entry, browsers will reject it with ERR_CERT_COMMON_NAME_INVALID. Always include all hostnames in the SAN.
Certificate Files and Formats
Same data, multiple file formats. This is a frequent source of confusion:
| Extension | Format | Contains |
|---|---|---|
.pem, .crt, .cer | PEM (Base64-encoded, text) | Cert(s), often a chain |
.der | DER (binary) | Single cert |
.key | PEM | Private key (keep secret!) |
.csr | PEM | Certificate Signing Request |
.p12, .pfx | PKCS#12 (binary) | Cert + private key, password-protected |
.jks | Java KeyStore | Java-specific bundle |
A PEM file is just Base64 between -----BEGIN CERTIFICATE----- and -----END CERTIFICATE----- markers. You can have multiple blocks concatenated to form a chain:
-----BEGIN CERTIFICATE-----
MIIDxTCCAq2gAwIBAgIQAqxN... (your server's cert)
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIEpDCCA4ygAwIBAgIQc6N5... (intermediate CA)
-----END CERTIFICATE-----
The Chain of Trust
Browsers don't trust your server's certificate directly. They trust a chain of certificates that ends at a root CA already in the operating system or browser trust store.
┌──────────────────────┐
│ Root CA │ ← Pre-installed in your OS/browser
│ (self-signed) │
└──────────┬───────────┘
│ signs
▼
┌──────────────────────┐
│ Intermediate CA │ ← Served by you, alongside leaf
│ │
└──────────┬───────────┘
│ signs
▼
┌──────────────────────┐
│ Your server's cert │ ← The "leaf" certificate
│ (boltkit.app) │
└──────────────────────┘
Roots are kept offline; intermediates do the day-to-day signing. The OS/browser trust store contains hundreds of root CAs operated by Let's Encrypt, DigiCert, Sectigo, GlobalSign, etc.
The Most Common Misconfiguration
The leading cause of "untrusted certificate" errors in production is a missing intermediate. Your server must serve the full chain (leaf + intermediates) to the client. Browsers usually fetch missing intermediates over the network — many other clients (Java, Go, curl on minimal containers, mobile apps) don't.
Test with:
openssl s_client -connect example.com:443 -servername example.com -showcerts
If Verify return code: 21 (unable to verify the first certificate) appears, your chain is incomplete.
The TLS 1.3 Handshake (Simplified)
When a client connects to https://example.com:
- ClientHello — client sends supported TLS versions, cipher suites, a random nonce, and the server name (SNI) it wants to talk to.
- ServerHello — server picks a cipher suite, returns its certificate chain, and a key share for ephemeral Diffie-Hellman key agreement.
- Validation — client checks: (1) chain leads to a trusted root, (2) leaf's SAN matches the hostname, (3) current time falls inside the validity window, (4) cert isn't revoked (OCSP/CRL/CT), (5) signatures are valid.
- Key derivation — both sides compute the shared secret. Application data starts flowing, encrypted.
TLS 1.3 cuts a full round trip compared to TLS 1.2 by sending the key share in the very first ClientHello.
Reading Certificates From the Command Line
# Inspect a remote server's certificate
openssl s_client -connect example.com:443 -servername example.com </dev/null \
| openssl x509 -text -noout
# Inspect a local PEM file
openssl x509 -in cert.pem -text -noout
# Just the SAN entries
openssl x509 -in cert.pem -noout -ext subjectAltName
# Just the expiry date
openssl x509 -in cert.pem -noout -dates
# Days until expiry
openssl x509 -in cert.pem -noout -checkend 0 # Returns 0 if valid
openssl x509 -in cert.pem -noout -checkend 2592000 # Valid for 30+ days?
# Verify chain locally
openssl verify -CAfile chain.pem cert.pem
# What's in a private key?
openssl rsa -in key.pem -text -noout
openssl ec -in key.pem -text -noout
# Match a key to its certificate (modulus must match)
openssl x509 -in cert.pem -noout -modulus | openssl md5
openssl rsa -in key.pem -noout -modulus | openssl md5
Common Certificate Errors and Fixes
NET::ERR_CERT_DATE_INVALID / x509: certificate has expired
The cert is past its Not After date, or the client clock is wrong. Renew the cert; if the client clock is wrong (common on embedded devices, Raspberry Pis without RTC), fix NTP first.
SSL_ERROR_BAD_CERT_DOMAIN / ERR_CERT_COMMON_NAME_INVALID
The hostname being connected to isn't in the SAN. Reissue with the correct SAN list. If you're behind a CDN, make sure the cert covers the apex and any subdomains in use.
NET::ERR_CERT_AUTHORITY_INVALID / unable to get local issuer certificate
Either the chain is incomplete (most common — see above) or you're using a self-signed cert that the client doesn't trust. For corporate/internal CAs, you'll need to install the root in the client's trust store.
SSL_ERROR_PROTOCOL_VERSION_ALERT
Client and server can't agree on a TLS version. Almost always means an old client trying TLS 1.0/1.1 against a modern server. Upgrade the client.
SEC_ERROR_REVOKED_CERTIFICATE
The CA published a revocation. Reissue the certificate; if the private key was compromised, generate a new key first.
Let's Encrypt and Automated Certificates
Let's Encrypt changed everything in 2016 by making CA-signed certificates free and automated. The trade-off: certs are valid for 90 days (much shorter than commercial 1-year+ certs), so you must automate renewal.
The most common tools:
- Certbot — the original ACME client; works well with Apache, Nginx, and standalone
- acme.sh — pure-shell client, no Python dependency, supports many DNS providers
- Caddy — web server with built-in automatic HTTPS; just point it at a domain
- Traefik — reverse proxy with built-in ACME
- cert-manager — Kubernetes operator that issues and renews automatically
Industry direction: shorter lifetimes. The CA/Browser Forum has approved a phased reduction of the maximum certificate lifetime to 47 days by 2029. If you're still managing certs by hand, you're already behind. Automate now.
Mutual TLS (mTLS)
Standard TLS authenticates the server to the client. Mutual TLS also authenticates the client to the server, by issuing each client a certificate (typically from a private CA) and validating it during the handshake.
mTLS is widely used for:
- Service-to-service auth in microservices (often via service mesh: Istio, Linkerd, Consul Connect)
- Device authentication (IoT, MDM-managed laptops)
- Bank/government APIs
- Zero-trust network architectures
Self-Signed Certs and Local Development
For localhost and local dev, real CAs won't sign you a cert. Options:
- mkcert — installs a local CA in your trust store and issues certs for any name. Best DX for local HTTPS.
- Caddy local mode — same idea, built into Caddy.
- Manual self-sign with OpenSSL — works but you'll have to bypass browser warnings.
- Real cert with DNS-01 — Let's Encrypt can issue for internal hostnames if you control the DNS zone.
Auditing Your Fleet's TLS Posture
Knowing how certificates work is one thing; actually verifying that every internal service, dev VM, and homelab box is up-to-date is another. Common findings on infrastructure that hasn't been audited recently:
- Servers still negotiating TLS 1.0 or 1.1 alongside TLS 1.3
- Self-signed certs on internal admin panels (Proxmox, NAS UIs, Grafana) that have silently expired
- Reverse proxies serving an incomplete chain
- Internal CAs whose root certificates expire next year and nobody noticed
- Open ports on a VPS that aren't serving the cert you expected
If you run a homelab or a small VPS fleet and want this audited continuously rather than by remembering to do it, Noxen — a Mac-native security audit tool — runs nightly checks for deprecated TLS, weak SSH, exposed services, and CVEs across every host you own. No agents, no Docker, no SaaS — it just SSHes and reports.
Quick Reference
- Validity sources of truth:
Not Before,Not After, SAN entries, signature chain. - Use TLS 1.3 if you can; TLS 1.2 as a fallback. Disable TLS 1.0 and 1.1 on every server you control.
- Always serve the full chain, not just the leaf.
- SAN is what matters, not Common Name.
- Automate renewal — certs only get shorter.
- Keep private keys offline — never commit them, never email them, set
0600permissions.
Inspect Tokens, Headers, and More On Your Phone
BoltKit's JWTInspect decodes auth tokens, HTTPCodes documents every status, DNSLookup resolves records over HTTPS. 10 essential dev tools, free on iPhone, iPad, and Mac.
Get BoltKit Free