diff --git a/relay-run/src/main-phase1.ts b/relay-run/src/main-phase1.ts index f5ca366..ad02edc 100644 --- a/relay-run/src/main-phase1.ts +++ b/relay-run/src/main-phase1.ts @@ -205,7 +205,11 @@ async function main(): Promise { resolver, mtls: mtlsBridge.sync, now, - caBundle: [readFileSync(agentCaCertPath)], + // Agent-TLS trust store for validating the agent's CLIENT leaf (requestCert+rejectUnauthorized). + // MUST be the FULL chain (intermediate + self-signed root): Node/OpenSSL rejects a leaf whose only + // anchor is a non-self-signed intermediate (no PARTIAL_CHAIN flag). Intermediate-only ⇒ every agent + // is reset at the TLS layer before attach() runs. + caBundle: [readFileSync(agentCaChainPath)], onError: (e) => console.error('[data-plane]', errText(e)), tlsServerFactory: mtlsBridge.wrap(agentTlsFactory), }) diff --git a/relay-run/src/servers/agent-tls.ts b/relay-run/src/servers/agent-tls.ts index e8ba6da..3d0b833 100644 --- a/relay-run/src/servers/agent-tls.ts +++ b/relay-run/src/servers/agent-tls.ts @@ -37,6 +37,11 @@ export function makeAgentTlsServerFactory(opts: AgentTlsOptions): TlsServerFacto const der = peer && peer.raw ? new Uint8Array(peer.raw) : new Uint8Array() onPeer(wsToWebSocketLike(ws), der) }) + wss.on('error', (e) => opts.onError?.(e)) + // Surface TLS-layer client failures (e.g. a client leaf that doesn't chain to `ca`). Node + // otherwise SWALLOWS 'tlsClientError' — the peer just sees an abrupt reset with no server signal, + // which is exactly what masked the intermediate-only-CA bug. + server.on('tlsClientError', (e) => opts.onError?.(e)) server.on('error', (e) => opts.onError?.(e)) server.listen(opts.bindPort, opts.bindHost, () => opts.onListening?.()) return {