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-cicdcarries 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.