HTTPS is HTTP with a TLS layer on top. That layer encrypts every byte in transit, authenticates the server, and prevents tampering. This guide explains the mechanics step by step — from the handshake to certificate chains — plus how to set it up in code.
Quick reference
| Concept | Key fact |
|---|---|
| Protocol stack | HTTP → TLS → TCP → IP |
| Default port | 443 (HTTP uses 80) |
| Current version | TLS 1.3 (2018), widely supported |
| Deprecated | SSL 2.0, SSL 3.0, TLS 1.0, TLS 1.1 |
| Key exchange | ECDHE (ephemeral, forward secrecy) |
| Bulk cipher | AES-128-GCM or AES-256-GCM |
| Certificate type | X.509, signed by a Certificate Authority |
| Free certificates | Let's Encrypt (90-day, auto-renewable) |
| HSTS | Forces HTTPS in browser for a domain |
| Mixed content | HTTP resources on HTTPS page — blocked by browsers |
HTTP vs HTTPS
| Feature | HTTP | HTTPS |
|---|---|---|
| Encryption | None | TLS (symmetric AES) |
| Authentication | None | Server cert signed by CA |
| Integrity | None | AEAD cipher (tamper-evident) |
| Port | 80 | 443 |
| Speed | Slightly faster | Negligible difference with TLS 1.3 |
| SEO | Lower ranking | Google ranking boost |
| Browser indicator | ⚠ Not secure | 🔒 |
| Required for | — | Service Workers, HTTP/2, geolocation, camera |
HTTPS is not optional for modern web apps. Browsers block Service Workers, HTTP/2, and many APIs (camera, geolocation, clipboard) on plain HTTP.
Symmetric vs asymmetric encryption
Symmetric encryption: the same key encrypts and decrypts. Fast. Problem: how do two strangers agree on a shared key without an eavesdropper learning it?
Asymmetric encryption: a key pair (public + private). Anything encrypted with the public key can only be decrypted with the private key. Slow for bulk data. Used to establish a shared secret.
HTTPS uses both:
- Asymmetric crypto during the handshake to agree on a shared secret.
- Symmetric AES for all subsequent data — fast and efficient.
The TLS 1.3 handshake
TLS 1.3 completes in one round trip (vs two for TLS 1.2).
Client Server
| |
|-- ClientHello ------------------>|
| (supported ciphers, key share) |
| |
|<-- ServerHello ------------------|
| Certificate |
| CertificateVerify |
| Finished |
| (server's key share) |
| |
|-- Finished -------------------->|
| |
|<====== Encrypted data ===========>|
Step by step
1. ClientHello — browser sends:
- TLS version (1.3)
- List of supported cipher suites (e.g.,
TLS_AES_128_GCM_SHA256) - Key share: a temporary public key for ECDHE key exchange
- Random nonce
2. ServerHello + Certificate — server responds with:
- Chosen cipher suite
- Server's ECDHE public key (its half of the key exchange)
- Its X.509 certificate (containing the server's RSA/EC public key, domain name, validity dates, CA signature)
At this point, both sides can compute the same shared secret using ECDHE — without it ever crossing the wire.
3. CertificateVerify — server signs the handshake transcript with its private key, proving it owns the certificate.
4. Finished — both sides send a MAC over the handshake, confirming nothing was tampered with.
5. Application data — all subsequent bytes are encrypted with AES-GCM using session keys derived from the shared secret.
How ECDHE key exchange works
ECDHE (Elliptic Curve Diffie-Hellman Ephemeral) lets two parties compute the same number without transmitting it.
# Conceptual (simplified)
# Both agree on a public curve and base point G
Server: picks random s → computes S = s × G → sends S
Client: picks random c → computes C = c × G → sends C
Server computes: s × C = s × c × G
Client computes: c × S = c × s × G ← same value!
# Eavesdropper sees G, S, C — cannot compute s×c×G
# (discrete log problem on elliptic curves)
"Ephemeral" means a fresh key pair is generated per connection. Even if the server's long-term private key is later stolen, past sessions are safe — this property is called forward secrecy.
X.509 certificates
A certificate is a signed document that binds a public key to a domain name.
Certificate fields:
Subject: CN=example.com, O=Example Inc
Issuer: CN=Let's Encrypt R3
Valid from: 2026-01-01
Valid until:2026-04-01
Public key: EC (prime256v1) — server's public key
SANs: example.com, www.example.com
Signature: SHA256withECDSA (signed by Let's Encrypt R3)
The browser trusts it because:
- Let's Encrypt R3 signed it → verify with Let's Encrypt's public key.
- Let's Encrypt R3's cert is signed by ISRG Root X1 (a root CA).
- ISRG Root X1 is in the browser's trust store (pre-installed root CAs).
This chain of trust is called the certificate chain or PKI (Public Key Infrastructure).
ISRG Root X1 (root CA — self-signed, in browser trust store)
└── Let's Encrypt R3 (intermediate CA)
└── example.com (end-entity certificate)
Certificate types
| Type | Validation | Use case |
|---|---|---|
| DV (Domain Validated) | Proves domain control only | Most websites, Let's Encrypt |
| OV (Organization Validated) | Domain + org identity checked | Corporate sites |
| EV (Extended Validation) | Deep org vetting | Banks, payment processors |
| Wildcard | *.example.com covers subdomains |
Multi-subdomain setups |
| SAN | Multiple domains in one cert | example.com + api.example.com |
| Self-signed | No CA, browser shows error | Local dev only |
How to get a certificate
Let's Encrypt with Certbot (nginx)
# Install Certbot
sudo apt install certbot python3-certbot-nginx
# Obtain and install certificate
sudo certbot --nginx -d example.com -d www.example.com
# Auto-renewal (certbot sets up a systemd timer or cron)
sudo certbot renew --dry-run
# Check renewal timer
sudo systemctl status certbot.timer
Certbot edits your nginx config to enable HTTPS and redirect HTTP → HTTPS.
Manual certificate (nginx config)
server {
listen 443 ssl http2;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
# Modern TLS settings
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 (faster cert validation)
ssl_stapling on;
ssl_stapling_verify on;
resolver 1.1.1.1 1.0.0.1 valid=300s;
# HSTS: tell browsers to always use HTTPS for 1 year
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
location / {
proxy_pass http://localhost:3000;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
# Redirect HTTP → HTTPS
server {
listen 80;
server_name example.com www.example.com;
return 301 https://$host$request_uri;
}
HTTPS in code
Node.js (built-in https module)
import https from 'https';
import fs from 'fs';
import { createServer } from 'http';
// HTTPS server
const options = {
cert: fs.readFileSync('/etc/letsencrypt/live/example.com/fullchain.pem'),
key: fs.readFileSync('/etc/letsencrypt/live/example.com/privkey.pem'),
};
https.createServer(options, (req, res) => {
res.writeHead(200);
res.end('Hello HTTPS\n');
}).listen(443);
// Redirect HTTP → HTTPS
createServer((req, res) => {
res.writeHead(301, { Location: `https://${req.headers.host}${req.url}` });
res.end();
}).listen(80);
In production, use a reverse proxy (nginx, Caddy) for TLS termination and run Node.js on a plain HTTP internal port.
Node.js — making HTTPS requests
// fetch uses HTTPS automatically for https:// URLs
const response = await fetch('https://api.example.com/data');
const data = await response.json();
// Self-signed cert in dev (Node.js)
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; // dev only, never production
Python (ssl module + requests)
import ssl
import http.server
# HTTPS server
context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
context.load_cert_chain('/path/to/fullchain.pem', '/path/to/privkey.pem')
server = http.server.HTTPServer(('', 443), http.server.SimpleHTTPRequestHandler)
server.socket = context.wrap_socket(server.socket, server_side=True)
server.serve_forever()
# Making HTTPS requests
import requests
# Normal HTTPS (verified by default)
response = requests.get('https://api.example.com/data')
# Custom CA bundle
response = requests.get('https://internal.corp', verify='/path/to/ca-bundle.pem')
# Self-signed cert in dev (don't use in production)
response = requests.get('https://localhost', verify=False)
Go (standard library)
package main
import (
"crypto/tls"
"net/http"
"log"
)
func main() {
// HTTPS server
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello HTTPS"))
})
server := &http.Server{
Addr: ":443",
Handler: mux,
TLSConfig: &tls.Config{
MinVersion: tls.VersionTLS12,
CurvePreferences: []tls.CurveID{
tls.X25519,
tls.CurveP256,
},
},
}
log.Fatal(server.ListenAndServeTLS(
"/etc/letsencrypt/live/example.com/fullchain.pem",
"/etc/letsencrypt/live/example.com/privkey.pem",
))
}
// Making HTTPS requests (Go verifies certs by default)
resp, err := http.Get("https://api.example.com/data")
HSTS (HTTP Strict Transport Security)
HSTS tells browsers to always use HTTPS for a domain, even if the user types http://.
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
| Directive | Meaning |
|---|---|
max-age=31536000 |
Remember for 1 year |
includeSubDomains |
Also applies to *.example.com |
preload |
Submit to browser preload list (hardcoded HTTPS) |
HSTS preload: submit your domain to hstspreload.org and it's baked into Chrome, Firefox, Safari. Even the very first request is HTTPS. Irreversible for ~1 year — only add if you're committed to HTTPS forever.
Certificate validation by the browser
When a browser receives a server certificate, it checks:
- Chain of trust: every certificate up the chain is signed by the next, ending at a trusted root CA.
- Hostname match: the cert's Subject Alternative Names (SANs) include the domain being visited.
- Validity period: current date is between
notBeforeandnotAfter. - Revocation: OCSP (Online Certificate Status Protocol) or CRL — has the CA revoked this cert?
- Signature algorithm: not MD5 or SHA-1 (deprecated).
If any check fails, the browser shows an error and blocks the page.
OCSP stapling
Without stapling, the browser queries the CA's OCSP server on every page load — slow and privacy-leaking (CA learns which sites you visit).
With OCSP stapling, the web server periodically fetches its own OCSP response and "staples" it to the TLS handshake. The browser gets fresh revocation status without a round trip to the CA.
ssl_stapling on;
ssl_stapling_verify on;
ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
resolver 1.1.1.1 8.8.8.8 valid=300s;
resolver_timeout 5s;
Common mistakes
| Mistake | Problem | Fix |
|---|---|---|
| Expired certificate | Browser error, site down | Auto-renew with Certbot, monitor expiry dates |
| Self-signed cert in production | Users see security warning | Get a DV cert (free via Let's Encrypt) |
| Missing intermediate cert | Some clients fail | Serve fullchain.pem, not just cert.pem |
| TLS 1.0/1.1 enabled | Vulnerable to POODLE, BEAST | Allow only TLS 1.2+ |
| HTTP resources on HTTPS page | Mixed content blocked | Update all URLs to HTTPS, use CSP |
| No HSTS header | Downgrade attacks possible | Add Strict-Transport-Security header |
| Storing private key insecurely | Key theft → impersonation | chmod 600, owned by root, never commit to git |
NODE_TLS_REJECT_UNAUTHORIZED=0 in production |
Disables cert verification | Only in dev, remove before deploy |
Debugging HTTPS
# Check certificate details
openssl s_client -connect example.com:443 -servername example.com
# View cert expiry
echo | openssl s_client -connect example.com:443 2>/dev/null | openssl x509 -noout -dates
# Test TLS version and cipher support
nmap --script ssl-enum-ciphers -p 443 example.com
# Test from command line (follows redirects, shows cert info)
curl -vI https://example.com 2>&1 | grep -E "subject|issuer|expire|TLS|SSL"
# Check HSTS header
curl -sI https://example.com | grep -i strict
# Test HTTP → HTTPS redirect
curl -sI http://example.com | grep -i location
# Online tools
# https://www.ssllabs.com/ssltest/ — full TLS configuration audit
# https://securityheaders.com — check security headers
FAQ
What's the difference between SSL and TLS?
SSL (Secure Sockets Layer) was the original protocol. It was deprecated because of serious vulnerabilities (SSLv3/POODLE). TLS (Transport Layer Security) is its successor. TLS 1.0 and 1.1 are also deprecated. Today you should use TLS 1.2 or 1.3. Despite this, people still say "SSL certificate" colloquially — they mean a TLS certificate.
Does HTTPS hide the URL from my ISP?
The domain name is visible (in DNS queries and in the TLS SNI field). The path, query string, request body, and headers are encrypted. Your ISP sees example.com but not /login?user=alice. Use a VPN or Tor to hide the domain too.
What is a wildcard certificate?
A cert for *.example.com covers any single-level subdomain: api.example.com, www.example.com, app.example.com. It does not cover deep.api.example.com or example.com itself (though you can add example.com as a SAN).
Can I get HTTPS for localhost?
Yes. Use mkcert — a tool that creates locally trusted certs:
brew install mkcert # or choco install mkcert on Windows
mkcert -install # installs a local root CA into your system trust store
mkcert localhost 127.0.0.1 # creates localhost.pem + localhost-key.pem
How long does a TLS certificate last?
As of September 2020, browser-trusted certificates are valid for a maximum of 398 days. Let's Encrypt issues 90-day certs and recommends auto-renewal every 60 days. Apple and Google are pushing for 47-day max certs starting 2026.
What is certificate pinning?
Mobile apps can "pin" a specific cert or public key and refuse connections to any other cert — even a valid one. It defeats CA compromise attacks but breaks when the cert rotates. Modern recommendation: pin the public key (HPKP is deprecated; use TrustKit or similar library), not the cert itself.