Files
home-network-k3s-impl/bootstrap/sops
jakman85andClaude Opus 4.8 1a98429434 feat(phase3): author shared VPN egress gateway (Gluetun/PIA/WireGuard) — author-first
Per ADR-0013: platform/vpn-egress/ = a single shared Gluetun gateway (PIA over WireGuard,
built-in kill-switch FIREWALL=on, native port forwarding), namespace (non-app, NET_ADMIN +
/dev/net/tun), control-API Service, and a pre-wired (commented) pia-credentials SOPS line.
Defined the PIA secret location (platform/vpn-egress, keys username/password → OPENVPN_USER/
OPENVPN_PASSWORD) and updated the SOPS template accordingly.

NOT wired into platform/kustomization.yaml (Phase 3, author-first) — deploy needs PIA creds +
validation of the egress-steering mechanism (Cilium Egress Gateway → the Gluetun node's wg0;
documented in README with options + a fail-closed rationale, deliberately NOT committed as an
untested CiliumEgressGatewayPolicy). WireGuard chosen (UDP, protocol != TCP); PIA supports it on
all regions and the secret stays username/password.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-08-21 22:19:22 -05:00
..

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

The SOPS creation rules live in the single canonical repo-root .sops.yaml. The operator generates the age keypair via age-keygen.sh, pastes the public recipient into that root file, and installs the private key in-cluster as the sops-age Secret. No private key material ever enters git. (CREDENTIALS.md is the end-to-end provisioning checklist.)

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.
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.
CREDENTIALS.md End-to-end operator checklist: SOPS setup + the full secret inventory (per component/phase).
secret-templates/ *.yaml.example scaffolds (correct name/namespace/keys, placeholder values) to copy → fill → sops -e.
repo-root .sops.yaml The single canonical SOPS creation rules (encrypt only data/stringData → readable diffs). At the repo root, NOT here — a copy in this dir would shadow it for files under bootstrap/**.

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 (.gitignore guards age.key, *.key, *.dec.yaml).
  • Flux decrypts in-cluster using the age private key held in the sops-age Secret — the one documented out-of-band bootstrap action (docs/07, ADR-0007 Consequences).
  • decryption.provider: sops + secretRef: sops-age on the platform/apps Flux Kustomizations is wired by ../flux/patches/ (realising the commented TODOs in clusters/base).
  • No key material appears in scripts, docs, PRs, or logs (AGENTS.md §5).

Interface / handoff

  • .sops.yaml is a SINGLE file at the REPOSITORY ROOT. SOPS discovers it by walking up from the file being encrypted, so a root file governs platform/**, apps/**, catalog/**, and secrets/**. Do not keep a second copy in this directory — it would be discovered first for files under bootstrap/** and shadow the root config (that duplicate was removed after it caused exactly that).
  • Fill the recipient at bootstrap: paste age-keygen.sh's public recipient into the root .sops.yaml — the same value in both creation rules.
  • Order (see ../flux/README.md): age-keygen.sh → install sops-age Secret (age-key-secret.md) → ../flux/bootstrap.sh. The Flux entrypoint fails fast if the Secret is missing.