· Paul Crossland
The Certificate You Did Not Send Still Belongs in the Connection Key
Fresh Playwright and curl changes show why explicit no-certificate policy must survive origin routing, TLS interception, and connection reuse.
A browser worker is configured not to present a client certificate to one origin. The page can still open a native certificate picker. An HTTP worker can disable automatic certificate selection for one request, then reuse a TLS connection created under a different setting. Neither failure begins with an HTTP status because the identity decision happens during the TLS handshake.
The practical thesis is: client-certificate policy, including the explicit choice to send none, must be bound to an exact origin and included in connection-pool identity, then verified at the handshake boundary before a fetch artifact is trusted. An empty certificate list, a request option, or a final 401 cannot prove that chain by itself.
This is defensive reliability guidance for systems using certificates they are authorized to use. Do not copy certificates between customers, origins, or tasks; suppressing a prompt does not authorize access; and a certificate requirement, login, denial, challenge, or rate limit remains a stop or review outcome rather than a cue to try another identity.
Fresh evidence from two TLS control paths
| Primary source | Date | What it contributes |
|---|---|---|
| Playwright issue 42546 | 2026-09-03 | Reproduces a regression in Playwright 1.62.1 where the documented non-matching-origin workaround no longer forces the client-certificate proxy path, so the browser can receive the server's certificate request instead of explicitly sending no certificate. |
| Playwright pull request 42547 | 2026-09-03 | Proposes an origin-scoped sendNone option, rejects mixing it with real certificate material, and tests both API-request and browser paths against a server that requires a client certificate. |
| curl pull request 22827 | 2026-09-04 | Moves Schannel's automatic-client-certificate flag into curl's primary connection SSL configuration and compares it during connection matching, so connections with different certificate-selection policy are not treated as interchangeable. |
Both pull requests were open at publication time; the curl change had an approving review. They are evidence for testing the exact deployed build, not proof that released packages already contain the behavior.
Recent Better Fetch posts already cover authentication boundaries, principal handoffs, TLS diagnostics, browser-state checkpoints, and cookie admission. Repeating “credentials are state” would add little. The new information surplus is the negative credential contract: “send no certificate” is not missing configuration. It is an active origin-specific identity decision that must survive wrapper routing and connection reuse before HTTP begins.
No certificate is a handshake outcome
In TLS client authentication, the server can request a certificate while establishing the connection. The client may present an eligible certificate or continue without one, after which the server decides whether the connection or application request is acceptable. Native browser behavior may also ask a person to choose from certificates available to the operating system.
Those outcomes are materially different:
not_configured: the fetch layer declared no client-certificate policy;not_requested: the peer did not request a certificate on this handshake;requested_none: the peer requested one and the configured client deliberately presented none;certificate_selected: one approved certificate reference was selected;native_prompt_possible: the browser or operating system retained selection authority;selection_failed: policy expected a certificate but no eligible credential could be used;handshake_unknown: instrumentation cannot establish what occurred.
A 401 from an owned fixture can be useful application evidence that no accepted client identity arrived. It still does not distinguish every earlier path. Conversely, a 200 does not prove a certificate was used; the route may not require one, a gateway may authenticate differently, or a reused connection may already carry the relevant TLS state.
Origin omission and explicit denial are different
The Playwright report exposes a routing distinction. Playwright implements configured browser client certificates through an interception path. An optimization stopped activating that path for origins absent from the certificate map. The old workaround—configure a certificate for some unrelated origin so visited origins effectively send none—therefore stopped forcing the intended behavior. On affected systems, the server's certificate request could reach the browser and trigger native selection UI.
The proposed sendNone entry makes the negative choice explicit for the visited origin. Its tests require that sendNone cannot coexist with certificate or key material for that origin. Browser tests also verify that the TLS interception path was actually used and that the application still observed no client certificate.
The production lesson is broader than one API. Configuration systems frequently collapse these states:
- no rule exists for the origin;
- a rule exists and forbids presenting a certificate;
- a rule exists but its credential is unavailable;
- a rule exists and selects one certificate.
Do not represent all four as an empty array. Compile a destination decision using normalized scheme, hostname, and port before connection admission. Return the matched rule ID, policy generation, and one explicit outcome. Redirects require a new decision for every origin; certificate authority must never follow a URL merely because the navigation did.
Connection reuse can preserve an earlier decision
Client authentication belongs to the TLS connection, not independently to every HTTP request carried over it. A request option can therefore arrive too late if the pool supplies an existing connection whose handshake occurred under another certificate-selection policy.
That is why the curl change matters. On Windows Schannel, CURLSSLOPT_AUTO_CLIENT_CERT controls automatic location and use of a client certificate. Pull request 22827 moves that flag into the primary SSL configuration, clones it with that configuration, and compares it during connection matching. The code change is small because the missing distinction sits at a high-leverage boundary: whether a live connection is eligible for reuse.
Treat the pool key as a security and provenance key, not only a performance key. It should account for every setting capable of changing peer or client identity, including the normalized origin, proxy and tunnel identity, trust policy, client-certificate mode, approved certificate reference and generation where applicable, and runtime-specific automatic-selection policy. The exact fields depend on the stack; the invariant is that two requests with different identity policy cannot share a connection merely because host and port match.
Do not “fix” uncertainty by disabling pooling everywhere. Fresh connections increase handshake cost, latency, and origin load. Partition pools by policy, retire affected connections when policy or credential generation changes, and preserve reuse when equivalence is proven.
Issue a client-identity receipt
Attach a metadata-only receipt to each connection and request:
- task, tenant, principal segment, request, redirect-hop, and connection-generation IDs;
- requested URL origin, normalized certificate-policy origin, matched rule ID, and policy generation;
- client mode:
none_explicit,certificate_explicit,automatic_allowed,automatic_forbidden, orunconfigured; - certificate reference and rotation generation when selected, never private keys or broadly logged certificate contents;
- browser direct-versus-intercepted path, proxy generation, native-prompt suppression expectation, and actual prompt signal when observable;
- runtime, TLS backend, browser or HTTP-client version, operating system, and worker image;
- connection candidate ID, fresh or reused decision, pool-key version, and policy-equivalence verdict;
- whether the peer requested a certificate, selection outcome, TLS protocol, handshake result, and application status;
- final verdict such as
identity_attested,explicit_none_attested,pool_policy_mismatch,native_prompt_possible,certificate_unavailable,policy_stop, orunknown.
Certificate subjects, serial numbers, URLs, and account context can be sensitive. Prefer stable restricted references and short retention. Store private keys only in the credential system responsible for release, never in crawl logs, traces, or test fixtures committed to source control.
An owned mTLS fixture matrix
Test the deployed browser, wrapper, HTTP client, TLS backend, and pool together:
- Run owned endpoints that request no certificate, request one optionally, and require one. Assert distinct handshake and application verdicts.
- For one exact origin, test explicit none, one approved synthetic certificate, automatic selection disabled, and deliberately absent policy. Do not accept an empty config as proof of explicit none.
- Put two owned origins on different ports and redirect between them. Require a fresh origin-policy decision at each hop and prohibit certificate inheritance through the redirect.
- Warm a connection under each client mode, then issue a request under every other mode. Require reuse only when the complete identity policy matches.
- Rotate the synthetic certificate and policy generation while keep-alive connections exist. Verify that stale connections become ineligible rather than carrying the old identity into new work.
- Exercise headed and headless browser paths on every supported operating system. An unexpected native picker should fail admission, not wait until a generic navigation timeout.
- Compare the current and candidate Playwright, curl, TLS-backend, and base-image builds. Record whether the relevant upstream changes are actually present instead of inferring behavior from a repository merge.
- Return certificate-required, login, denial, challenge, and
429responses. Preserve the boundary and prohibit retries with broader credential scope, another customer's certificate, or automatic selection enabled as a fallback.
The operator decision rule
When a certificate-capable fetch prompts, hangs, or changes representation, locate the first unsupported boundary. If no exact-origin rule matched, classify the run as unconfigured rather than claiming explicit none. If the rule matched but the browser bypassed the intended interception path, quarantine that client configuration. If the handshake policy was correct but a non-equivalent connection was reused, repair the pool key and retire affected connections. If an approved certificate was selected and the source still denied access, preserve the denial; do not broaden identity or retry scope.
A certificate is not merely another request header, and its absence is not merely an empty field. Both are TLS identity outcomes with origin, routing, connection, and policy lineage. Make the negative choice explicit and make connection reuse prove equivalence, and client-auth incidents become bounded transport diagnoses instead of native prompts, accidental credential carryover, or misleading HTTP failures.