It is the oldest interview question in web engineering, and the answer everybody links to is roughly a decade out of date. The canonical write-up on GitHub still describes the protocol decision as a choice between HTTP/1.1 and SPDY — a protocol Chrome removed in 2016 — and still describes the TLS handshake in terms that stopped being accurate when TLS 1.3 moved version negotiation into an extension. Almost everything downstream of it inherits those errors.

This page walks the path as it actually works in September 2026, in six stages, on a Linux machine. Nearly everything that has changed since 2023 sits in the middle of it, and one of those changes — how a browser decides whether a certificate has been revoked — is a complete replacement of the mechanism rather than an adjustment to it.

Two things you might expect are deliberately not here. Name resolution has its own page, and this one simply says what that lookup hands back. The packet’s journey through the kernel has its own page too, so ARP and routing and queueing are one sentence rather than a section — they happen identically for ssh and apt update and have nothing to do with typing a URL. And what a certificate proves is the introduction that stage 5 goes underneath.

Rendering is not here either, and that is a considered omission rather than an oversight. Parsing HTML, building the DOM and CSSOM, layout, paint, composite — it is identical on every operating system, nothing you can install or configure or measure on a Linux box changes it, and it is the section every other article on this subject already duplicates. What is Linux-specific is the process that does the rendering: how it is sandboxed, which security boundary your distribution has already broken, and which display server it is talking to. That gets a section of its own at the end.

The six stages, and one command that halves them

  1. Before you press Enter. The browser may already have fetched the page.
  2. The URL is parsed — by a state machine that deliberately disagrees with RFC 3986.
  3. The upgrades that happen before any packet. Four separate mechanisms can turn your http:// into https:// without touching the network.
  4. A connection is raced into existence. Not just IPv4 against IPv6 — protocols against each other.
  5. The handshake, and who you trust. The densest stage, and the one that has changed most.
  6. The exchange, and the dozen caches. A request can be answered from any of about twelve stores before a socket opens.

Now the split. Type http://example.com — without the s — with this running:

sudo tcpdump -n -i any 'tcp port 80'

Zero packets on port 80 means the upgrade to HTTPS was the browser’s own decision, taken in stage 3, before any network I/O at all. A GET / HTTP/1.1 followed by a 301 or 308 means it was the server’s decision, and it belongs to stage 6.

This is not a rule of thumb. HSTS is defined in RFC 6797 as a rewrite the user agent performs on the URI before the request is made, so it is incapable of producing a packet on port 80. A server redirect is incapable of existing without one. The two halves are distinguishable by construction, and no configuration can make them overlap.

One useful consequence straight away: if you see the redirect, then any Strict-Transport-Security header on that response is being set, not used. It will affect your next visit, not this one.

Stage 1 — Before you press Enter

Every article on this subject opens with the same sentence: you press Enter, and the browser now has a URL. For a meaningful share of navigations that sentence is false, because the fetch already happened while you were still typing.

Chrome’s omnibox sends what you type to the default search engine for suggestions, and the response can carry instructions back. An index in the suggestion payload marks a result for prefetch; another marks it for prerender. Prefetch issues the request immediately, tagged with a Purpose: Prefetch header so the origin can tell it apart from a real visit. Prerender goes further and builds the page in a hidden renderer process.

Two details about the prefetched response are worth holding on to, because they contradict things stage 6 will tell you about caching. It is held in memory for sixty seconds, and when it is served, it ignores the response’s cache headers entirely. It is not in the HTTP cache. It is not subject to Cache-Control. It is a separate store with its own rules and its own lifetime.

There is a long list of conditions that suppress it — speculative loading turned off, JavaScript disabled for the search domain, a non-default search engine, recent prefetch errors, too many recent prefetches that went unused, a non-2xx response, staleness. Which is a polite way of saying you cannot predict from the outside whether it happened. You can only observe it.

The other decision in this stage is whether what you typed is a search or a navigation. Broadly: a single word goes to the search engine, a string with dots in it is treated as a hostname, and there is a “did you mean” affordance when the browser guesses wrong. The exact disambiguation rules are not published in any current specification, so treat anyone who states them confidently — including yourself — with suspicion.

And the keystroke itself, since this is a Linux site: it comes off the keyboard as an evdev event, is processed by libinput, delivered to the compositor, and handed to the browser over wl_keyboard on Wayland or through XKB on X11. That is genuinely the path, and it is genuinely one sentence long, because nothing about it changes what you do next.

Stage 2 — The URL is parsed

The corpus describes this as splitting a string on :// and /. What actually runs is a state machine with roughly thirty states, defined by the WHATWG URL Standard — and the important thing about it is that it is not RFC 3986 and was never meant to be.

If you write “the URL is parsed according to RFC 3986,” you are wrong for every browser. The WHATWG parser was written to describe what browsers already did with real-world input, and it diverges from the RFC in several places that produce different results for the same string:

  • Backslashes are treated as path separators in special schemes, which the RFC does not do.
  • Trailing dots in hostnames are handled by the host parser rather than passed through.
  • Empty ports — a bare colon before the path — are normalised away.
  • Percent-encoding sets differ per component, so the same byte is escaped in a query and not in a path.

These are exactly the divergences that turn up in URL-parsing security bugs, where a proxy validates a string with one parser and a browser fetches it with the other. If you are writing anything that inspects URLs before a browser does, the two parsers not agreeing is the vulnerability class.

It is a Living Standard, so it has no version number to get wrong — which, given the rest of this article, is a mercy. The one modern addition worth naming is the non-throwing URL.parse() static method, which sits alongside URL.canParse() as the sanctioned way to test whether a string is a URL without wrapping the constructor in a try.

For international domain names, the correct reference is UTS #46 ToASCII, not “punycode”. Punycode is one step inside it, and describing it as the whole process skips the mapping, normalisation and validity checks that come first — which is where the interesting failures live. Note also that whether a browser displays the Unicode form or the xn-- form is a separate spoof-detection heuristic and not part of the URL standard at all.

Stage 3 — The upgrades that happen before any packet

You typed http://. The browser is going to use HTTPS. Every article says HSTS did that. In 2026, HSTS is the least likely of four mechanisms to be the one that fired.

Here they are in the order they are actually consulted:

  1. The preload list, compiled into the browser binary. Consulted before DNS.
  2. The profile’s dynamic HSTS store — hosts that have sent you a Strict-Transport-Security header before. Also before DNS.
  3. Unconditional upgrading. Chrome’s HTTPS-Upgrades reached all Stable users on 16 October 2023 and upgrades every main-frame navigation with a fast fallback to HTTP. Firefox shipped the equivalent, HTTPS-First, as a default for everyone in Firefox 136, on 4 March 2025; Mozilla measured it upgrading 57% of upgradable content. Also before DNS.
  4. An HTTPS DNS record. Since Chrome 102, the mere existence of one causes Chrome to connect over HTTPS, modelled internally as a 307. This is the only one of the four that happens after the lookup.

So what does HSTS still uniquely do? Two things, and they are worth stating precisely because they are what survive. It makes the upgrade mandatory — there is no silent fallback to plaintext when HTTPS fails — and it removes the click-through on certificate errors, so a user cannot proceed past a bad certificate. Neither of the unconditional mechanisms does either of those. “HSTS is how http:// becomes https://” is a much larger claim than the truth, and the truth is more useful.

The preload list is a build artefact

This surprises people who assume it updates like a blocklist. It does not. The list lives in transport_security_state_static.json in the Chromium source tree, is regenerated once per Chromium release just before the branch point, and is compiled into the binary. It is not a component update and it is not fetched.

The consequences are all downstream of that one fact, and the corpus states none of them:

  • Getting on the list takes months to reach users, because it takes a Chrome release to reach them.
  • Getting off it takes months too, and the site’s own documentation says removal cannot easily be undone and makes no guarantees about other browsers.
  • Entries added by bulk scanning can be pruned automatically. Manually submitted entries cannot — a human has to do it.
  • The maintainers’ stated constraint is download bandwidth: every byte added to the binary is multiplied by the number of Chrome installs on every future release.

The submission requirements, if you are considering it: a valid certificate; a redirect from HTTP to HTTPS on the same host if you listen on port 80 at all; every subdomain served over HTTPS, including www if that record exists; and on the base domain, a header carrying max-age of at least 31536000 together with includeSubDomains and preload. Given that removal is slow and manual, treat the decision as permanent.

The seam: what the name lookup hands back

Between stages 3 and 4 the name gets resolved, and that path has its own eight stages. What matters here is what comes back, because it is no longer just addresses.

A modern browser asks for A and AAAA records and the HTTPS record together. That third record is what makes the rest of this article different from the 2020 version of it, because it carries three parameters that each drive a different stage:

  • alpn — which protocols the origin speaks. Drives stage 4: with h3 in there, the first packet can be QUIC.
  • ipv4hint and ipv6hint — addresses usable if the A and AAAA answers are slow. Also stage 4.
  • ech — the key material for Encrypted Client Hello. Drives stage 5.
  • And its mere existence drives stage 3, per the upgrade mechanism above.

Which is why a resolver that strips the record, or a middlebox that mangles it, degrades three separate stages at once and looks like none of them.

Stage 4 — A connection is raced into existence

The classic answer says: open a TCP connection, three-way handshake, done. What actually happens is a race, and the entrants are no longer just IPv4 against IPv6.

Happy Eyeballs v2 is RFC 8305 and is the published standard: start the connection to the first address family, and if it has not answered within a short delay, start the other one and take whichever completes first. Version 3 is still a draft — the current revision is dated 2 July 2026 and is in IESG evaluation, so anyone citing an RFC number for it is citing one that does not exist.

What v3 adds is what has made this stage bigger than address families: the HTTPS record is queried alongside A and AAAA, candidate endpoints are prioritised by which protocols they advertise and whether ECH is available, and the address hints are used when the ordinary answers are slow. Modern connection racing races protocols.

The trap here is a well-known 2020-era fact that has expired. It used to be true that HTTP/3 required a prior TCP connection so the server could advertise Alt-Svc. With an HTTPS record carrying alpn="h3", it does not: the first packet on the wire can be a QUIC Initial and TCP is never touched. Firefox has used the record’s alpn since version 92, and Chrome uses it too.

The round trips, counted properly

PathRound trips before the first request byte
TCP + TLS 1.32 — one for the TCP handshake, one for TLS
TCP + TLS 1.23 — TLS 1.2 needs two, which is where the old figure comes from
QUIC + HTTP/31 — the crypto handshake is carried inside the transport handshake
“HTTPS costs you three round trips” is a TLS 1.2 statement. It has been wrong since TLS 1.3 shipped.

TCP Fast Open does not rescue the TCP column, and it is worth being clear about why: it never became the default path for browser traffic. Firefox removed its support in version 87 after problems with middleboxes and with TLS 1.3 interactions. Do not assume the other browsers kept it — the public documentation on that is not current enough to rely on.

There is a structural point buried in that table. For HTTP/3, stages 4 and 5 are the same round trip. QUIC carries the TLS 1.3 handshake inside its own Initial packets; there is no separate TLS exchange to count. Splitting them into two stages, as this article does, is a TCP artefact. It is a useful way to organise the material and a misleading way to picture the network.

Testing it, which requires a caveat first

curl -V | grep -o HTTP3          # nothing? your curl cannot do this
curl -sv --http1.1     https://example.com -o /dev/null
curl -sv --http3-only  https://example.com -o /dev/null

That first line matters more than it looks. “You have to build curl yourself for HTTP/3” was true in 2023 and is false now: Debian has shipped an HTTP/3-capable curl since Debian 13, with backports available for bookworm since August 2024. Run the command rather than reasoning about it, because upstream has been moving — curl 8.19.0 removed the OpenSSL-native QUIC backend entirely, on the grounds that the alternative was substantially faster and used a fraction of the memory, leaving ngtcp2 with nghttp3 as the non-experimental option.

The two curl lines then give you a second clean split. QUIC and TCP+TLS present the same certificate and run the same TLS 1.3 handshake; the only difference is the transport carrying it. So if --http1.1 succeeds and --http3-only fails, the fault cannot be the certificate, the trust store or the TLS layer. It is UDP/443 being blocked on the path, or the server not offering h3 at all.

On the server side, the fact that dates a 2023 article fastest: you no longer need a patched TLS library to serve HTTP/3. nginx has had QUIC since 1.25.0, it is in the official Linux binary packages, and OpenSSL 3.5.1 and later is a supported provider. Below 3.5.1 nginx falls back to a compatibility layer that does not support 0-RTT — so ssl_early_data on silently does nothing there, which is a fine example of a setting that appears to work. Apache httpd, meanwhile, still has no HTTP/3 at all: as of 2.4.68 it ships mod_h2 and nothing else, and any tutorial telling you to load mod_http3 is describing a module that does not exist.

Stage 5 — The handshake, and who you trust

This is the stage where a 2023-vintage answer falls apart completely, and it ends with a claim about your own machine that a great many Linux users have backwards.

Start with a detail that catches people reading packet captures: in TLS 1.3 the version is negotiated in the supported_versions extension, and the legacy version field in the ClientHello is pinned at 1.2 so that middleboxes do not choke. A capture showing “TLS 1.2” in the record header is not evidence of anything.

Post-quantum key exchange is now the majority case

A 2023 article files this under “coming soon”. By early December 2025, 52% of human-generated web traffic reaching Cloudflare was using post-quantum encryption, up from 29% at the start of that year. Those are Cloudflare’s own figures with their date attached; treat any 2026 number you see without a source as invented.

It has a practical consequence nobody predicted: post-quantum key shares are large enough that the ClientHello no longer fits in one packet on many paths. A first flight that spans two packets interacts badly with anything that was quietly assuming otherwise, which is a genuinely new source of connection failure.

Related, and neater than it sounds: TLS 1.2 is not deprecated — it is frozen, formally, as of RFC 9851 in July 2026. The working group has committed to approving no changes to it beyond urgent security fixes, new exporter labels and new ALPN identifiers. The consequence is sharper than deprecation would have been: post-quantum key exchange will never be standardised for TLS 1.2. It is not being switched off. It is being left behind.

Encrypted Client Hello is a real RFC now

ECH encrypts the server name so that an observer cannot see which site you are visiting from the handshake. It stopped being a draft on 3 March 2026, when it was published as RFC 9849, with its DNS bootstrap in RFC 9848. Anyone still calling it draft-ietf-tls-esni is a year behind.

Deployment is real but not a straight line — Cloudflare enabled it in September 2023, disabled it globally that November, and re-enabled it in stages; it is on by default for Free zones and opt-in elsewhere. And the two conditions that decide whether you get it are the ones people miss, because they are not about TLS at all:

  • Chrome will not use ECH if the resolver strips the ech parameter from the HTTPS record.
  • Firefox will not use it if the canary domain use-application-dns.net resolves, which is how a network signals that it wants to intercept DNS.

Which is to say: ECH is gated on getting an unmolested HTTPS record, which in practice means encrypted DNS. The privacy property in stage 5 depends on a decision made in a stage this article delegates entirely.

Revocation: the mechanism was replaced, not adjusted

This is the largest change in this article’s subject matter since 2023, and every stale source gets it wrong in the same way. The old sentence — the browser checks the certificate’s revocation status with OCSP, ideally via stapling — is now wrong in both halves, for both browsers.

WhenWhat happened
14 July 2023The CA/Browser Forum made OCSP optional for CAs and CRLs mandatory (ballot SC-063v4)
30 January 2025Let’s Encrypt began failing requests for OCSP Must-Staple certificates
7 May 2025Let’s Encrypt stopped putting OCSP URLs in certificates at all
6 August 2025Let’s Encrypt switched the OCSP responders off
Firefox 137CRLite replaced live OCSP checking
Firefox 142OCSP disabled entirely for domain-validated certificates
A corollary worth stating plainly: OCSP Must-Staple is effectively dead, and advice to enable it is now actively harmful.

What replaced it is more interesting than the cynical shrug the corpus offers. Firefox’s CRLite encodes the set of every certificate that appears as revoked in the Certificate Transparency logs, in a set-membership structure compact enough to ship: roughly 300 kB of revocation data per day, a full snapshot of about 4 MB every 45 days, deltas in between, updating every twelve hours. Mozilla’s claim is that it is three orders of magnitude more bandwidth-efficient than downloading CRLs would be. Chrome’s equivalent, CRLSets, is a smaller curated push.

The honest 2026 statement is therefore: revocation checking is a push model in both browsers — a set the browser already has when your handshake begins — not a query made during it. That is a different mechanism, not a degraded one, and getting the distinction right is most of what separates a current answer from a stale one.

Two more things that moved

Certificate Transparency is enforced by Firefox now. Since Firefox 135, on 4 February 2025, on desktop only, a certificate chaining to a root in Mozilla’s program must present two signed certificate timestamps or the connection fails with MOZILLA_PKIX_ERROR_INSUFFICIENT_CERTIFICATE_TRANSPARENCY. Certificates chaining to roots you added yourself are exempt — which is exactly right, since a corporate MITM root could not possibly log to a public CT log. “Only Chrome enforces CT” was true until early 2025 and is the kind of claim a careful writer checked once and never rechecked.

Certificate lifetimes are on a published shrinking schedule, and the first step has already happened. Under CA/Browser Forum ballot SC-081v3, adopted April 2025:

FromMaximum certificate lifetime
15 March 2026200 days — this is the limit today
15 March 2027100 days
15 March 202947 days
Domain-validation data reuse shrinks on the same steps, ending at ten days. “Certificates last a year” has been wrong since March.

If you are still renewing certificates by hand on any schedule, that table is the argument for stopping. The 47-day step makes manual renewal untenable, and it is dated.

And the part that is specifically about your machine

Here is that claim, and it is worth checking on your own box before you argue with it: Chrome and Firefox on Linux use the system CA store in /etc/ssl/certs. Neither of them does, and they are wrong in different ways.

Chrome has used its own root store on Linux since Chrome 114. The Chromium documentation is explicit that the certificate verifier does not rely on the platform’s default trust store. So running update-ca-certificates and watching it succeed tells you precisely nothing about whether Chrome will trust the CA you just added. What Chrome does read for locally-added trust is the NSS shared database — and the path for that moved: since M146 the default is $HOME/.local/share/pki/nssdb, with the old $HOME/.pki/nssdb used only if it already exists. Effectively every tutorial online still gives the old path.

Firefox does not use the operating system’s trust store on Linux at all, and cannot be made to with the preference everybody reaches for: security.enterprise_roots.enabled is a Windows and macOS feature and Mozilla’s own documentation lists no Linux support for it. The documented route on Linux is to load p11-kit-trust.so as a PKCS#11 security device.

Which is why the answer differs by distribution, and why there is no table of distributions here. Fedora wires libnssckbi.so through the alternatives system to p11-kit-trust.so, so Firefox on Fedora does see the system trust store. Debian has an open bug asking for the same thing and does not do it by default. Those two poles are documented by Fedora and by Mozilla respectively; everything between them would be guesswork, and a wrong table here is the sort of thing people screenshot. Check your own machine instead:

ls -l /etc/alternatives/libnssckbi.so
# points at p11-kit-trust.so   -> Firefox sees the system store
# points at the real libnssckbi -> it does not

One more version number, because this one has a date on it and requires action: OpenSSL 3.0’s long-term support ends on 7 September 2026. RHEL 9 and Ubuntu 22.04 LTS both ship it. Current OpenSSL is 4.0.2; the supported long-term release is 3.5, maintained to April 2030.

The split inside stage 5: same handshake, different trust anchors

openssl s_client -connect example.com:443 -servername example.com \
    -verify_return_error < /dev/null

openssl s_client and your browser run the same TLS 1.3 handshake over the same wire bytes, build the chain by the same rules, and match the hostname the same way. The only input that differs is the set of trust anchors. So a disagreement between them is not evidence about TLS at all — it is a measurement of a configuration difference, and the direction names the store.

  • Both fail — the fault is on the wire. Expired certificate, wrong name in the SAN, an incomplete chain being served, a protocol or group mismatch, SNI not being sent. Trust stores are irrelevant.
  • openssl passes, the browser fails — the certificate is valid against /etc/ssl/certs, so the certificate is fine. The browser is using a different set of roots. The usual cause is a root installed with update-ca-certificates, which Chrome has not read since 114.
  • The browser passes, openssl fails — the inverse. A root in the browser’s bundled store that is missing from, or distrusted in, the system store.

Add curl as a third arm to remove the last ambiguity: on most distributions it links against the system store, so curl agreeing with openssl and both disagreeing with the browser localises the fault to the browser’s own roots with nothing left to guess.

Stage 6 — The exchange, and the dozen caches

The request itself is the least changed part of this whole path, so it can be brief. Four corrections and one table.

Cite the right documents. HTTP is specified by RFC 9110 through 9114. The 723x series was obsoleted in June 2022, and RFC 2616 — which is still cited constantly — was obsoleted in 2014. If a page cites 2616 for anything, its HTTP content is at least twelve years old.

HTTP/2 server push is gone. Chrome 106 disabled it by default; the measured usage that justified the removal was under one per cent of HTTP/2 sites. The sanctioned replacements are the 103 Early Hints status code and <link rel=preload>. HTTP/3 never had push in browsers at all. It is in nearly every HTTP/2 explainer ever written, presented as a live feature.

HTTP/1.1 pipelining has been dead for a decade, and is likewise still in the corpus. On compression, Chrome ships zstd as a content encoding, and Compression Dictionary Transport has been standardised and shipped — but the syntax names changed during standardisation, so check MDN for the current tokens rather than any blog post about it, including this one.

On deployment share, one number, labelled: W3Techs put HTTP/3 at 40.3% of websites in September 2026. That is a site measure. Request-share measures are much lower and are dominated by a handful of very large properties. Anyone printing a site-share figure next to a request-share figure without saying which is which is averaging two incompatible surveys, and at least one widely-cited page does exactly that.

“The browser checks its cache” — which one?

Every article says the browser checks the cache, singular, before doing anything else. There are about a dozen stores that can answer a request before a socket opens, they have different keys and different lifetimes, and two of them are not caches at all but get confused for them.

StoreKeyed byWhat makes it distinct
HTTP disk cachePartitioned: network isolation key + URLNot the URL alone — see below
In-memory resource cachePer documentNot the disk cache
Omnibox prefetch storeSearch term60 seconds, ignores cache headers
Back/forward cacheHistory entryHolds a live, frozen document, not bytes
Service Worker CacheStorageScript-controlledIntercepts before the HTTP cache
Preload and prefetch cachesSpeculationSeparate again from both of the above
V8 code cacheScript URLCompiled bytecode, not response bytes
Host resolver cacheHostnameOne of several DNS caches on the path
Socket and connection poolOrigin, plus isolation keyReuse, not caching — but confused for it constantly
TLS PSK ticket storeServerResumption material
Alt-Svc / HTTP/3 storeOriginRemembers that an origin speaks h3
Dynamic HSTS storeHostNot a cache, but consulted at the same instant
Twelve stores, several of which can produce a complete response with no network activity whatsoever — and the last two of which are not caches at all.

Two rows deserve elaboration.

The HTTP cache is partitioned. Since Chrome 86 the key includes a network isolation key derived from the top-level site, and Firefox partitions network state similarly. Two different pages loading the same CDN URL do not share a cache entry. That change quietly killed the entire performance argument for putting a shared JavaScript library on a public CDN, which is still recommended in a great deal of writing.

A service worker intercepts before the HTTP cache. For any installed progressive web app, a page can be served with the network stack never consulted at all — no cache lookup, no resolution, no connection. Which means “check the cache, then do DNS” is not merely imprecise; it is the wrong order.

What the browser actually is on your machine

This is not stage 7. The six stages describe how a request becomes a response; this section describes the thing that did it, and it is the part of the story that is genuinely different on Linux.

pstree -p $(pgrep -o chrome)             # one browser, many processes
ls -l /proc/PID/ns                       # which namespaces it is in
grep Seccomp /proc/PID/status            # 2 means filter mode
echo $XDG_SESSION_TYPE                   # wayland or x11

A browser is not a process. Each renderer, the GPU process and the network service run separately, and the renderers are the ones that execute untrusted code, so they get confined. Chromium describes this in two layers: layer 1 restricts what the process can name and reach — historically a setuid helper, now normally a user namespace — and layer 2 reduces the kernel surface it can call, using seccomp-BPF.

Since M43, unprivileged user namespaces are used in preference to the setuid helper where the kernel allows it. The helper is still shipped, still works, and the documentation warns against deleting it — which brings us to a genuinely odd situation on the Linux desktop in 2026.

Ubuntu restricts unprivileged user namespaces by AppArmor policy, on the reasoning that they have been a productive source of kernel privilege escalations. Chromium’s sandbox wants exactly that capability. The result is that the browser falls back to the setuid binary — a mechanism from 2012 that its own documentation calls deprecated — and the visible symptom is an error message about a SUID sandbox helper that has generated bug reports continuously since Ubuntu 24.04. The two sysctls involved:

sysctl kernel.apparmor_restrict_unprivileged_userns
sysctl kernel.apparmor_restrict_unprivileged_unconfined

What makes this worth a paragraph rather than a bug report is the shape of the disagreement. A distribution’s security policy and a browser’s security architecture have reached opposite conclusions about whether unprivileged user namespaces are a hardening feature or a hole, both are defensible, and the user experiences the collision as a confusing error about a binary from over a decade ago. There is no version of this that exists on Windows or macOS.

On display: Chrome 140 changed the default from X11 to automatic detection, so on a Wayland session it now uses Wayland natively rather than going through XWayland. The motivating bug was blurry text under fractional scaling — XWayland rendered at an integer scale and the compositor stretched the result. If you have an --ozone-platform-hint=auto flag in a desktop file or a wrapper script somewhere, it is now redundant.

Two internal pages verify the rest: chrome://sandbox lists the renderer processes and the confinement actually applied to each, and chrome://gpu reports what the GPU process negotiated.

Advice that has expired

This question is one of the most search-optimised on the internet, which means the stale answers rank above the primary sources and get copied into new ones. Every row below is a claim that was true when someone first wrote it down.

What you will still readWhat is true now
“The browser checks revocation with OCSP”Let’s Encrypt switched its responders off in August 2025; Firefox replaced OCSP with CRLite in 137 and disabled it for DV certificates in 142
“Enable OCSP Must-Staple”Effectively dead. Let’s Encrypt began failing those requests in January 2025
“Revocation checking doesn’t really work”It is a push model now — a set the browser already holds. CRLite ships about 300 kB a day and updates every twelve hours
“HSTS is what upgrades your http:// to https://”The least likely of four mechanisms. Chrome upgrades everything since October 2023; Firefox since 136
“The preload list is downloaded and updated”Compiled into the binary, regenerated once per release. Months on, months off
“HTTP/2 server push sends resources before you ask”Disabled by default in Chrome 106. Use 103 Early Hints
“The browser checks its cache”About a dozen stores, partitioned since Chrome 86, and a service worker gets there first
“Session resumption gives you 0-RTT”Two different things. TLS 1.3 resumption is still 1-RTT; only early data saves the trip, and it is replayable
“TLS 1.3 uses session IDs or session tickets”Neither exists in 1.3. Resumption is a PSK from a NewSessionTicket sent after the handshake, intended for single use
“HTTPS costs three round trips”Two with TLS 1.3 over TCP, one with QUIC. Three is the TLS 1.2 figure
“HTTP/3 needs Alt-Svc from a prior TCP connection”Not with alpn="h3" in the HTTPS record. The first packet can be QUIC
“You need a patched OpenSSL fork for HTTP/3”nginx supports OpenSSL 3.5.1+ directly, since 1.25.0, in the official packages
“Build curl yourself for HTTP/3”Debian has shipped it since Debian 13. Run curl -V
“Load mod_http3 for HTTP/3 on Apache”No such module exists. httpd 2.4.68 ships mod_h2 and nothing else
“Chrome and Firefox use /etc/ssl/certsChrome uses its own root store since 114; Firefox never used the OS store on Linux
“Set security.enterprise_roots.enabled in Firefox”Windows and macOS only. On Linux, load p11-kit-trust.so
“Locally-trusted certificates go in ~/.pki/nssdbSince M146 the default is ~/.local/share/pki/nssdb
“Only Chrome enforces Certificate Transparency”Firefox enforces it on desktop since 135, requiring two SCTs
“Public certificates last a year”200 days since March 2026; 100 days from March 2027; 47 from March 2029
“TLS 1.2 has been deprecated”Frozen, not deprecated — RFC 9851. Which means no post-quantum key exchange, ever
“ECH is still a draft”RFC 9849, March 2026, with its DNS bootstrap in RFC 9848
“Chrome on Linux uses XWayland”Chrome 140 changed the default to automatic detection
Anything citing RFC 2616Obsoleted in 2014. HTTP is RFC 9110–9114
The canonical GitHub answer to this question fails several of these and still treats SPDY as the alternative to HTTP/1.1.

A worked diagnosis

An internal service is served with a certificate from the company’s own CA. The root was installed on every workstation by configuration management, using the documented, correct command. It works from the command line. Chrome shows NET::ERR_CERT_AUTHORITY_INVALID. The reporter’s conclusion is that Chrome is broken.

$ curl -sI https://internal.example.com | head -1
HTTP/2 200

$ openssl s_client -connect internal.example.com:443 \
      -servername internal.example.com -verify_return_error < /dev/null 2>&1 \
  | grep -E 'Verify return code|Protocol'
    Protocol  : TLSv1.3
    Verify return code: 0 (ok)

Two of the three agree, and they are the two that read /etc/ssl/certs. By the split earlier in stage 5, that settles it: the handshake is fine, the chain is fine, the hostname matches, the certificate is valid. The only remaining variable is the trust anchor set, and the direction of the disagreement names the store — the browser’s own.

Which is not a bug. Chrome has used the Chrome Root Store on Linux since version 114, and its verifier does not consult the platform trust store at all. update-ca-certificates succeeded, updated the file it was supposed to update, and was never going to affect Chrome. The fix is to put the root where Chrome does look:

# Chrome reads the NSS shared DB. Note the path, which moved in M146.
certutil -d sql:$HOME/.local/share/pki/nssdb \
         -A -t "C,," -n "Example Corp Root" \
         -i /usr/local/share/ca-certificates/corp.crt

certutil -d sql:$HOME/.local/share/pki/nssdb -L

And then the part that makes this a Linux problem rather than a Chrome one. Firefox on the same machine is a third answer. It does not read /etc/ssl/certs and it does not read Chrome’s database. On Fedora it will work anyway, because Fedora points libnssckbi.so at p11-kit-trust.so and Firefox picks up the system store through the back door. On Debian it will not, because Debian does not.

So: one machine, one certificate, one correctly-executed installation command, and three programs with three different answers to “do I trust this” — only one of which is affected by the file everyone edits. The moral is not that any of them is wrong. It is that “the system trust store” has not been a single thing on Linux for years, and the command that appears to configure it configures the smallest share of what you actually use.

What to hold on to

Most of the interesting decisions happen before a packet exists. The upgrade to HTTPS, whether the response comes from one of a dozen local stores, and whether the page was already fetched while you were still typing — all of that is settled before a connection is attempted. The tcpdump in the box at the top is how you find out which side of that line you are on.

When two tools disagree about a certificate, the disagreement is the measurement. openssl s_client, curl and the browser run the same handshake against the same bytes and differ only in which roots they trust. Which two agree tells you which store is misconfigured, with nothing left to guess.

The half-life of an answer to this question is about two years. Revocation, the upgrade path, server push, cache partitioning, certificate lifetimes and the Linux trust store have all changed since 2023, and the canonical answer is stale by a decade. Check the date on anything you read about this, including this page.

Related reading