Skip to content

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.caBundle is 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 up reads the file, host-side. jr2.config.ts is 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. up fails loudly on an unreadable path.
  • up materializes the jr2-ca ConfigMap beside the harness ConfigMap; both Harness placements mount it at /etc/jr2/ca on the Harness container and set NODE_EXTRA_CA_CERTS=/etc/jr2/ca/ca.crt. CR-level volumeMounts land on the primary container by the operator's contract, so the asymmetry costs no operator change: the User Container (user-owned image — same rule as harness.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 up adds three keys to jr2-ca: extra.crt (caBundle + the Instance's held-secret CA), which NODE_EXTRA_CA_CERTS names instead; bundle.crt (the roots + both), which SSL_CERT_FILE, REQUESTS_CA_BUNDLE and GIT_SSL_CAINFO name, because those replace a tool's store; and upstream.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 runs jr2 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_CERTS on the check pod, from its own copy of jr2-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's fetch honoring NODE_EXTRA_CA_CERTS was verified live against the real endpoint before this landed.)

Considered options ​

  • Generic sandbox.volumes/volumeMounts passthrough. 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=0 via harness.env. Zero kit change, but it disables verification for every TLS connection the Harness makes (git, MCP, everything), and the preflight still fails — up would 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.crt edit + jr2 up + pod restart — the same ConfigMap-update path as Agent definition edits (ADR-0018).
  • NODE_EXTRA_CA_CERTS adds to Node's trust store, so public endpoints keep working. With no held secret it reaches Node processes alone — an agent shelling out to curl/git against the private CA needs the system store, which is the Sandbox Image's business. With a held secret, bundle.crt reaches curl, git and Python too; the cost is that it REPLACES their store, so a private CA the Sandbox Image trusts on its own, and caBundle does 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.

The docs of Kit version 0.5.1. Each page is also at its .md URL.