fix(tls): honor ciphers/SECLEVEL from every configured source when building TLS listeners#1841
Conversation
…ilding TLS listeners A TLS listener has exactly one effective cipher string: OpenSSL takes the cipher list (and any @SECLEVEL, which governs client-cert chain verification) from the context the server was created with; SNI-swapped contexts don't carry their own cipher list onto the connection. Harper applied only tls.ciphers ?? tls[0].ciphers and silently ignored every other configured value — tls[] entries beyond [0] and certificate records, including client-CA records carrying DEFAULT@SECLEVEL=0 for SHA-1-signed chains, which then failed with authorizationError UNSPECIFIED on valid in-date certs. resolveEffectiveTlsCiphers (security/keys.ts) now resolves the listener string from all sources: top-level tls.ciphers wins; otherwise tls[] entries plus relevant cert records (uses-matched, and authorities when the listener verifies client certs) are candidates, with the lowest explicit @SECLEVEL winning conflicts and everything ignored logged. Post-boot changes to the resolved value warn (once per value) that a restart is required. Bun path untouched (BoringSSL has no @SECLEVEL). Closes #1840 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request improves TLS cipher resolution by ensuring that the single effective cipher string (and its @SECLEVEL) governing a TLS listener is correctly resolved from all configured sources, rather than only checking the first entry. It updates the HTTP and socket servers to use this resolved configuration and adds corresponding unit and integration tests. The review feedback suggests guarding the cleanup step in the integration test's after hook to prevent a secondary TypeError if the setup fails early.
This comment has been minimized.
This comment has been minimized.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
heskew
left a comment
There was a problem hiding this comment.
The ignored ciphers behavior is real, and the base/head regression is convincing. The listener wiring, restart warning, teardown guard, and cold-path performance look sound. I think the resolution policy needs the inline issues addressed before merge so a CA cannot relax unrelated listeners or replace the server's cipher suites, and listener-specific values are not still silently ignored.
🤖 Reviewed by OpenAI Codex.
…picking one cipher string Addresses the external review on #1841: config array entries are now relevance-filtered like certificate records (CA entries only when the listener verifies client certs; uses matched with the selector's tolerant rule incl. legacy 'https' and no-uses generics), the suite list is preserved from the highest-priority suite-bearing candidate with only the minimum explicit @SECLEVEL composed on (no assumed runtime default level), and the operations API listener resolves from operationsApi.tls before root tls so an inherited-certificate override is no longer ignored. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@kriszyp forward-looking note for the edge-termination roadmap (no action needed on this PR): if public mTLS ever moves to symphony's edge termination (h2/h3 routes), the legacy-client-CA problem this PR fixes moves with it — client chains signed with weak digests (e.g. SHA-1-signed CAs) need a 🤖 posted by Claude Code on Nathan's behalf |
|
Docs follow-up (promised in the description): HarperFast/documentation#598 — multi-source cipher/SECLEVEL resolution reference. 🤖 posted by Claude Code on Nathan's behalf |
Looking into this, RusTLS flatly refuses and will not support insecure ciphers (SECLEVEL=0), so for users that need it, Symphony won't terminate (t will forward TLS on to Harper). |
Summary
A TLS listener has exactly one effective cipher string: OpenSSL takes the cipher list — and any
@SECLEVEL=nembedded in it, which governs client-certificate chain verification — from the context the server was created with, and an SNI-swapped context does not carry its own cipher list onto the connection. Harper applied onlytls.ciphers ?? tls[0].ciphersand silently ignored every other configuredciphersvalue: entries beyondtls[0]and certificate records (authority records'cipherswere never read at all).This resolves the listener cipher string from every configured source (
resolveEffectiveTlsCiphersinsecurity/keys.ts), wired into both Node listener-creation sites (server/http.ts,server/threads/threadServer.js):operationsApi.tlsbefore roottlsfor the operations listener; array entries relevance-filtered — CA entries only when the listener verifies client certs, served entries matched byuseswith the selector's tolerant rule incl. legacy'https'and no-usesgenerics) and from relevant certificate records.DEFAULT@SECLEVEL=0(SHA-1-signed chains, elseauthorizationError: UNSPECIFIED) must not replace or broaden the listener's configured suites.@SECLEVELacross candidates; strings without one keep the runtime default (no level is assumed — the OpenSSL default varies across Node builds). The two compose, e.g.HIGH:!aNULL+ CADEFAULT@SECLEVEL=0→HIGH:!aNULL@SECLEVEL=0, and any cross-source composition or dropped suite list is logged.updateTLSwarns (once per value) that a restart is required — the listener-level string can't be swapped by an SNI-context rebuild.Closes #1840.
Purpose
An mTLS deployment whose client certs chain through SHA-1-signed CAs configured
ciphers: DEFAULT@SECLEVEL=0on the CA entry — the natural, least-privilege place.list_certificatesshowed the override present while the verify path rejected every client. The only working escape hatch (top-leveltls.ciphers) relaxes all listeners and is easy to lose in platform config reconciliation. (Field incident on 4.7; 5.x has the same shape.)Where to look
resolveEffectiveTlsCiphers) is the policy decision: preserve the configured suites, apply only the minimum explicit@SECLEVELany relevant cert requires, warn on everything composed or dropped. This follows the external (Codex) review on the first cut, which correctly objected to whole-string replacement and to an assumed default security level.SECLEVELmatters exactly when its chains are being verified; a non-mTLS listener is not relaxed.server.appliedCiphers/server.verifiesClientCertsstamps at both creation sites feed the divergence warning inupdateTLS; the Bun path is untouched (BoringSSL has no@SECLEVEL).Tests
unitTests/security/keys.test.js): 17 cases over the pure resolver — layer precedence (incl.operationsApi.tls), array entries beyond[0], CA-entry mTLS gating, authority/uses/legacy-httpsrelevance, scalar-usesnormalization, suite+minimum-SECLEVELcomposition, no-assumed-level, bare-@SECLEVELanchoring.integrationTests/security/tls-ciphers-seclevel.test.ts): boots Harper withtlsas an array whose second entry is a SHA-1-signed client CA carryingciphers: DEFAULT@SECLEVEL=0(the previously-ignored position) andhttp.mtls.required; asserts a client cert chained through that CA completes the handshake and an unchained cert is refused. Verified the test fails on unfixed code (the SHA-1 chain is refused — the incident) and passes with the fix.Local runs:
test:unit:maingreen except two consistently-failing-locally, unrelated cases also failing without this change (globalIsolation "should enforce process spawning restrictions",uwsServer "rejects a body over maxBodyBytes with 413"EPIPE) and an intermittent macOS-localEMFILEteardown flake inkeys.test.jstied to system fd pressure, not this diff. CI should arbitrate.Cross-model review
Thorough pass (Gemini leg + Harper-domain adjudication): no blockers, no significant concerns. Gemini's one substantive finding (an
isMtls/mtlsasymmetry that could fire a spurious restart warning) was fixed via theverifiesClientCertsstamps before this PR was opened; the repeat-warning and Bun-comment suggestions are folded in. Caveat: the Codex leg failed to complete (CLI exited during context collection), so there is no Codex coverage on this run. A follow-up external (Codex) review raised five findings on the first-cut resolution policy (whole-string replacement, assumed default level, unfiltered tls[] entries, operationsApi.tls gap, relevance drift from the selector); all five are addressed by the composed model in 773b42b — see the resolved threads.Docs
reference/http/tls.mdin HarperFast/documentation documentstls.ciphersas a single string only; a companion docs PR describing the multi-source resolution (and the restart-required caveat) will follow once the resolution policy here survives review.🤖 Generated with Claude Code (Claude Fable 5)