ADR-0020 Architecture decision record
Private-CA trust is a named concept, not volume plumbing
Running the jr-parity exercise against a real vLLM (2026-07-19) hit an endpoint that is HTTPS-only behind an ingress whose cert chains to a private CA. The host trusts that CA; pods do not — so every jr2-owned egress point fails the same way: the Harness's model calls, and jr2 up's own provider preflight (which checks from a one-shot in-cluster pod, the check pod, ADR-0019). The tempting fix was generic volumes/volumeMounts passthrough on sandbox — the CRD already carries both, and harness.env/envFrom set the precedent of corev1 shapes passed through verbatim. But trusting a CA is not pod-shaped config: it is an intent ("this instance's egress must trust this CA") that jr2 alone can thread through every place it owns an outbound TLS connection — including the check pod, which no instance-authored volume can ever reach. A passthrough would also make the one-intent story a three-part incantation (hand-managed ConfigMap + mount + NODE_EXTRA_CA_CERTS via harness.env), the runbook shape ADR-0019 exists to kill.
Decision
harness.caBundleis the surface: a path to a PEM bundle, relative to the instance folder, committed (CA certs are public data — never a Secret). One file may carry multiple certs; that is what a PEM bundle is.- Only
jr2 upreads the file, host-side.jr2.config.tsis evaluated in two worlds — by the CLI on the host and by the deployed orchestrator in-cluster — so the config carries a path, not file contents: the in-cluster evaluation checks presence and never touches the filesystem.upfails loudly on an unreadable path. upmaterializes thejr2-caConfigMap beside the harness ConfigMap; both Harness placements mount it at/etc/jr2/caon the Harness container and setNODE_EXTRA_CA_CERTS=/etc/jr2/ca/ca.crt. CR-levelvolumeMountsland on the primary container by the operator's contract, so the asymmetry costs no operator change: the User Container (user-owned image — same rule asharness.env, ADR-0005/0013) never inherits the trust path.- While a held secret exists, the bundle also reaches the Custodian and the Harness's other tools (ADR-0059).
jr2 upadds three keys tojr2-ca:extra.crt(caBundle+ the Instance's held-secret CA), whichNODE_EXTRA_CA_CERTSnames instead;bundle.crt(the roots + both), whichSSL_CERT_FILE,REQUESTS_CA_BUNDLEandGIT_SSL_CAINFOname, because those replace a tool's store; andupstream.crt(the roots +caBundle, never the held-secret CA), which the Custodian verifies a bound upstream against — a corporate gateway whose certificate chains to the private CA included. "Roots" are the Mozilla roots of the Node that runsjr2 up. The ConfigMap is then applied server-side: two copies of the roots are more than client-side apply's annotation holds. The Custodian's control listener is plain HTTP to the Orchestrator's Service and trusts nothing. - The provider preflight trusts the same bundle the same way (
NODE_EXTRA_CA_CERTSon the check pod, from its own copy ofjr2-ca). A preflight that fails where the Harness would succeed — or passes where it would fail — is a broken promise; sharing the mechanism keeps the two verdicts identical. (Node 24'sfetchhonoringNODE_EXTRA_CA_CERTSwas verified live against the real endpoint before this landed.)
Considered options
- Generic
sandbox.volumes/volumeMountspassthrough. More powerful and precedent-consistent, but it cannot reach the check pod, splits one intent across three keys, and adds a forever-surface for a need that has appeared exactly once. Deferred until a second real need for arbitrary volumes shows up — the CRD support is already there when it does. NODE_TLS_REJECT_UNAUTHORIZED=0viaharness.env. Zero kit change, but it disables verification for every TLS connection the Harness makes (git, MCP, everything), and the preflight still fails —upwould refuse to converge an instance whose pods would work.- Baking the CA into the Harness image. Contradicts ADR-0018's stock published image — a per-instance CA means a per-instance image, the exact build ADR-0018 deleted.
Consequences
- Rotating or adding a CA is a
ca.crtedit +jr2 up+ pod restart — the same ConfigMap-update path as Agent definition edits (ADR-0018). NODE_EXTRA_CA_CERTSadds to Node's trust store, so public endpoints keep working. With no held secret it reaches Node processes alone — an agent shelling out tocurl/gitagainst the private CA needs the system store, which is the Sandbox Image's business. With a held secret,bundle.crtreaches curl, git and Python too; the cost is that it REPLACES their store, so a private CA the Sandbox Image trusts on its own, andcaBundledoes not name, stops being trusted by them in that pod (ADR-0059).- The orchestrator pod itself (boot-reconcile
git clone) does not consume the bundle yet; repos behind a private CA would extend the same ConfigMap to the orchestrator Deployment — a deliberate later step, same concept.