Hangar

Docs · Glidepath

ADR-0006: Cluster-agnostic bootstrap, no cluster state in the app repo

Context

Every new cluster needs its own irreducible per-cluster material (its own API server root CA, its own independently-generated Fulcio signing root - trust roots are deliberately never shared between clusters). Hand-typing this per cluster (openssl run by hand, certs copy-pasted into a values file) doesn’t scale past one or two clusters and risks a copy-paste trust-root leak between them. Committing that material inside platform-cicd’s own repo also means the “reusable application” repo accumulates cluster-specific secrets and identity over time - platform-cicd should be installable standalone on any cluster, including ones that don’t exist yet.

Decision

hack/generate-cluster-values.sh reads a target cluster’s own API server CA live and generates a fresh Fulcio root for it, refusing to regenerate the Fulcio root accidentally (FORCE=1 required) since that would invalidate every image already signed under the old one. Its output is a values file written to wherever the caller points it - by default, platform-cicd’s own hack/ (for an ad-hoc/imperative install with no gitops repo yet), but normally the target cluster’s own gitops-cluster-<name> repo, next to the ArgoCD Application that consumes it via a multi-source $ref. Chart-level values that used to be hand-typed per cluster (tenant-repo URL, ApplicationSet name) now derive from a single clusterName value by convention instead.

Consequences

  • platform-cicd carries zero cluster-specific secrets or identity in its own repo - standing up cluster N is “run the generator, commit its output to that cluster’s own config repo,” not “hand-edit a new file inside platform-cicd and hope nothing collides with another cluster’s values.”
  • A new cluster’s onboarding is close to “fill in a small parameter set” rather than “copy the last cluster’s file and edit every field by hand.”
  • One documented, deliberate exception: a cluster bootstrapped before this convention existed keeps its historical values file as pinned overrides rather than being retrofitted to match - see that file’s own header for why.

Update, 2026-09-05: the manual script is now an override path, not the bootstrap path

The gap this ADR left open was real: “generate real values, then run this script against the cluster” is still a human running a command from their laptop, so gitops-cluster-template couldn’t build a genuinely complete cluster from git alone. Re-reading the actual Decision above, the constraint it states is narrower than “no GitOps” - the private key must never be persisted to a repo or exist anywhere but the cluster’s own Secret. Nothing about the mechanism that generates it needs to be manual.

charts/glidepath-control-plane/templates/hooks/fulcio-bootstrap-job.yaml now ports this exact recipe (same live kube-root-ca.crt read, same ed25519 openssl invocation, same refuse-to-clobber-an-existing-root idempotency) into an ArgoCD pre-install hook Job - the private key still never leaves that Job’s own pod, still never touches a repo or a human’s terminal, still gets generated fresh per cluster. Only the trigger changes: “runs automatically on this cluster’s first sync” instead of “an operator remembers to run a script.” hack/generate-cluster-values.sh stays in the repo as the documented path for deliberately rotating an existing root (FORCE=1) - a rare, consequential action that should stay an explicit human decision - but it is no longer part of standing up a new cluster.

Rekor+Trillian+MySQL (deferred at the time this ADR was written, see docs/admin/provenance-policy.md) followed the same pattern once it was actually built: a real ArgoCD Application (gitops-cluster-template/50-platform-cicd/rekor/) against the upstream chart, with its own low-stakes MySQL credentials generated by hack/customize-cluster.sh the same way Infisical’s PG/Redis passwords already were - not committed by a human copy-pasting openssl rand output either.