Skip to content

feat: root-CA + per-host leaf certs for AgentBridge static server (#6684)#7731

Merged
diegosouzapw merged 2 commits into
release/v3.8.49from
feat/6684-mitm-root-ca
Jul 19, 2026
Merged

feat: root-CA + per-host leaf certs for AgentBridge static server (#6684)#7731
diegosouzapw merged 2 commits into
release/v3.8.49from
feat/6684-mitm-root-ca

Conversation

@diegosouzapw

Copy link
Copy Markdown
Owner

Summary

Replaces the AgentBridge static MITM server's single self-signed leaf cert (src/mitm/cert/generate.ts, scoped only to the 4 antigravity hosts — see #6494) with a persisted local root CA + per-SNI leaf certs, covering the full MITM_TOOL_HOSTS set (9 tool entries). Reuses the CA/leaf crypto already proven for the TPROXY capture mode (src/mitm/tproxy/dynamicCert.ts) rather than writing new signing logic.

  • src/mitm/cert/rootCa.ts (new) — loadOrCreateMitmCa(): loads a persisted ca.key/ca.crt pair if present, otherwise generates once and writes both, chmod'ing the private key 0o600.
  • src/mitm/cert/migration.ts (new) — pure migration-gate function. An install that already trusted the old static leaf (server.crt/server.key present, no CA pair yet) stays on the legacy leaf this run; a fresh install (no legacy leaf) gets the CA model automatically. Existing trusted installs opt in explicitly via MITM_ROOT_CA_ENABLED=true. A CA that can sign a leaf for any host is materially more powerful than the old fixed-SAN leaf, so the switch is never silent for an already-trusted install.
  • src/mitm/cert/install.tsinstallCaCert(), a thin named wrapper over the existing installCertResult() (already cert-path-agnostic). Installs under the same omniroute-mitm.crt trust-store slot the old leaf used — the CA simply supersedes it, no dual-trust cleanup needed. Distinct from TPROXY's own omniroute-tproxy-ca.crt slot, untouched.
  • src/mitm/manager.ts — wires the migration gate + CA load/install into the bridge-start sequence, preserving the existing best-effort/non-aborting error-handling shape ([BUG] Agent Bridge cannot start in Docker container #4546). Passes the resolved MITM_CERT_MODE to the spawned server.cjs child so its own gate can't drift from manager.ts's decision.
  • src/mitm/server.cjs — server creation is now wrapped in an async startMitmServer() bootstrap, gated by MITM_CERT_MODE. Default (legacy, unset) reproduces the exact prior synchronous behavior (byte-for-byte, verified with git diff -w) — existing installs are never silently changed. root-ca mode builds an SNICallback-driven https.createServer. Since server.cjs is spawned via plain node (no TS/ESM loader — see manager.ts's spawn(process.execPath, [MITM_SERVER_PATH], ...)), it cannot import() the TS sources directly; the CJS/ESM boundary is crossed via a new src/mitm/_internal/rootCaShim.cjs, a CJS twin of the CA/leaf crypto (byte-identical selfsigned.generate() parameters to tproxy/dynamicCert.ts), matching the established pattern of the sibling _internal/*.cjs shims already in this file (e.g. the hand-ported sanitizeErrorMessage).

How validated

TDD (Hard Rule #18) — 14 new unit tests, all genuinely new (none of rootCa.ts/migration.ts exist on the base branch, so they fail-to-import before this change and pass after):

  • tests/unit/mitm-root-ca-persistence-6684.test.ts — generate-once/load-on-repeat, 0o600 key perms, CA basicConstraints.
  • tests/unit/mitm-root-ca-leaf-issuance-6684.test.ts — every MITM_TOOL_HOSTS host resolves a SecureContext, issued leaf SAN matches the hostname, leaf chain validates against the CA cert (X509Certificate.checkIssued/verify), leaf caching (identical SecureContext on repeat lookup), and a drift guard proving a newly-added tool host needs no matching entry anywhere.
  • tests/unit/mitm-cert-migration-6684.test.ts — every migration-gate branch (legacy stays legacy, fresh install gets CA, explicit opt-in overrides, partial legacy state treated as no-legacy).

All 14 pass (node --import tsx/esm --test tests/unit/mitm-root-ca-persistence-6684.test.ts tests/unit/mitm-root-ca-leaf-issuance-6684.test.ts tests/unit/mitm-cert-migration-6684.test.ts). Pre-existing mitm tests (mitm-cert-mac-fingerprint, mitm-cert-removal-wiring, mitm-manager-*) still pass unchanged.

Manual live smoke test (in-repo, not the VPS): spawned server.cjs directly in both modes.

  • Legacy mode (default, unset MITM_CERT_MODE): boots and listens exactly as before.
  • Root-CA mode (MITM_CERT_MODE=root-ca, fresh DATA_DIR): generates ca.key/ca.crt with 0600 perms and boots via SNICallback. A real TLS handshake with servername: "api.githubcopilot.com" (a Copilot host from MITM_TOOL_HOSTS, previously uncovered by the legacy single-SAN antigravity-only cert) returned a CA-issued leaf whose subject/SAN matched that hostname exactly, and chained to the CA.

Gates run in the worktree, all green: check-test-discovery.mjs, typecheck:core, typecheck:noimplicit:core (2 pre-existing unrelated files only, confirmed via git show origin/release/v3.8.49:<path>), eslint on every changed file (0 errors), check:complexity-ratchets (both cyclomatic + cognitive, no regression), check-file-size.mjs (no frozen-file growth), check:cycles (no new cycle), check-docs-sync.mjs, check-fabricated-docs.mjs --strict (3 pre-existing failures, all in files this PR does not touch — docs/INCIDENT_RESPONSE.md, docs/PERF_BUDGETS.md, docs/routing/REASONING_ROUTING.md, confirmed base-red).

Deferred to VPS live validation

Actual OS trust-store mutation (installCaCert()'s end effect) is not unit-testable. Deferred per Hard Rule #18 path 2:

# On 192.168.0.15, with a build of this branch:
DATA_DIR=/tmp/mitm-ca-live-test MITM_CERT_MODE=root-ca MITM_ROOT_CA_ENABLED=true \
  node src/mitm/manager.ts   # (via the real startMitm() flow / dashboard "Start Server")
# then confirm:
#   - checkCertInstalled(caCertPath) returns true
#   - curl --cacert /tmp/mitm-ca-live-test/mitm/ca.crt https://<any AgentBridge-proxied host via SNI>
#     completes without a certificate warning

Scoping notes

  • i18n: N/A — no new user-facing strings (the trust-flow gate is env-var driven, not a dashboard toggle in this PR).
  • Kept MITM_ROOT_CA_ENABLED as a plain env var (not a DB-backed featureFlagDefinitions.ts entry) — consistent with how server.cjs/manager.ts already gate other MITM behavior (MITM_VERBOSE, MITM_LOCAL_PORT, OMNIROUTE_SKIP_SYSTEM_TRUST), and avoids pulling in the i18n-coverage gate for a flag with no dashboard UI in this PR.
  • Docs updated: docs/security/MITM-TPROXY-DECRYPT.md §4 and docs/frameworks/AGENTBRIDGE.md §2.2 now describe the unified CA architecture, the one-time trust step, MITM_ROOT_CA_ENABLED, and the legacy-leaf back-compat behavior.

Refs #6684 — the dashboard UI toggle / one-click "opt into root-CA" flow and the VPS trust-store live check above are left for a follow-up so this PR stays reviewable; the core persistence/issuance/migration/server-wiring behavior is complete and tested.

…er (#6684)

Replace the AgentBridge static server's single self-signed leaf cert
(scoped only to the 4 antigravity hosts) with a persisted local root CA
+ per-SNI leaf certs, reusing the CA/leaf crypto already proven for the
TPROXY capture mode (tproxy/dynamicCert.ts). server.cjs switches from a
static key/cert to an SNICallback so every host in MITM_TOOL_HOSTS gets
a matching leaf, not just antigravity.

- src/mitm/cert/rootCa.ts: load-or-generate-once CA persistence
  (ca.key/ca.crt under <DATA_DIR>/mitm/), private key chmod 0o600.
- src/mitm/cert/migration.ts: pure migration gate — an already-trusted
  legacy leaf install stays on the old leaf until the operator opts in
  via MITM_ROOT_CA_ENABLED=true; a fresh install gets the CA model
  automatically. A CA that can sign a leaf for any host is materially
  more powerful than the old fixed-SAN leaf, so the switch is never
  silent for an already-trusted install.
- src/mitm/cert/install.ts: installCaCert() — thin wrapper over the
  existing cert-path-agnostic installCertResult(), same
  omniroute-mitm.crt trust-store slot the old leaf used (supersedes it,
  no dual-trust cleanup needed).
- src/mitm/manager.ts: wires the migration gate + CA load/install into
  the bridge-start sequence, passes the resolved MITM_CERT_MODE to the
  spawned server.cjs child so it can't drift from manager.ts's decision.
- src/mitm/server.cjs: async-bootstraps server creation behind the same
  MITM_CERT_MODE gate; default ("legacy") reproduces the exact prior
  synchronous behavior. The CJS/ESM boundary (server.cjs is spawned via
  plain `node`, no TS loader) is crossed via a new
  _internal/rootCaShim.cjs CJS twin of the CA/leaf crypto, matching the
  established pattern of the sibling _internal/*.cjs shims in this file.

Validated: 14 new unit tests (CA generate-once, 0o600 key perms, CA
basicConstraints, leaf issuance across every MITM_TOOL_HOSTS host, SAN
match, chain validation against the CA, leaf caching, migration-gate
branches) plus a manual live smoke test spawning server.cjs in both
legacy and root-ca mode (confirmed a real TLS handshake with SNI
api.githubcopilot.com returns a CA-issued leaf for that host).

Deferred to VPS live validation (OS-trust-store mutation is not
unit-testable): actual OS trust-store install of the CA cert via
installCaCert() on Linux/macOS/Windows.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

const ca = await generateMitmCa("Test MITM CA");
const store = new DynamicCertStore("Test MITM CA", ca);
const neverRegisteredHost = "some-brand-new-tool-host.example.com";
assert.ok(!allHosts().includes(neverRegisteredHost));
@diegosouzapw
diegosouzapw merged commit a200406 into release/v3.8.49 Jul 19, 2026
11 of 20 checks passed
@diegosouzapw
diegosouzapw deleted the feat/6684-mitm-root-ca branch July 19, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants