Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

ADR-0006: Name-constrained dev CA stored per profile

Status: accepted · Date: 2026-09-29 · Decision makers: James Hebden

Context and Problem Statement

Local clusters terminate TLS for hostnames like gitlab.test. Browsers and tools should trust those certificates without click-through warnings. That means a CA the developer trusts system-wide, and a way for the cluster’s cert-manager to issue certificates from it. A CA that’s trusted everywhere and has its key sitting on disk and in a cluster is a sharp tool, so the design needed to limit what it can sign.

Decision Drivers

  • Trust only what the developer’s own environment needs
  • No manual certificate steps for the common case
  • The cluster has to hold signing capability for cert-manager to issue leaf certificates
  • Regeneration and rotation shouldn’t leave a half-written CA visible
  • Existing trusted CAs from Caproni shouldn’t be orphaned

Considered Options

  1. Generate an ECDSA P-256 CA per profile with Name Constraints (the default subtrees .local, .test, and .svc, plus the configured hostname and cluster domain when they fall outside those); store it under the data directory; deploy it to the cluster as a Secret for cert-manager
  2. Shell out to mkcert
  3. Use an unconstrained CA
  4. Use a CA generated inside the cluster only, and export its certificate for trust

Decision Outcome

Chosen option: “Generated, name-constrained CA per profile”, because Name Constraints bound the damage if the key leaks (to non-public names, though the defaults cover every .test, .local, and .svc name, not just the project’s) and the whole flow needs no extra tool. Option 4 avoids putting the key in a Secret but makes the CA vanish with the cluster, so the trusted certificate would change on every recreate.

Details: rcgen (ring backend) with a 10-year lifetime, notBefore back-dated one hour, a 128-bit random serial, IsCa::Ca(BasicConstraints::Constrained(0)), KeyCertSign and CrlSign. Files live in gen-<random>/{ca.key,ca.crt} under ~/.local/share/xianyun/<profile>/pki/ and a relative current symlink is swapped atomically. ensure_current re-signs with the same key when constraints change and regenerates on expiry.

Consequences

Good:

  • Atomic publish means readers never see a partial CA
  • The key file is created 0600 with create_new

Bad:

  • The CA key is copied into a kustomize staging directory under $TMPDIR with mode 644 inside a 755 directory (measured at the backend on 2026-10-13; latent until the deployer registry gap is fixed) (TM-PKI-001)
  • The key is stored as a cluster Secret, and PkiCaProvider::load can generate a CA with no Name Constraints on a profile that has none (TM-PKI-002)
  • With driver: external, the cluster reports its own DNS zone and that value feeds the constraints without validation (TM-PKI-003)
  • Directories are created with the default umask, loading doesn’t verify that the key matches the certificate, and client enforcement of Name Constraints varies (TM-PKI-004, TM-PKI-005, TM-PKI-006)
  • A 10-year lifetime is long for a certificate whose key lives in a cluster

Neutral:

  • The common name is still “Caproni Dev CA”; see ADR-0003

Implementation Location

  • crates/xianyun-pki/src/ca.rs
  • crates/xianyun-pki/src/constraints.rs
  • crates/xianyun-pki/src/truststore/install.rs
  • crates/xianyun-deploy/src/deployer/kustomize/camaterial.rs

More Information

  • Retroactively documented on 2026-10-13 from the code and crates/xianyun-pki/PORT_NOTES.md.
  • Related: FR-008, SR-010, SR-011, STORY-003.