Operator directive: don't diverge architecture to avoid asking for credentials; provision them upfront via SOPS and keep the existing (unified) graph. Roadmap reconciliation: Phase 2 = Network/Storage/TLS/DNS/Ingress, so cert-manager/externaldns/gateway (the credential-gated pieces) ARE Phase 2; monitoring is Phase 4. Earlier A2/rollout scope was inverted by credential-avoidance. Reverts (credential-driven): remove clusters/dev/rollout/ + the $patch:delete's; restore unified base graph (dev = ../base + cluster-vars + SOPS decryption, R-ENV-02 intact); restore monitoring files (Phase 4). Kept non-credential improvements: tofu for_each/rejoin, Kyverno parametrization + webhook excludes. platform/kustomization.yaml now phased (single shared list): Phase-2 set = kyverno, cilium, longhorn, cert-manager, externaldns, gateway; later components commented with phase markers. SOPS activated: .sops.yaml at repo root; bootstrap/sops/secret-templates/*.yaml.example for all secrets (cloudflare x2, grafana, alertmanager-smtp, keycloak, oauth2-proxy, pia); bootstrap/sops/CREDENTIALS.md checklist; pre-wired commented cloudflare-api-token lines in cert-manager + externaldns; filled non-secret ACME email. Agents never touch real secret values (operator encrypts). Validated: kubectl kustomize dev(unified)/prod/platform(Phase-2 only), templates inert, make -C ci gate PASS. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
bootstrap/sops — SOPS + age key material & decryption wiring (Phase 1, T-1.4)
Generates the age keypair, declares SOPS creation rules, and documents how the age private key is installed into the cluster so Flux can decrypt SOPS at reconcile. Spec: ADR-0007 (SOPS+age), docs/07 · Requirement: R-SEC-01 (secrets encrypted at rest in git, decrypted only in-cluster).
AUTHOR-ONLY. Nothing here has been executed. No key or secret material exists in this output. No
age-keygen,sops, orkubectlwas run. The age keypair is generated later by the operator;.sops.yamlcarries only a placeholder for the public recipient.
Files
| File | Purpose |
|---|---|
age-keygen.sh |
Operator-run script: generates the age keypair to a path outside the repo, prints only the public recipient. Refuses to write inside the repo or clobber an existing key. |
.sops.yaml |
SOPS creation rules (encrypt only data/stringData → readable diffs). age: recipient is a clearly-marked REPLACE-ME placeholder filled at bootstrap. Effective home is the repo root (see below). |
age-key-secret.md |
Runbook + kubectl create secret template installing the age private key as the sops-age Secret in flux-system (the sanctioned out-of-band step). Rotation + DR custody included. |
How this satisfies R-SEC-01 / ADR-0007
- Secrets are committed encrypted (
sops --encrypt) using the age public recipient in.sops.yaml; plaintext never enters git (.gitignoreguardsage.key,*.key,*.dec.yaml). - Flux decrypts in-cluster using the age private key held in the
sops-ageSecret — the one documented out-of-band bootstrap action (docs/07, ADR-0007 Consequences). decryption.provider: sops+secretRef: sops-ageon theplatform/appsFlux Kustomizations is wired by../flux/patches/(realising the commented TODOs inclusters/base).- No key material appears in scripts, docs, PRs, or logs (AGENTS.md §5).
Interface / handoff
.sops.yamlbelongs at the REPOSITORY ROOT. SOPS discovers it by walking up from the file being encrypted, so it only governsplatform/**andapps/**from root. It is authored here (in this worker's subtree); the PM/clusters owner installs it at the repo root (copy or symlink). Flagged as an escalation in the T-1.4 report.- Fill two placeholders at bootstrap: the
age:recipient in.sops.yaml(fromage-keygen.sh's output) — same value in both creation rules. - Order (see
../flux/README.md):age-keygen.sh→ installsops-ageSecret (age-key-secret.md) →../flux/bootstrap.sh. The Flux entrypoint fails fast if the Secret is missing.