fix: hand the OS CA bundle to npm and the agents behind TLS proxies#206
Open
quickbeard wants to merge 1 commit into
Open
fix: hand the OS CA bundle to npm and the agents behind TLS proxies#206quickbeard wants to merge 1 commit into
quickbeard wants to merge 1 commit into
Conversation
Follow-up to #205, which fixed only our own process. `npm i -g` and the agents are separate processes behind the same proxy, each with its own trust store, so an affected user could get past login and still fail at install or agent launch. Write every CA we trust to ~/.codev-hub/system-ca.pem and point children at it with NODE_EXTRA_CA_CERTS, from both execAsync (npm, code, JetBrains CLIs, codegraph) and runAgent (the agents). Who actually needs it: npm Node yes — ignores the OS store opencode / codev-code Bun yes — ignores the OS store Claude Code bun-native no — reads the OS store itself Codex Rust/rustls no — reads it via rustls-native-certs NODE_EXTRA_CA_CERTS *appends*, which is what makes it safe to set for all four: the two that don't need it are unharmed. Deliberately not npm's `cafile` or Codex's SSL_CERT_FILE — those REPLACE the root set, so they'd break every other endpoint, and aiming them at our bundle could narrow trust for a tool that already works. The bundle is the complete set (default ∪ system), never just the corporate root: "default" carries the Mozilla roots plus any NODE_EXTRA_CA_CERTS the user set, so a child gains the proxy without losing anything. A user's own NODE_EXTRA_CA_CERTS wins — the var holds a single path, so ours would silently replace theirs. Written only once something has *seen* a cert failure, same as the merge: the OS-store read is a synchronous ~300ms stall on Windows (the #205 CI regression). Unaffected users pay one existsSync per spawn and nothing else. execAsync also retries a child once when its stderr blames the chain, covering the gap where nothing fetched first (a cached session, or `codevhub update`) and npm is the first thing to meet the proxy. Two things end-to-end testing caught that unit tests could not: - Node returns bundled certs WITHOUT a trailing newline but the OS store's WITH one, so a plain join("") produced `-----END CERTIFICATE----------BEGIN CERTIFICATE-----`. OpenSSL rejects the file ("bad end line") and Node merely warns and ignores it — the whole feature silently did nothing. Every cert is now terminated explicitly, and the fixtures preserve that newline skew, since uniform fixtures are what hid it. - Verified a real child process against a server presenting an intercepted chain: without the bundle it fails with the users' exact SELF_SIGNED_CERT_IN_CHAIN; with it, HTTP 200. The bundle parses as 146 certs (145 public + the corporate root) — nothing narrowed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #205, which fixed only our own process.
npm i -gand the agents are separate processes behind the same proxy, each with its own trust store — so an affected user could get past login and still fail at install or agent launch.The fix
ensureSystemCaBundlewrites every CA we trust to~/.codev-hub/system-ca.pem;childCaEnvpoints children at it withNODE_EXTRA_CA_CERTS. Injected from bothexecAsync(npm,code, JetBrains CLIs, codegraph) andrunAgent(the agents).Only two of the four children actually need it:
NODE_EXTRA_CA_CERTSsince Bun 1.1.22Key decisions:
NODE_EXTRA_CA_CERTSappends, which is what makes it safe to set for all four — the two that don't need it are unharmed. Deliberately not npm'scafile/ca(RFC #748) or Codex'sSSL_CERT_FILE: those replace the root set, so pointing them at a corporate root breaks every other endpoint, and aiming them at our bundle could narrow trust for a tool that already works.default∪system), never just the corporate root —defaultcarries the Mozilla roots plus anyNODE_EXTRA_CA_CERTSthe user already set, so a child gains the proxy without losing anything.NODE_EXTRA_CA_CERTSwins — the var holds a single path, so ours would silently replace theirs.existsSyncper spawn and nothing else.execAsyncretries a child once when its stderr blames the chain (outputHasCertError, the text-level twin ofisCertError— a child's failure reaches us as output, not a typed error). The bundle usually exists already, sincecodevhub installlogs in before it installs; this covers the gap where nothing fetched first (a cached session, orcodevhub update) and npm is the first thing on the machine to meet the proxy.Two bugs end-to-end testing caught that unit tests could not
The bundle was invalid PEM. Node returns the bundled certs without a trailing newline but the OS store's with one, so
join("")produced-----END CERTIFICATE----------BEGIN CERTIFICATE-----. OpenSSL rejects the file (bad end line) and Node merely warns and ignores it — the entire feature silently did nothing while every unit test passed, because my fixtures all ended in\n. Certs are now terminated explicitly, and the fixtures deliberately preserve the newline skew. Re-running with the naivejoin("")now fails two tests.My first check of the PEM format sampled
sys[0] ?? def[0], which picked a system cert — the one shape that happens to be newline-terminated. Sampling one store hid the skew entirely.Verification
Drove a real child process against a server presenting an intercepted chain (leaf + untrusted self-signed root) — the shape of
npm install -gbehind the proxy:And the bundle is complete, not narrowing:
openssl storeutlparses 146 certs = 145 public defaults + the corporate root.Unit tests pin the bundle contents, dedup, newline handling, the user's-var-wins rule, the npm retry (including that it does not retry a 404), and that an unaffected agent launch reads neither the bundle nor the OS store.
pnpm fix/typecheck/test(960 passing) /build && node dist/index.js --versionall green.Note for reviewers
Bun has two open reports that touch this exact path — #24581 (
NODE_EXTRA_CA_CERTSnot working in 1.3.2) and #23735 (1.3.0 stopped using the system store by default). Neither is confirmed resolved. If OpenCode/CoDev Code still fail behind a proxy with the bundle set, the fallback isNODE_USE_SYSTEM_CA=1(Bun ≥1.2.23) — deliberately not set here, since it would make each agent launch re-read the OS store rather than use the file we already wrote.🤖 Generated with Claude Code