Phase 1 (author-only): Flux + SOPS bootstrap + interface wiring (T-1.4)
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]>
This commit is contained in:
1 parent
0e901f135c
commit
84a2d762f0
12 files changed
+573
-4
No files matched your search
@@ -0,0 +1,69 @@
|
||||
# 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").
|
||||
Reference in new issue
Block a user