bootstrap/sops (age-keygen refusing in-repo, .sops.yaml, install-age-key.sh glue installing the sops-age Secret) + bootstrap/flux (bootstrap.sh: flux install -> apply -k clusters/dev; decryption/url patches). PM wiring: place .sops.yaml at repo root (SOPS walks to root); inline the 3 flux patches into clusters/dev (avoids kustomize ../ load-restriction). clusters/dev builds with decryption enabled; full CI gate PASS. No key material; nothing executed. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
70 lines
3.5 KiB
Markdown
70 lines
3.5 KiB
Markdown
# 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").
|