In 30 seconds
HTTPS is HTTP sent inside TLS, an encrypted and authenticated tunnel. TLS makes sure nobody can read or change the traffic, and the certificate proves you are really talking to example.com. With Let's Encrypt, certificates are free and renew automatically, so every site should be HTTPS-only.
Why I use it
- I run HTTPS everywhere with auto-renewing Let's Encrypt certificates; it is part of my security baseline for every server.
- Logins, payments and personal data in my products must never travel in plain text.
- Browsers mark HTTP pages as not secure and many modern features (service workers, geolocation, HTTP/2 in browsers) require HTTPS.
- Certificate problems take sites down instantly, so understanding them saves stressful hours.
How it works
01What TLS gives you
TLS (Transport Layer Security, the successor of SSL) provides three things. Confidentiality: traffic is encrypted, so someone on the same Wi-Fi sees only gibberish. Integrity: any modification in transit is detected. Authentication: the server proves its identity with a certificate signed by a trusted authority. Analogy: a sealed, tamper-evident envelope delivered by a courier who checked the recipient's passport. TLS 1.3 (RFC 8446) is the current version and TLS 1.2 is still acceptable; SSL 3.0, TLS 1.0 and TLS 1.1 are deprecated and should be disabled. Note that HTTPS hides paths, headers and bodies, but the domain name is still visible via DNS and the SNI field unless Encrypted Client Hello is used.
02The TLS 1.3 handshake
The handshake takes one round trip. The browser sends ClientHello: supported cipher suites, a random value, the hostname (SNI), supported protocols (ALPN, for example h2) and a key share, its half of an ephemeral Diffie-Hellman exchange (usually X25519, and in current browsers often a hybrid with the post-quantum ML-KEM). The server replies with ServerHello and its key share; both sides now compute the same secret without ever sending it. Everything after that is encrypted: the server sends its certificate chain, a CertificateVerify signature proving it owns the private key, and Finished. The browser checks the certificate and sends its own Finished, along with the first HTTP request. Because keys are ephemeral, a stolen server key cannot decrypt past traffic (forward secrecy). Resumed sessions can even send data at once (0-RTT), with replay risks.
# See the negotiated version, cipher and certificate chain
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null
# Only accept TLS 1.3 to test support
openssl s_client -connect example.com:443 -servername example.com -tls1_3 </dev/null
03Certificates and the chain of trust
A certificate binds a public key to one or more hostnames (in the Subject Alternative Name field) for a limited period, and is signed by a Certificate Authority (CA). Browsers and operating systems ship a list of trusted root CAs. Roots rarely sign site certificates directly; they sign intermediate CAs, which sign your leaf certificate. So your server must send the leaf plus intermediates (the full chain); the browser builds the path up to a root it trusts. Validation types: DV (domain validated, automatic, what almost everyone uses), OV and EV (organization checks, no visible benefit in browsers today). Wildcards (*.example.com) cover one level of subdomains. All public certificates are logged in Certificate Transparency logs, so you can see every certificate issued for your domain. Lifetimes are shrinking: the industry maximum falls to 200 days in 2026 and reaches 47 days in 2029, so automation is mandatory.
04Let's Encrypt with certbot
Let's Encrypt is a free, automated CA using the ACME protocol. certbot proves you control the domain, usually with the HTTP-01 challenge (it serves a token at /.well-known/acme-challenge/ on port 80) or DNS-01 (a TXT record, required for wildcards). Install it, request a certificate with the nginx plugin, and it edits the nginx config for you. Packages install a systemd timer that tries renewal twice a day and renews when the certificate is close to expiry. Always test renewal with --dry-run and add a deploy hook so nginx reloads with the new certificate. Let's Encrypt certificates are 90 days by default today, with shorter 45-day options already available and the default moving to 64 days in 2027 and 45 days in 2028, so never rely on manual renewal.
sudo apt update && sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d example.com -d www.example.com
# Renewal is automatic; verify the timer and do a test run
systemctl list-timers | grep certbot
sudo certbot renew --dry-run
# Reload nginx after each successful renewal
sudo certbot renew --deploy-hook "systemctl reload nginx" --dry-run
sudo certbot certificates
05Good nginx TLS config and HSTS
Redirect all HTTP to HTTPS, allow only TLS 1.2 and 1.3, and let modern defaults choose ciphers. Mozilla's SSL Configuration Generator gives up-to-date settings for each server. HSTS (Strict-Transport-Security) tells browsers to use only HTTPS for your domain for max-age seconds, even if someone types http:// or clicks an old link; this blocks SSL-stripping attacks. Roll it out carefully: start with a short max-age, add includeSubDomains only when every subdomain has HTTPS, and only submit to the browser preload list (with the preload flag) when you are sure, because removal takes months. If you are behind Cloudflare with Full (strict), the same certificate on the origin covers the second hop.
server {
listen 80;
server_name example.com www.example.com;
return 301 https://example.com$request_uri;
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_session_cache shared:SSL:10m;
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
}
06Common SSL errors and how to fix them
NET::ERR_CERT_DATE_INVALID or "certificate has expired": renewal failed; check certbot logs, the timer and that port 80 is reachable for HTTP-01. ERR_CERT_COMMON_NAME_INVALID: the certificate does not cover this hostname, often www missing; reissue with all names. "unable to get local issuer certificate" in curl, Android or Node clients while browsers work: the server sends only the leaf, not the chain; use fullchain.pem. ERR_SSL_VERSION_OR_CIPHER_MISMATCH: old protocols only, or no certificate for this SNI name. Mixed content: an HTTPS page loading http:// scripts or images; fix URLs or add upgrade-insecure-requests in CSP. HSTS errors you cannot click through: the certificate is invalid on a domain with HSTS; fix the certificate. Behind Cloudflare, 525 and 526 are the origin-side versions of these problems.
# Expiry dates and names covered
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null \
| openssl x509 -noout -dates -subject -ext subjectAltName
sudo journalctl -u certbot --since "7 days ago"
sudo nginx -t && sudo systemctl reload nginx
Use it when
- Every public website, API and admin panel, without exception.
- Internal services that carry credentials or personal data, even on private networks.
- Between a CDN and your origin (Full (strict) mode).
Skip it when
- Do not terminate TLS in application code when nginx or a load balancer can do it more simply.
- Do not use self-signed certificates for public sites; use Let's Encrypt or an origin certificate behind a CDN.
Alternatives
- Cloudflare Origin CA
Free long-lived certificates trusted only by Cloudflare, for the edge-to-origin hop.
- Caddy
Web server that obtains and renews certificates automatically with no extra tools.
- acme.sh / lego
Lightweight ACME clients, handy for DNS-01 and unusual setups.
- Managed certificates
AWS ACM, load balancers and PaaS platforms handle issuance and renewal for you.
Best practices
- Automate issuance and renewal, test with --dry-run, and monitor expiry dates.
- Serve the full chain (fullchain.pem), not only the leaf certificate.
- Enable only TLS 1.2 and 1.3; follow Mozilla's intermediate profile.
- Redirect HTTP to HTTPS with 301 and add HSTS after verifying all subdomains.
- Protect private keys with strict file permissions and never commit them.
- Add a CAA record listing the CAs you actually use.
Common mistakes
- Renewing manually and forgetting, so the site goes down on expiry day.
- Blocking port 80 entirely, which breaks HTTP-01 renewal.
- Missing intermediate certificates, so some clients fail while desktop browsers work.
- Enabling HSTS preload before every subdomain supports HTTPS.
- Leaving mixed-content http:// URLs in templates and the database.
Deep dive
07Revocation, and why lifetimes are shrinking
If a private key leaks, the certificate should stop being trusted before it expires. The two classic tools both have problems. CRLs are lists of revoked serial numbers that can grow large. OCSP lets a client ask the CA about one certificate, but it leaks which sites a user visits to the CA, adds latency, and browsers soft-fail when the responder is unreachable, so an attacker who blocks it wins. OCSP stapling, where the server attaches a signed status, fixed some of that. Let's Encrypt shut down its OCSP service in 2025 and now publishes only CRLs, and browsers push compact revocation sets built from CRLs to clients. The industry's main answer, however, is short lifetimes: a stolen key for a 47-day certificate is useful for weeks, not a year. That is the real reason behind the shrinking maximum validity.
08Key types, cipher suites and post-quantum key exchange
Your certificate key is either RSA (2048 bits is the common minimum) or ECDSA (P-256). ECDSA keys are smaller and signatures are faster, which shortens handshakes; RSA has the widest support among very old clients. nginx can serve both at once and pick per client. In TLS 1.3 there are only a few cipher suites, all AEAD (AES-GCM, ChaCha20-Poly1305), and they are fine by default. For TLS 1.2, follow Mozilla's intermediate profile, which keeps only ECDHE key exchange with AEAD ciphers. Key exchange is moving to post-quantum hybrids: X25519MLKEM768 combines classic X25519 with ML-KEM, so recorded traffic stays safe even against future quantum computers. Current browsers offer it; on nginx it needs OpenSSL 3.5 or newer, and naming the group on an older OpenSSL makes nginx fail to start.
# Dual certificates: modern clients get ECDSA, older ones RSA
ssl_certificate /etc/letsencrypt/live/example.com-ecdsa/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com-ecdsa/privkey.pem;
ssl_certificate /etc/letsencrypt/live/example.com-rsa/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com-rsa/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
# Only with OpenSSL 3.5+:
# ssl_ecdh_curve X25519MLKEM768:X25519:prime256v1;
09ACME in detail: challenges, ARI and rate limits
An ACME client creates an account key, places an order for a set of names, completes one challenge per name, sends a certificate signing request and downloads the certificate. HTTP-01 needs port 80 reachable on every server that answers for the name, so behind a load balancer the token must be served by all nodes or validation fails randomly. DNS-01 works for wildcards and private servers, but the API token that edits DNS is powerful, so scope it to one zone or delegate _acme-challenge to a separate zone. TLS-ALPN-01 validates on port 443. ACME Renewal Information (ARI, RFC 9773) lets the CA tell clients when to renew, for example earlier during a mass revocation; certbot supports it from version 4.1. Let's Encrypt also enforces rate limits, such as a weekly cap on certificates per registered domain and on duplicates, so test against the staging environment.
# Practise against staging first (no rate-limit pain)
sudo certbot certonly --nginx --staging -d example.com -d www.example.com
# Wildcard via DNS-01 with a DNS plugin and a zone-scoped token
sudo certbot certonly --dns-cloudflare \
--dns-cloudflare-credentials /root/.secrets/cloudflare.ini \
-d example.com -d "*.example.com"
# ECDSA key (the default for new certbot certificates)
sudo certbot certonly --nginx --key-type ecdsa -d example.com
10Beyond the public web: mTLS, internal PKI and ECH
TLS can authenticate both sides. In mutual TLS (mTLS) the client also presents a certificate, which the server checks against a CA it trusts. It is common between services, for partner APIs and for CDN-to-origin connections. Service meshes issue and rotate these certificates automatically. For internal services you can run a private CA (for example with step-ca or a cloud private CA), but then every client must trust that root and you own rotation and revocation; for public names, a public CA via ACME is simpler. Encrypted Client Hello (ECH) hides the SNI hostname from network observers by encrypting the ClientHello with a key published in the HTTPS DNS record. Major browsers and CDNs support it, so for proxied sites it often works without any change on your own server.
# nginx: require a client certificate from your internal CA
ssl_client_certificate /etc/ssl/internal/ca.pem;
ssl_verify_client on;
ssl_verify_depth 2;
# Test with a client certificate
# curl --cert client.pem --key client.key https://api.internal.example.com/health
Real-world scenarios
Certificates expired over the weekend
Problem
On Monday the site shows NET::ERR_CERT_DATE_INVALID. certbot was installed months ago and renewals had worked before.
How to solve it
Run sudo certbot renew --dry-run and read the error. Common causes: port 80 was closed in a firewall change, an HTTP-to-HTTPS redirect or auth rule blocks /.well-known/acme-challenge/, DNS now points to a CDN or a new server, or the timer is disabled. Check systemctl list-timers and journalctl -u certbot. Fix the cause, run certbot renew, reload nginx and confirm the new dates with openssl x509 -noout -dates. Then add external expiry monitoring that alerts at 14 days and a deploy hook that reloads nginx after each renewal.
A private key was committed to a repository
Problem
A developer pushed privkey.pem to a shared Git repository by mistake. The repository is private, but several contractors have access.
How to solve it
Treat the key as compromised. Issue a certificate with a fresh key immediately, for example sudo certbot renew --cert-name example.com --force-renewal --new-key, and reload nginx. Revoke the old certificate with certbot revoke --cert-path pointing to it, giving keyCompromise as the reason. Remove the file from the repository history with a history-rewriting tool and rotate any other secrets in the same commit. Add the certificate directory to .gitignore and a secret-scanning hook so it cannot happen again.
An Android app cannot connect, browsers can
Problem
After moving to a new server, the website works in every browser, but the mobile app and a partner's Java client fail with certificate path errors.
How to solve it
The new nginx config most likely points ssl_certificate to cert.pem instead of fullchain.pem, so the intermediate is missing. Browsers repair this with cached intermediates or AIA fetching; many apps and Java clients do not. Run openssl s_client -showcerts and confirm two certificates appear. Change to fullchain.pem, test with nginx -t, reload, and run an SSL checker. If the app uses certificate pinning, check whether it pins a key that changed, and plan pin updates with backup pins before rotation.
Cheat sheet
- Negotiated protocol and cipher
openssl s_client -connect example.com:443 -servername example.com -brief </dev/null- Expiry date and names
echo | openssl s_client -connect example.com:443 -servername example.com 2>/dev/null | openssl x509 -noout -dates -ext subjectAltName- Show the full chain sent
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null- Inspect a local certificate
openssl x509 -in /etc/letsencrypt/live/example.com/fullchain.pem -noout -text | less- List certbot certificates
sudo certbot certificates- Test renewal
sudo certbot renew --dry-run- Add a name to an existing certificate
sudo certbot --nginx --expand -d example.com -d www.example.com -d shop.example.com- HSTS header (after testing)
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;- Check a key matches a certificate
openssl x509 -noout -pubkey -in cert.pem | sha256sum; openssl pkey -pubout -in privkey.pem | sha256sum- Search Certificate Transparency logs
https://crt.sh/?q=example.com
In my real projects
- I use Let's Encrypt certificates issued by certbot on the servers I manage, with HTTPS everywhere and auto-renewal as part of my baseline.
- My firewall baseline keeps ports 80 and 443 open, so HTTP-01 challenges and HTTP-to-HTTPS redirects work.
- The POS SaaS server uses a Docker Compose TLS override with nginx as the reverse proxy in front of the services.
Questions & answers
Q1How does the TLS 1.3 handshake work?
The client sends ClientHello with supported ciphers, the hostname in SNI, ALPN protocols and a key share for an ephemeral Diffie-Hellman exchange. The server answers with ServerHello and its own key share, and both derive the same session keys. From then on everything is encrypted: the server sends its certificate chain, a signature proving it holds the private key, and Finished. The client validates the chain and hostname, sends Finished and can immediately send the HTTP request. That is one round trip, compared with two in TLS 1.2. Ephemeral keys give forward secrecy, so a later key leak cannot decrypt recorded traffic.
Q2What is the chain of trust?
Browsers and operating systems trust a small set of root CA certificates. Roots sign intermediate CAs, and intermediates sign site certificates. When a server presents its leaf certificate plus the intermediates, the client verifies each signature up the chain until it reaches a trusted root, and checks validity dates, the hostname in Subject Alternative Name, and key usage. If an intermediate is missing, some clients can fetch it and others fail, which is why curl or mobile apps may break while desktop browsers work. Roots stay offline and protected; intermediates can be rotated without changing what devices trust.
Q3How do you make sure certificates never expire in production?
Automation plus monitoring. I use certbot or another ACME client with the systemd timer or cron, a deploy hook that reloads nginx, and I run certbot renew --dry-run after setup and after any server change. Port 80 stays open for HTTP-01, or I use DNS-01 with a scoped API token. Then I monitor from outside: an uptime tool that alerts when a certificate has less than about two weeks left. Lifetimes are getting shorter across the industry, heading to 47 days by 2029, so anything manual will fail eventually. Behind a CDN the edge certificate is managed, but the origin still needs the same care.
Q4What is HSTS and what are the risks?
HSTS is a response header, Strict-Transport-Security, that tells the browser to use only HTTPS for this domain for a period such as a year. After the first visit, the browser upgrades http:// links itself and refuses to let users click through certificate errors, which blocks SSL-stripping attacks. With the preload list, browsers enforce it even on the very first visit. The risk is lock-in: if any subdomain covered by includeSubDomains does not support HTTPS, or a certificate breaks, users cannot reach it. So I start with a short max-age, verify all subdomains, then increase it, and preload only when certain.
Q5A site works in Chrome but curl says "unable to get local issuer certificate". Why?
The server is most likely sending only its leaf certificate without the intermediate. Desktop browsers often hide this because they cache intermediates or download them using the AIA extension, but curl, many mobile clients, Node and Java do not. The fix is to configure the full chain: in nginx, ssl_certificate should point to fullchain.pem from certbot, not cert.pem. I verify with openssl s_client -showcerts and check that the intermediate appears, or use an online SSL checker. Less often, the client machine itself has an outdated CA bundle that needs an update.
Senior-level questions
S1Where should TLS terminate in a system with a CDN, load balancer and containers?
Each hop should be encrypted when it crosses a network you do not fully control. The CDN terminates the visitor's TLS. From CDN to origin I use Full (strict) with a valid certificate, ideally plus authenticated origin pulls. A load balancer may terminate and re-encrypt, or pass TLS through to nginx; termination there simplifies certificate management, re-encryption keeps data protected inside the network. Between nginx and containers on the same host over a private Docker network, plain HTTP is a common, acceptable trade-off. Across hosts or in shared clusters, use TLS or mTLS, often via a service mesh. Fewer termination points mean fewer keys to protect, but every hop that inspects traffic needs its own certificate and rotation.
S2How would you prepare for 47-day certificate lifetimes across many servers?
Assume nothing manual survives. Inventory every certificate: web servers, load balancers, mail, VPN, appliances and anything using a certificate file copied by hand. Move each one to ACME automation, or to a managed service such as a CDN or cloud load balancer that renews itself. Use ARI-capable clients so the CA can signal early renewal. Make reloads automatic through deploy hooks, and make sure configuration management does not overwrite renewed files. Monitor externally for expiry and for chain errors, alerting well before a third of the lifetime remains. Devices that cannot run ACME should sit behind a proxy that can. The investment pays off beyond compliance: rotations become routine, so an emergency revocation is no longer stressful.
S3Is certificate pinning in mobile apps worth it?
Pinning makes the app accept only specific keys, which blocks interception even if a user installs a rogue CA or a CA is compromised. The cost is fragility: if you rotate keys without the app knowing, every installed version stops working, and with short certificate lifetimes rotation is frequent. If pinning is required, pin the public key of an intermediate or your own long-lived key rather than the leaf, always ship backup pins, provide a remote way to update or disable pinning, and keep the same key across renewals where your CA process allows it. For most apps, standard TLS validation plus Certificate Transparency checks on the platform is enough. Banking and high-risk apps are where pinning usually pays off.
Glossary
- CA
- Certificate Authority: an organisation trusted to sign certificates after checking that the requester controls a domain.
- Leaf certificate
- The certificate for your own hostname, at the bottom of the chain.
- Intermediate CA
- A CA certificate between the root and your leaf; servers must send it along with the leaf.
- SAN
- Subject Alternative Name: the list of hostnames a certificate is valid for.
- SNI
- Server Name Indication: the hostname the client sends in the handshake so the server picks the right certificate.
- Forward secrecy
- Using temporary session keys, so a stolen server key cannot decrypt traffic recorded in the past.
- ACME
- The protocol CAs like Let's Encrypt use to issue and renew certificates automatically.
- HSTS
- A header telling browsers to use only HTTPS for a domain for a set period.
- Certificate Transparency
- Public logs of every issued certificate, so domain owners can spot unexpected ones.
- mTLS
- Mutual TLS: both client and server present certificates and verify each other.
Keep learning
Have a product in mind?Let's build it.
Tell me about your idea, your product or your team. I usually reply within one working day.