# Installing the age private key as the `sops-age` Secret (out-of-band) **Phase 1, T-1.4 · ADR-0007 (SOPS+age) · R-SEC-01 · spec docs/07 "Secrets management".** This is the **one sanctioned non-GitOps action** in the secrets model (ADR-0007 Consequences, docs/07): the age **private** key is bootstrapped into the cluster out-of-band as a Kubernetes Secret that Flux's kustomize-controller reads to decrypt SOPS at reconcile time. The key is **never** committed (`.gitignore` guards `age.key`, `*.key`); no key material appears in git, PRs, or logs (AGENTS.md §5). > **AUTHOR-ONLY runbook.** Nothing here has been executed. The operator runs these steps once, after > the age key exists (`age-keygen.sh`) and Flux controllers are installed (`../flux/bootstrap.sh`). ## Prerequisites 1. `age-keygen.sh` has been run → private key at `~/.config/sops/age/keys.txt` (outside the repo). 2. The `flux-system` namespace exists (created by `flux install` / `flux bootstrap`). 3. `kubectl` context points at the dev cluster. ## Create the Secret (command template — NOT run) Flux discovers age keys inside the `sops-age` Secret by the **`.agekey` filename suffix**, so the data key MUST end in `.agekey`: ```bash # Reads the private key file and stores it under data key "age.agekey". cat ~/.config/sops/age/keys.txt | kubectl create secret generic sops-age \ --namespace=flux-system \ --from-file=age.agekey=/dev/stdin ``` Idempotent re-apply variant (avoids "already exists" on re-runs): ```bash kubectl create secret generic sops-age \ --namespace=flux-system \ --from-file=age.agekey="$HOME/.config/sops/age/keys.txt" \ --dry-run=client -o yaml | kubectl apply -f - ``` - Secret name `sops-age` and namespace `flux-system` MUST match the `decryption.secretRef.name` on the `platform`/`apps` Flux Kustomizations (see `../flux/patches/`). - This Secret is created imperatively and is **NOT** in git — it holds the private key, so it must never be committed, encrypted or otherwise. (It is the trust anchor, not a managed resource.) ## Verify (read-only — NOT run) ```bash kubectl -n flux-system get secret sops-age # exists kubectl -n flux-system get secret sops-age \ -o jsonpath='{.data.age\.agekey}' | base64 -d | age-keygen -y # prints PUBLIC recipient only ``` The printed public recipient MUST equal the `age:` value in `.sops.yaml`. (This command derives the public key from the private key; it never exposes the private key.) ## Rotation (documented per ADR-0007 "Key custody/rotation") 1. Generate a NEW key with `age-keygen.sh` (to a fresh path; the script refuses to clobber). 2. Add the new **public** recipient to `.sops.yaml` **alongside** the old (multi-recipient), then `sops updatekeys` every encrypted file so both keys can decrypt. 3. Update the `sops-age` Secret to contain **both** private keys (two `*.agekey` data entries) so Flux can still decrypt existing files during the transition. 4. Once all files are re-encrypted to the new recipient, remove the old recipient from `.sops.yaml`, `sops updatekeys` again, and drop the old key from the Secret. ## Key custody / DR (ADR-0007: "losing it blocks decryption") - The age **private** key is DR-critical: without it Flux cannot decrypt any committed Secret. - Keep an **off-cluster** backup of `keys.txt` in the operator's password manager / offline store (NOT in this repo, NOT on the cluster it protects). This mirrors the off-cluster-mirror posture for Gitea (docs/05 "Circularity & DR").