HTTPS and TLS in Practice
What the padlock really means: certificates in the browser, the TLS handshake in practice, HSTS, automated certificates with ACME, shrinking certificate lifetimes, and the errors you will meet.
What HTTPS adds to HTTP
Plain HTTP (see HTTP in Depth) sends every request and response as readable text. Anyone on the path — the café Wi-Fi, a compromised router, an internet provider — can read it, and change it. HTTPS (Hypertext Transfer Protocol Secure) is the same HTTP, carried inside a Transport Layer Security (TLS) connection, by default on TCP port 443 instead of 80. Nothing about methods, headers or status codes changes; TLS wraps around them.
TLS adds three guarantees:
- Encryption (confidentiality): someone on the path sees that you talk to a server, how much and when — but not the URL path, the headers, the cookies or the body.
- Integrity: every record carries an authentication tag; a changed byte makes the connection fail instead of delivering altered content.
- Server authentication: the server proves it holds the private key for a certificate that a trusted certificate authority issued for the name you asked for. How that trust works is explained in PKI and Certificates.
What stays visible: the server's IP address and, in most connections today, the host name, because the client sends it in the clear as Server Name Indication (SNI) so that one server can pick the right certificate. DNS lookups are also visible unless the device uses encrypted DNS.
What the padlock does and does not mean
The padlock — or, in recent Chrome versions, a neutral settings icon in the same place — means one thing: this connection is encrypted, and the other end proved it holds a valid certificate for exactly this host name. It says nothing about who runs the site or whether it is safe. A phishing site gets a free, valid certificate for its own look-alike name in minutes. Teach users to read the name, not the icon (see the padlock in PKI and Certificates).
The padlock also says nothing about what happens after the server: a reverse proxy may decrypt the traffic and pass it on in plain HTTP inside the data center (see TLS termination), and a company proxy may inspect it with its own root certificate.
The TLS 1.3 handshake, simplified
Before any HTTP is sent, client and server run a TLS handshake: they agree on a TLS version and cipher suite, exchange key shares from which both compute the same secret session keys, and the server proves its identity with its certificate. In TLS 1.3 (RFC 8446, 2018) this costs a single round trip after the TCP connection (three-way handshake) is up.
-
ClientHello. The browser lists the TLS versions
and cipher suites it supports, sends a key share (its half of an
ephemeral Diffie–Hellman exchange), the host name (SNI) and the
application protocols it can speak, such as
h2andhttp/1.1(Application-Layer Protocol Negotiation, ALPN). - ServerHello. The server picks the version and cipher suite and sends its own key share. From the two shares both sides now compute the same secret, so everything that follows is encrypted.
- Certificate, CertificateVerify, Finished. The server sends its certificate chain and a signature over the handshake made with its private key — the proof that it owns the certificate. Finished seals the handshake against tampering.
- Client checks and Finished. The browser validates the chain, the name and the dates (next section). If all is well it sends its own Finished and, without waiting, its first HTTP request.
Because the session keys come from throwaway key shares, recorded traffic cannot be decrypted later even if the server's private key leaks (forward secrecy). TLS 1.3 also allows a resumed connection to send data in its very first flight (0-RTT, "early data"); that data can be replayed by an attacker, so servers should accept it only for requests that are safe to repeat, such as a plain GET. HTTP/3 builds the same TLS 1.3 handshake into QUIC, so the transport and the encryption are set up together (see HTTP in Depth).
Which TLS versions to allow
| Version | Status (as of 2026) |
|---|---|
| SSL 2.0, SSL 3.0 | Broken and prohibited; disable everywhere |
| TLS 1.0, TLS 1.1 | Formally deprecated by RFC 8996 (March 2021); browsers no longer accept them |
| TLS 1.2 | Still widely used and acceptable with modern cipher suites (forward secrecy, AES-GCM or ChaCha20-Poly1305) |
| TLS 1.3 | Current version (RFC 8446); fewer options, all of them strong |
A sensible server configuration today offers TLS 1.2 and 1.3 and nothing older. If an old device or script suddenly cannot connect after a server upgrade, it probably only speaks TLS 1.0 or 1.1: the fix is to update the client, not to re-enable the old protocol.
Certificates as the browser sees them
Click the icon in the address bar and open the certificate viewer: you see the fields a browser actually checks. How certificates, certificate authorities and the chain of trust work is covered in PKI and Certificates; here is what to look at when something is wrong.
-
Subject Alternative Name (SAN): the list of host
names the certificate is valid for. Browsers match only against
this list; the old Common Name (CN) in the subject is ignored for
matching. A wildcard such as
*.portal.example.comcovers exactly one extra label on the left —api.portal.example.com, but notportal.example.comitself and notv2.api.portal.example.com. - Chain: leaf certificate, then one or more intermediate certificates, ending at a root that the device already trusts. The server must send the leaf and the intermediates (see root and intermediate CAs).
- Validity: Not Before and Not After. The client compares them with its own clock — which is why a device with a wrong date sees errors on every site.
Client settings and name matching in Python
Python's HTTPS clients (urllib.request,
http.client) use the context from
ssl.create_default_context(). Printing its settings
shows what "verify the certificate" means in practice:
import ssl
# The context Python's HTTPS clients (urllib, http.client) use by default
ctx = ssl.create_default_context()
print("check_hostname: ", ctx.check_hostname)
print("verify_mode: ", ctx.verify_mode.name)
print("minimum_version:", ctx.minimum_version.name)
# Make the floor explicit instead of leaving it to the system's OpenSSL
ctx.minimum_version = ssl.TLSVersion.TLSv1_2
print("minimum_version:", ctx.minimum_version.name)
# What "just make the certificate error go away" really switches off
unsafe = ssl._create_unverified_context()
print("unverified: ", unsafe.check_hostname, unsafe.verify_mode.name)
check_hostname: True verify_mode: CERT_REQUIRED minimum_version: MINIMUM_SUPPORTED minimum_version: TLSv1_2 unverified: False CERT_NONE
CERT_REQUIRED means the chain must lead to a trusted
root; check_hostname means the name must be in the SAN.
On this system it prints MINIMUM_SUPPORTED:
Debian-family builds leave the floor to the system's OpenSSL, whose
3.x defaults already refuse TLS 1.0 and 1.1. Python.org builds (3.10
and later) set TLS 1.2 as the minimum themselves. Either way,
setting TLSv1_2 explicitly makes your intent visible.
The unverified context turns both checks off: the connection is
still encrypted, but to anyone who answers — exactly what
an on-path attacker needs.
The name and date checks themselves are simple. This example applies
them to a certificate written out as a Python dictionary, in the
shape getpeercert() returns — no network needed:
import ssl
# A certificate as Python's ssl module reports it (getpeercert())
cert = {
"subject": ((("commonName", "portal.example.com"),),),
"subjectAltName": (
("DNS", "portal.example.com"),
("DNS", "*.portal.example.com"),
),
"notBefore": "Sep 1 00:00:00 2026 GMT",
"notAfter": "Mar 19 23:59:59 2027 GMT",
}
def name_matches(pattern, host):
"""One SAN entry against a host name (case-insensitive)."""
pattern, host = pattern.lower(), host.lower()
if not pattern.startswith("*."):
return pattern == host
# A wildcard stands for exactly one label, in the leftmost position
first, _, rest = host.partition(".")
return first != "" and rest == pattern[2:]
def check(cert, host, now):
names = [value for kind, value in cert["subjectAltName"] if kind == "DNS"]
if not any(name_matches(n, host) for n in names):
return "name mismatch"
if now < ssl.cert_time_to_seconds(cert["notBefore"]):
return "not yet valid (check the clock)"
if now > ssl.cert_time_to_seconds(cert["notAfter"]):
return "expired"
return "OK"
today = ssl.cert_time_to_seconds("Oct 1 12:00:00 2026 GMT")
for host in ["portal.example.com", "PORTAL.example.com",
"api.portal.example.com", "v2.api.portal.example.com",
"example.com"]:
print(f"{host:28} {check(cert, host, today)}")
later = ssl.cert_time_to_seconds("Apr 1 12:00:00 2027 GMT")
print(f"{'portal.example.com in 2027':28} {check(cert, 'portal.example.com', later)}")
portal.example.com OK PORTAL.example.com OK api.portal.example.com OK v2.api.portal.example.com name mismatch example.com name mismatch portal.example.com in 2027 expired
Real clients do more — they verify every signature in the chain, key usage and revocation — but the name and date rules are exactly these. Note the lifetime of this certificate: just under 200 days, the current maximum (next sections).
HSTS: HTTPS only, from the first click
Most people type portal.example.com, not
https://portal.example.com. The first request then goes
out over plain HTTP, and the server answers with a redirect (301) to
HTTPS. That first plain request is a gap: an attacker on the path
can answer it instead and keep the victim on HTTP.
HTTP Strict Transport Security (HSTS), RFC 6797, closes that gap after the first visit. The server sends this header over HTTPS:
Strict-Transport-Security: max-age=31536000; includeSubDomains
For the next max-age seconds (here one year), the
browser rewrites every http:// link to that host to
https:// before sending anything, and — just as
important — it no longer lets the user click through a certificate
warning for that site. includeSubDomains extends the
rule to every subdomain. Browsers ignore the header when it arrives
over plain HTTP, because an attacker could forge it there.
The preload list
HSTS still trusts the very first visit. The
preload list removes that: it is a list of domains
built into the browser itself, so even the first request goes over
HTTPS. Chrome maintains it, and Firefox, Safari and Edge derive
their lists from it. To be accepted at hstspreload.org a domain
needs a valid certificate, a redirect from HTTP to HTTPS, HTTPS on
all subdomains, and a header with max-age of at least
one year plus includeSubDomains and
preload.
According to hstspreload.org, Chrome and Safari now also try HTTPS first for ordinary navigations. HSTS remains useful: it makes the upgrade mandatory instead of best effort, and it blocks click-through on certificate errors.
Automated certificates with ACME
The Automatic Certificate Management Environment (ACME), RFC 8555, is the protocol that lets software obtain and renew certificates without a person: an ACME client on (or near) the server creates a key pair, asks the CA for a certificate, proves control of the domain, and installs the result. Let's Encrypt made it popular; today many CAs, web servers (Caddy does it by default), load balancers and hosting panels speak ACME. A short overview is also in ACME and Let's Encrypt.
Proving control: the challenges
| Challenge | How the client proves control | Good for |
|---|---|---|
| HTTP-01 |
Serves a token at
http://<name>/.well-known/acme-challenge/<token>
on port 80
|
Ordinary web servers reachable from the internet |
| DNS-01 |
Publishes a TXT record at
_acme-challenge.<name>
|
Wildcard certificates, servers not reachable from the internet; needs API access to DNS |
| TLS-ALPN-01 | Answers a special TLS handshake on port 443 | Proxies and servers that handle TLS themselves |
A firewall that blocks port 80 "because we only use HTTPS" is a classic reason why HTTP-01 renewals suddenly fail: the CA must reach port 80 to fetch the token.
Let's Encrypt today
- No more OCSP. Let's Encrypt shut down its Online Certificate Status Protocol (OCSP) service on 6 August 2025, for privacy reasons (OCSP tells the CA which sites a user visits). Revocation is now published only through certificate revocation lists (CRLs) — see revocation.
- Six-day certificates (the "shortlived" profile, 160 hours) and certificates for IP addresses have been generally available since 15 January 2026. They are opt-in and not planned to become the default; IP address certificates are always short-lived.
- 90 days, then less. The default (the "classic" profile) is still 90 days as of October 2026. Let's Encrypt has announced 64 days from 10 February 2027 and 45 days from 16 February 2028; the opt-in "tlsserver" profile already issues 45-day certificates since 13 May 2026.
Shrinking certificate lifetimes
The certificate lifetime is the period between a certificate's Not Before and Not After dates. For publicly trusted TLS certificates the CA/Browser Forum — the body of certificate authorities and browser makers that sets the rules — caps it. Ballot SC-081v3, passed on 11 April 2025, shrinks the cap in steps. At the time of writing the schedule is:
| Issued from | Maximum lifetime |
|---|---|
| Before 15 March 2026 | 398 days |
| 15 March 2026 | 200 days |
| 15 March 2027 | 100 days |
| 15 March 2029 | 47 days |
All these dates are in the ballot itself; some CAs issue a day less (199 days) to keep a margin. The same ballot also shortens how long a CA may reuse an earlier domain check, to 10 days by 2029. Any material that says "certificates last a year" is out of date.
Why shorter? A stolen key or a wrongly issued certificate is useful to an attacker only until it expires, and revocation has never worked reliably. And short lifetimes force automation, which removes the most common outage of all: the certificate someone forgot to renew. With 47-day certificates, a team that renews by hand would do so roughly eight times a year per certificate.
Certificate transparency
A CA that is tricked or compromised can issue a certificate for your domain to someone else. Certificate transparency (CT) makes that visible: CAs submit every publicly trusted certificate to several public, append-only logs, and the logs hand back signed receipts (signed certificate timestamps, SCTs) usually embedded in the certificate. Chrome, Safari and, since 2025, Firefox reject public certificates without them. The protocol is described in RFC 6962 (2013) and its successor RFC 9162 (2021), both experimental RFCs.
Because the logs are public, anyone can search them — for example at
crt.sh. Two practical uses: monitor the logs for certificates issued
for your own domains that you did not request, and remember that
every host name you put in a public certificate becomes public too.
A name like new-merger-test.example.com appears in the
logs within minutes.
Mixed content
Mixed content is an HTTPS page that loads some of its parts — scripts, stylesheets, images, frames — over plain HTTP. Those parts can be read and changed on the way, which undoes the protection of the page: a replaced script controls the whole page.
- Scripts, stylesheets, frames and requests made by scripts ("active" content) are blocked by browsers.
- Images, audio and video are upgraded to HTTPS automatically by modern browsers and blocked if that fails. Either way, the browser's developer tools console shows a mixed-content message (see developer tools).
The fix is to load every resource over HTTPS — absolute
https:// URLs or URLs relative to the page. During a
migration, the Content Security Policy directive
upgrade-insecure-requests tells the browser to rewrite
http:// subresources to https:// (see
Content Security Policy). Hard-coded http:// links in a database or a content
management system are the usual leftover.
Troubleshooting certificate errors
Certificate errors look alarming, but four causes cover almost all of them. The codes are Chrome's; other browsers and tools use similar wording.
| Symptom | Likely cause | How to confirm | Fix |
|---|---|---|---|
Expired (NET::ERR_CERT_DATE_INVALID) for
everyone
|
Renewal failed, or the new certificate was never loaded |
Expire date in the certificate viewer or
curl -v; renewal client logs
|
Renew, reload the server, automate and monitor |
Name mismatch
(NET::ERR_CERT_COMMON_NAME_INVALID)
|
The name typed is not in the SAN:
example.com vs www.example.com, an
internal alias, an IP address, or two subdomain levels under
a wildcard
|
Compare the address bar with the SAN list | Reissue with every name users actually use, or redirect to a covered name |
Untrusted issuer
(NET::ERR_CERT_AUTHORITY_INVALID); works in one
browser, fails in scripts or apps
|
Missing intermediate on the server, a self-signed or internal CA, or a TLS-inspecting proxy whose root is not on this device |
Look at the issuer and at how many certificates the server
sends (openssl s_client -showcerts)
|
Configure the full chain file; install the internal root through device management |
| Date errors on every HTTPS site from one device | The device's clock is wrong (dead CMOS battery, no time sync), so valid certificates look expired or not yet valid | Check the date and time on the device | Fix time synchronization (NTP) |
"Works in my browser, fails in curl or Python" points almost always to the missing intermediate: browsers often repair an incomplete chain themselves (from a cache or by downloading it), while most other clients do not. The PKI page has more on reading certificate problems and inspecting certificates with OpenSSL.
Watching the handshake with curl
curl -v (see
curl and wget) shows the handshake, the negotiated version and the certificate
it checked. Lines starting with * are curl's own
commentary; the exact wording differs between curl versions.
$ curl -v -o /dev/null https://portal.example.com/
* Trying 203.0.113.10:443... * Connected to portal.example.com (203.0.113.10) port 443 * ALPN: curl offers h2,http/1.1 * TLSv1.3 (OUT), TLS handshake, Client hello (1): * TLSv1.3 (IN), TLS handshake, Server hello (2): * TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8): * TLSv1.3 (IN), TLS handshake, Certificate (11): * TLSv1.3 (IN), TLS handshake, CERT verify (15): * TLSv1.3 (IN), TLS handshake, Finished (20): * TLSv1.3 (OUT), TLS handshake, Finished (20): * SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / X25519 / id-ecPublicKey * ALPN: server accepted h2 * Server certificate: * subject: CN=portal.example.com * start date: Sep 1 00:00:00 2026 GMT * expire date: Mar 19 23:59:59 2027 GMT * subjectAltName: host "portal.example.com" matched cert's "portal.example.com" * issuer: C=US; O=Example CA; CN=Example Issuing CA R1 * SSL certificate verify ok. > GET / HTTP/2 > Host: portal.example.com ... < HTTP/2 200
The two failures you will meet most often look like this:
curl: (60) SSL certificate problem: unable to get local issuer certificate curl: (60) SSL: no alternative certificate subject name matches target host name 'example.com'
The first means the chain could not be completed to a trusted root (missing intermediate, internal CA, inspecting proxy); the second is a name mismatch.