Decode PROXY protocol v2 with forwarded mTLS client certificates on UDS mirrors#1858
Decode PROXY protocol v2 with forwarded mTLS client certificates on UDS mirrors#1858kriszyp wants to merge 4 commits into
Conversation
|
Reviewed; no blockers found. |
There was a problem hiding this comment.
Code Review
This pull request adds support for decoding PROXY protocol v2 (binary) headers, enabling plaintext Unix Domain Socket (UDS) mirrors to receive forwarded client source addresses and mTLS client certificate chains from a fronting proxy. The changes mock TLSSocket semantics on plaintext sockets so that downstream HTTP and MQTT mTLS authentication paths work seamlessly. The review feedback identifies critical security gaps where the mtlsRequired configuration is not mirrored to the HTTP/1 and HTTP/2 UDS servers, which could lead to security bypasses. Additionally, the feedback points out that the mocked getPeerCertificate implementation should respect Node's native detailed parameter, and notes a minor typo in the design documentation.
|
Two follow-up commits since the initial reviews:
🤖 Claude Fable 5, on Kris's behalf |
…DS mirrors
Symphony (the fronting TLS proxy) can now terminate and verify mTLS and
forward the client certificate chain via PROXY v2 TLVs (SSL TLV 0x20 +
custom 0xE0, one DER per cert, leaf first). Harper's UDS mirrors only
spoke PROXY v1, so that identity was invisible behind termination.
- New server/serverHelpers/proxyProtocol.ts: v1+v2 decoder shared by
enableProxyProtocol and createH2CProxyFront (previously two inline v1
parsers), plus synthesis of a Node getPeerCertificate(true)-shaped
object from the forwarded DER chain (issuerCertificate links wired for
certificateVerification's chain walk and OCSP/CRL checks).
- Forwarded verified chains get TLSSocket semantics on the plaintext
socket (authorized, getPeerCertificate) so HTTP auth (security/auth.ts)
and MQTT mTLS work unchanged; sockets without a forwarded cert get
no-client-cert TLS defaults ({} / authorized=false).
- UDS mirror servers mirror the secure server's mtlsConfig; the metadata
yaml advertises mtls/mtlsRequired so the proxy can request client certs
(suppressed for the uWS mirror, which cannot decode PROXY headers).
- IPv6 source addresses are compressed to match net.Socket remoteAddress.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ificate(detailed), DESIGN.md typo Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… to 0xE2 MQTT (the primary mTLS consumer) reads socket.authorized and getPeerCertificate at connection time, but enableProxyProtocol only applies the forwarded identity when the first data event fires — after the listener has already checked. New withProxyProtocol(listener) consumes the header pre-handoff (shared consumeProxyHeader loop, also now used by createH2CProxyFront) and the threadServer raw-socket UDS mirror uses it instead of data-path interception. Cert chain TLV moves 0xE0 -> 0xE2 to coexist with symphony PR #23's allocation (0xE0 = JA3, 0xE1 = JA4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| | `onRequest()` | Thin alias of `httpServer({requestOnly: true})`. | | ||
| | `onUpgrade()` / `upgradeListeners` (const) | Register HTTP upgrade listener; underlying list. | | ||
| | `onWebSocket()` / `websocketListeners` (const) | Register WebSocket listener; auto-adds default upgrade handler the first time it runs for a port. Underlying list of registrations. | | ||
| | `enableProxyProtocol()` | PROXY v1/v2 stripping on UDS mirrors (Node 24+-compatible workaround). Decoding lives in `serverHelpers/proxyProtocol.ts`; v2 TLVs forward the client source address and the mTLS client cert chain that a fronting proxy (symphony) verified — exposed with TLSSocket semantics (`authorized`, `getPeerCertificate()`) so HTTP/MQTT mTLS auth works unchanged. | |
There was a problem hiding this comment.
Suggestion (non-blocking): This row still attributes forwarded mTLS identity for "HTTP/MQTT" to enableProxyProtocol(), but this PR's own threadServer.js change moves MQTT (and other raw-protocol UDS mirrors) off enableProxyProtocol onto the new withProxyProtocol()/consumeProxyHeader() pre-handoff path precisely because the data-interception approach applies identity too late for MQTT. Worth updating this line (and/or adding a withProxyProtocol() row) so the design map doesn't point future readers at the wrong function for MQTT's identity forwarding.
Summary
Symphony can now terminate and verify mTLS and forward the client certificate chain over PROXY protocol v2 TLVs (symphony#27). Harper's per-worker UDS mirrors only understood PROXY v1 text headers, so client identity was invisible behind the proxy's TLS termination. This PR is the consuming half:
server/serverHelpers/proxyProtocol.ts— a shared v1+v2 decoder replacing the two previously-inline v1 parsers (enableProxyProtocolandcreateH2CProxyFrontinserver/http.ts). v2 carries the client source address plus, when symphony verified an mTLS client cert, the DER chain (SSL TLV0x20+ one0xE0TLV per cert, leaf first).socket.authorizedand a lazy, memoizedsocket.getPeerCertificate()(built withcrypto.X509Certificate,issuerCertificatelinks wired socertificateVerification's chain walk and OCSP/CRL checks work unchanged). Existing HTTP (security/auth.ts) and MQTT mTLS auth paths need zero changes. Sockets with no forwarded cert get no-client-cert TLS defaults (getPeerCertificate() → {},authorized: false).mtlsConfig/verifiesClientCerts, and the raw-socket mirror path setsmtlsRequired(threadServer.js).mtls: true/mtlsRequired: trueso host-manager can configure symphony to request client certs and forward them (suppressed for the uWS mirror, which can't decode PROXY headers and uses X-Forwarded-For).Where to look
verify==0from the proxy should map directly tosocket.authorized = trueis the key design judgment.Request.peerCertificateon UDS-fronted requests now returns{}(TLS no-cert semantics) instead ofnull— this is what makesauth.ts'srequest.peerCertificate.subjectguards safe oncemtlsConfigexists on UDS servers.createH2CProxyFront(pre-handoff readable loop) — existing unit tests all pass, but it's the riskiest surface of the refactor.Open items from cross-model review
mtlsRequired, and the uWS mirror would have advertised forwarding it can't decode. Both addressed; worth a second look at the uWS gating (!process.env.HARPER_UWS_UDSat thewriteMetadatacall site).{}for the empty-cert return; kept per-call allocation deliberately (a shared object could leak mutations across connections — Node also returns a fresh object).Testing
unitTests/server/serverHelpers/proxyProtocol.test.js+ extensions toudsMirror.test.js): v1/v2 decode incl. split-across-chunks buffering, malformed TLVs, LOCAL command, IPv6 compression, cert-chain synthesis consumed byextractCertificateChain, yaml flags. All 53 tests in the touched suites pass; fulltest:unit:mainfails identically on clean main locally (env issue) — relying on CI.Generated by an LLM (Claude Fable 5) pairing with Kris.
🤖 Generated with Claude Code