The terse 'Zone->DNS->Edit + Zone->Read' sent the operator to the Account permission scope. Spell out switching the scope dropdown to Zone + the exact two Zone-scoped rows + Specific-zone resource. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
87 lines
5.0 KiB
Markdown
87 lines
5.0 KiB
Markdown
# Credentials & SOPS — operator provisioning checklist
|
|
|
|
All platform secrets are **SOPS-encrypted (age)**, committed to git, and decrypted by Flux at
|
|
reconcile (ADR-0007, R-SEC-01). **Agents never handle real secret values** — encryption is done by
|
|
you (the operator). This file is the end-to-end flow. Templates live in
|
|
[`secret-templates/`](secret-templates/); the master `.sops.yaml` is at the **repo root**.
|
|
|
|
Credentials are provisioned **upfront** so component rollout is gated only by *phase/readiness*,
|
|
never by a missing secret.
|
|
|
|
---
|
|
|
|
## One-time SOPS setup (do this first)
|
|
|
|
1. **Generate the age keypair** (private key stays on your machine + goes to the cluster; never in git):
|
|
```bash
|
|
./bootstrap/sops/age-keygen.sh # prints the PUBLIC recipient (age1…) + writes the private key
|
|
```
|
|
2. **Fill the recipient** into the repo-root `.sops.yaml` — replace **both** `REPLACE-ME-AGE-PUBLIC-RECIPIENT`
|
|
occurrences with the `age1…` public key from step 1. (This file governs encryption for `platform/**`,
|
|
`apps/**`, `catalog/**`, and any `secrets/**`.)
|
|
3. **Install the private key on the cluster** as the `sops-age` Secret so Flux can decrypt:
|
|
```bash
|
|
./bootstrap/sops/install-age-key.sh # creates secret/sops-age in flux-system (see age-key-secret.md)
|
|
```
|
|
The dev/prod overlays already point the `platform`/`apps` Flux Kustomizations at `decryption.secretRef: sops-age`.
|
|
|
|
> Keep the age **private** key out of the repo (same discipline as the ansible-vault passphrase at
|
|
> `~/.config/home-k3s/ansible-vault-pass`). If it leaks, rotate: new key → re-encrypt all `*.sops.yaml` → re-install.
|
|
|
|
---
|
|
|
|
## Per-secret flow (repeat for each secret below)
|
|
|
|
```bash
|
|
# 1. copy the template into its component dir under its real name
|
|
cp bootstrap/sops/secret-templates/<template>.yaml.example platform/<component>/<name>.sops.yaml
|
|
# 2. fill the real value(s) in stringData:
|
|
$EDITOR platform/<component>/<name>.sops.yaml
|
|
# 3. encrypt in place (SOPS picks up the root .sops.yaml rules automatically)
|
|
sops -e -i platform/<component>/<name>.sops.yaml
|
|
# 4. wire it: uncomment the pre-added `- <name>.sops.yaml` line in platform/<component>/kustomization.yaml
|
|
# 5. verify it still builds, then commit the CIPHERTEXT
|
|
kubectl kustomize platform/<component> >/dev/null && git add -A && git commit
|
|
```
|
|
|
|
---
|
|
|
|
## Secret inventory (provision all now; wire per phase)
|
|
|
|
| Secret | Namespace | Keys | Consumer | Phase | Template |
|
|
|---|---|---|---|---|---|
|
|
| `cloudflare-api-token` | `cert-manager` | `api-token` | cert-manager DNS-01 ClusterIssuer | **2** | `cloudflare-api-token.cert-manager.yaml.example` |
|
|
| `cloudflare-api-token` | `external-dns` | `api-token` | ExternalDNS (`CF_API_TOKEN`) — **same token**, 2nd copy | **2** | `cloudflare-api-token.external-dns.yaml.example` |
|
|
| `grafana-admin-credentials` | `monitoring` | `admin-user`, `admin-password` | Grafana admin | 4 | `grafana-admin-credentials.yaml.example` |
|
|
| `alertmanager-smtp` | `monitoring` | `smtp-password` | Alertmanager email | 4 | `alertmanager-smtp.yaml.example` |
|
|
| `keycloak-admin-credentials` | `keycloak` | `admin-user`, `admin-password` ⚠️verify chart keys; DB secret likely also needed | Keycloak IdP | 6 | `keycloak-admin-credentials.yaml.example` |
|
|
| `oauth2-proxy-credentials` | `oauth2-proxy` | `client-id`, `client-secret`, `cookie-secret` ⚠️verify chart keys | oauth2-proxy forward-auth | 6 | `oauth2-proxy-credentials.yaml.example` |
|
|
| `pia-credentials` | *(Phase-3 VPN ns, TBA)* | `username`, `password` | Gluetun VPN egress (protocol ≠ TCP) — **not wired yet** | 3 | `pia-credentials.yaml.example` |
|
|
|
|
**Cloudflare token — exact creation steps** (My Profile → API Tokens → Create Token → **Custom token**):
|
|
one token serves both cert-manager (DNS-01 `_acme-challenge` TXT) and ExternalDNS (A/CNAME), same value
|
|
in both namespace copies. In each Permissions row, the left dropdown defaults to **Account** — switch it
|
|
to **Zone** (Account-scope groups like "Account DNS Settings"/"DNS Firewall"/"Registrar Domains" are the
|
|
wrong scope and not needed). Set:
|
|
|
|
| Scope | Group | Access |
|
|
|---|---|---|
|
|
| Zone | DNS | Edit |
|
|
| Zone | Zone | Read |
|
|
|
|
- **Zone Resources:** Include → Specific zone → `keithjackson.dev`.
|
|
- Leave Client-IP/TTL filtering blank → Continue to summary → Create Token → copy the value into the
|
|
`api-token` field of **both** `cloudflare-api-token` secret copies.
|
|
|
|
---
|
|
|
|
## Phase-2 blockers (needed to un-suspend the Phase-2 platform tier)
|
|
|
|
- [ ] SOPS one-time setup (age key → `.sops.yaml` → `sops-age` on cluster).
|
|
- [ ] `cloudflare-api-token` provisioned + wired in **both** `cert-manager` and `external-dns`.
|
|
- [ ] ACME account email set in `platform/cert-manager/clusterissuers.yaml` (both issuers) — non-secret; a sensible default may already be filled, confirm it.
|
|
- [ ] Push current `main` to the host Gitea (Flux source), then create the dev root Kustomization to reconcile `clusters/dev`.
|
|
|
|
Later-phase secrets (Grafana/SMTP, Keycloak, oauth2-proxy, PIA) can be encrypted now and simply wired
|
|
when their component is enabled in `platform/kustomization.yaml`.
|