🔒 SSL/TLS Certificates
That little padlock in the address bar carries a surprising amount of weight: it means no one between your user and your server can read or tamper with the traffic, and that your server is who it claims to be. This lesson explains how that guarantee works — and how to get it for free.
Week 13 · Day 5 (Friday: Production Deployment) · Lecture 2
🎯 Learning Objectives
By the end of this lesson, you will be able to:
- Explain what TLS provides — encryption, integrity, and identity — and why HTTPS is now mandatory
- Describe the TLS handshake and how a certificate is verified through the CA chain of trust
- Obtain a free, auto-renewing certificate with Let's Encrypt and certbot
- Configure TLS termination at a reverse proxy or load balancer with Nginx
- Redirect HTTP to HTTPS and enable HSTS to lock in secure connections
- Choose between single-domain, wildcard, and SAN certificates
Estimated Time: 70 minutes
Practice: Secure a domain end-to-end with certbot and a hardened Nginx config.
In This Lesson
Why HTTPS?
Imagine mailing a bank withdrawal slip on a postcard. Every postal worker along the route can read your account number, and any of them could cross out the amount and write in a bigger one. Plain HTTP is exactly that postcard: it travels in clear text across dozens of routers, Wi-Fi access points, and ISPs, any of which can read or rewrite it.
HTTPS is HTTP inside a sealed, tamper-evident envelope. That envelope is TLS (Transport Layer Security), the modern successor to the older SSL protocol. The names are used interchangeably — "SSL certificate" is the common phrase — but every secure connection today speaks TLS, not the long-deprecated SSL.
Since 2018, browsers mark plain HTTP pages as "Not Secure." Beyond trust, HTTPS is now a hard requirement for modern web features — service workers, HTTP/2, the geolocation and camera APIs — and it's a Google ranking signal. There is no longer a reason to run a public site without it.
📖 SSL vs TLS in one line
SSL 2.0/3.0 and TLS 1.0/1.1 are all deprecated and insecure. Use TLS 1.2 as a minimum and TLS 1.3 as the preferred protocol — 1.3 has a faster handshake and drops every weak option.
What TLS Actually Does
TLS delivers three distinct guarantees. It helps to keep them separate, because a certificate is only responsible for one of them.
| Guarantee | Means | Provided by |
|---|---|---|
| Encryption | Eavesdroppers see only scrambled bytes | A shared session key agreed during the handshake |
| Integrity | Data can't be altered in transit undetected | Message authentication codes on every record |
| Identity | You're talking to the real server, not an impostor | The certificate, signed by a trusted CA |
This is the key insight: encryption alone is not enough. Anyone can generate an encryption key. Without identity, you might be exchanging perfectly encrypted messages with an attacker sitting in the middle. The certificate is what binds a public key to a verified domain name, so identity and encryption together defeat the "man in the middle."
The TLS Handshake
Before the first byte of your web page moves, the browser and server run a quick negotiation called the handshake. Its job: verify the server's identity and agree on a secret key that only the two of them know.
Walking through it: the browser lists what it supports, the server picks the strongest common option and presents its certificate, the browser checks that certificate is trustworthy (next section), and then both sides derive a fresh session key. From that point every HTTP request and response is encrypted with that key. TLS 1.3 compresses this into a single round trip, so the security cost is milliseconds.
💡 Asymmetric to symmetric
The certificate's public/private key pair (slow, asymmetric) is used only to safely agree on a session key. The bulk data is then encrypted with that session key (fast, symmetric). You get the trust of public-key crypto with the speed of symmetric crypto.
Certificates & the CA Chain
How does your browser decide a certificate is legitimate? It doesn't trust your server directly — it trusts a small set of Certificate Authorities (CAs) whose root certificates ship with the operating system and browser. Your certificate is trusted because a CA signed it, forming a chain up to one of those pre-trusted roots.
(in the OS/browser trust store)"] --> B["Intermediate CA"] B --> C["Your certificate
example.com"] C --> D["Browser verifies the
signature up the chain"] D --> E{"Chains to a
trusted root?"} E -->|Yes| F["Show the padlock"] E -->|No| G["Show a security warning"]
Each link is a cryptographic signature: the intermediate signs your certificate, the root signs the intermediate. If the browser can follow the signatures all the way to a root it already trusts — and the domain matches, and it hasn't expired — the padlock appears. Break any link (a missing intermediate, wrong domain, expired date) and the user sees a warning.
⚠️ Always serve the full chain
The most common "works in my browser but not for some users" bug is a missing intermediate certificate. Serve fullchain.pem (your cert plus the intermediates), not just cert.pem. Let's Encrypt gives you fullchain.pem for exactly this reason.
What's inside a certificate
- Subject — the domain(s) it's valid for (the Common Name and Subject Alternative Names)
- Public key — used during the handshake
- Issuer — which CA signed it
- Validity period — not-before and not-after dates
- Signature — the CA's cryptographic stamp of approval
# Inspect the certificate a live server is serving
echo | openssl s_client -servername example.com -connect example.com:443 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
Let's Encrypt & certbot
Not long ago, certificates cost money and were installed by hand once a year. Let's Encrypt, a non-profit CA launched in 2016, changed the web by issuing certificates that are free, automated, and open. It now secures hundreds of millions of domains.
The magic is the ACME protocol (Automated Certificate Management Environment). Instead of a human proving domain ownership, a program does it: it proves you control the domain, and the CA issues a certificate — all in seconds, all repeatable.
Domain validation challenges
- HTTP-01: certbot places a token at
/.well-known/acme-challenge/and Let's Encrypt fetches it over port 80. Simplest for a single server. - DNS-01: certbot creates a TXT record on your domain. Required for wildcard certificates and works even when the server isn't publicly reachable.
Getting a certificate with certbot
# Install certbot and the Nginx plugin (Ubuntu/Debian)
sudo apt update
sudo apt install certbot python3-certbot-nginx
# Obtain a certificate AND auto-configure Nginx to use it
sudo certbot --nginx -d example.com -d www.example.com
# Or just fetch certificates without touching server config
sudo certbot certonly --nginx -d example.com
# Wildcard certificate (requires the DNS challenge)
sudo certbot certonly --manual --preferred-challenges dns \
-d example.com -d '*.example.com'
✅ Auto-renewal is the whole point
Let's Encrypt certificates last only 90 days — deliberately, to force automation. certbot installs a systemd timer (or cron job) that renews them well before expiry. Verify it works with a dry run:
# Simulate a renewal without actually issuing a new cert
sudo certbot renew --dry-run
# Check the timer is active
systemctl status certbot.timer
Set this up once and certificate expiry — a classic cause of outages — simply stops being your problem.
TLS Termination & Nginx
Where does encryption get decrypted? In almost every real deployment, not in your Node.js app. Instead a reverse proxy or load balancer sits in front, decrypts TLS, and forwards plain HTTP to your app over a trusted internal network. This is called TLS termination.
TLS termination"] B -->|"HTTP (internal, trusted)"| C["Node.js app"] C --> D["Database"]
Terminating at the proxy means your app doesn't manage certificates, the proxy handles TLS efficiently for every backend, and you can scale app instances freely behind one certificate. Managed platforms (Render, Vercel, AWS ALB, Cloudflare) do this for you automatically — you often never touch a .pem file.
Nginx TLS configuration
# /etc/nginx/sites-available/example.com
server {
listen 443 ssl;
http2 on;
server_name example.com www.example.com;
# Certificate paths from certbot
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern, secure protocols only
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off; # let TLS 1.3 pick; modern guidance
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
# Forward to the Node app over plain HTTP on the internal network
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme; # so the app knows it was HTTPS
}
}
⚠️ Protect the private key
The privkey.pem file is the crown jewel — anyone with it can impersonate your site. Keep it readable only by root (chmod 600), never commit it, and never copy it into a Docker image or a chat message.
Redirects, HSTS & Cert Types
Redirect HTTP to HTTPS
Serving HTTPS isn't enough if users can still reach the insecure version. Redirect every HTTP request to HTTPS with a permanent 301.
# A second server block catches plain HTTP and bounces it to HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
HSTS — remove the insecure window
Even a redirect has a gap: the very first request travels over HTTP and could be hijacked. HSTS (HTTP Strict Transport Security) closes it. Once a browser sees this header, it refuses to talk to your domain over HTTP at all — for the entire max-age.
# Add inside the HTTPS server block. Two years, including subdomains.
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
⚠️ HSTS is a commitment
While the header is cached, browsers cannot reach your domain over HTTP — there's no override. Only add includeSubDomains and preload once every subdomain is HTTPS-ready. Start with a short max-age, confirm everything works, then raise it.
Choosing a certificate shape
| Type | Covers | Use when |
|---|---|---|
| Single-domain | example.com | One hostname, simplest case |
| SAN / multi-domain | example.com, example.org, … | A few specific hostnames on one cert |
| Wildcard | *.example.com | Many subdomains (app, api, blog) |
A wildcard covers only one level (api.example.com, not v1.api.example.com) and requires the DNS-01 challenge to issue. For most projects, a single cert listing example.com and www.example.com via SAN is exactly right.
💡 DV, OV, EV
Certificates also differ by validation level: Domain Validation (DV, proves domain control — what Let's Encrypt issues), Organization Validation, and Extended Validation. Modern browsers no longer show special UI for OV/EV, so for the vast majority of apps a free DV certificate is all you need.
Practice & Quiz
🏋️ Exercise 1: HTTP to HTTPS redirect
Goal: Write the Nginx server block that listens on port 80 for myapp.com and permanently redirects every request to the HTTPS version, preserving the path.
💡 Hint
Use listen 80;, a server_name, and return 301 with the $host and $request_uri variables so the full path is carried over.
✅ Solution
server {
listen 80;
server_name myapp.com www.myapp.com;
return 301 https://$host$request_uri;
}
🏋️ Exercise 2: Issue a wildcard certificate
Goal: Write the certbot command to obtain a certificate covering both shop.io and every one of its subdomains, and name the challenge type it requires.
✅ Solution
# Wildcards require the DNS-01 challenge (a TXT record proves control)
sudo certbot certonly --manual --preferred-challenges dns \
-d shop.io -d '*.shop.io'
Quote '*.shop.io' so the shell doesn't expand the asterisk.
🎯 Quick Quiz
Question 1: A certificate primarily proves which TLS guarantee?
Question 2: Why do Let's Encrypt certificates expire after only 90 days?
Question 3: Which file should Nginx's ssl_certificate point to?
Best Practices & Pitfalls
✅ Do
- Use free, auto-renewing Let's Encrypt certificates and verify the renewal timer
- Serve
fullchain.pemso the full CA chain validates for everyone - Restrict to TLS 1.2 and 1.3; disable everything older
- Redirect HTTP → HTTPS and add HSTS once you're confident
- Terminate TLS at a proxy/load balancer and keep the private key at
chmod 600
❌ Don't
- Commit private keys to git or copy them into container images
- Let renewal be a manual calendar reminder (it will be missed)
- Enable HSTS
preloadbefore every subdomain is HTTPS-ready - Serve mixed content — HTTP assets on an HTTPS page get blocked
✅ Verify your setup
After configuring TLS, grade it with the free Qualys SSL Labs Server Test. Aim for an A. It flags weak protocols, missing intermediates, and expiry problems before your users hit them.
Summary
🎉 Key Takeaways
- TLS provides encryption, integrity, and identity; the certificate supplies identity
- The handshake verifies the cert, then derives a fast symmetric session key
- Trust flows through the CA chain up to a root in the browser's trust store — serve
fullchain.pem - Let's Encrypt + certbot give free, 90-day, auto-renewing certificates
- Terminate TLS at a proxy, redirect HTTP → HTTPS, and lock it in with HSTS
📚 Additional Resources
- Let's Encrypt — Getting Started
- certbot (EFF) documentation
- MDN — Transport Layer Security (TLS)
- MDN — HTTP Strict-Transport-Security (HSTS)
- Mozilla SSL Configuration Generator
🚀 What's Next?
Your certificate is issued for a domain name — but a certificate is useless if that name doesn't point at your server. The next lesson covers Domain Configuration: DNS records (A, AAAA, CNAME, TXT, MX), apex vs www, TTL and propagation, and pointing your domain at a host or load balancer.
🔒 Padlock earned
You understand what that lock icon promises and how to deliver it — for free, on autopilot. Your users' data is safe in transit.