Add sops-nix secrets + dev defaults so local stack runs with zero setup
check / flake-check (pull_request) Has been cancelled

- sops-nix flake input, age + ssh-to-age in devShell, .sops.yaml with
  per-host recipients (real pubkeys still TODO until lars bootstraps).
- nix/modules/gebos-secrets.nix centralises per-host secret subsets and
  renders /run/secrets/gebos-env via sops.templates. Service modules now
  read EnvironmentFile= from services.gebos.secrets.envFile instead of
  the placeholder /etc/gebos/secrets.env.
- Falls back to a writeText env file when nix/secrets/secrets.yaml is
  absent, so `nix flake check` and first-boot eval work pre-bootstrap.
- nix/secrets/README.md walks through age key + sops bootstrap.
- Dev defaults: ingester binary falls back to local tcp/postgres URLs,
  process-compose adds mosquitto and wires GEBOS_MQTT_BROKER /
  GEBOS_POSTGRES_URL / GEBOS_POSTGRES_PASSWORD + VITE_SUPABASE_URL /
  VITE_SUPABASE_ANON_KEY, frontend defaults to 127.0.0.1:8000 in DEV.

Refs #23 (comment #110).
This commit is contained in:
Klaus
2026-05-30 00:17:13 +00:00
parent 2d57db8182
commit 2dd5ed5877
17 changed files with 375 additions and 28 deletions
+71
View File
@@ -0,0 +1,71 @@
# Secrets
Managed with [sops-nix](https://github.com/Mic92/sops-nix) + [age](https://github.com/FiloSottile/age).
## Layout
- `.sops.yaml` (repo root) — recipients and per-file creation rules.
- `nix/secrets/secrets.yaml.example` — plaintext template (committed).
- `nix/secrets/secrets.yaml` — sops-encrypted real values (committed once created).
Plaintext values never enter the Nix store or disk outside tmpfs. sops-nix
decrypts on activation into `/run/secrets/<name>` with declared owner/mode,
then renders `gebos-env` from those placeholders for systemd `EnvironmentFile=`.
## First-time bootstrap
```sh
# 1. Generate your personal age key (keep this private, NEVER commit).
mkdir -p ~/.config/sops/age
age-keygen -o ~/.config/sops/age/keys.txt
# → "Public key: age1abc..." ← paste this into .sops.yaml as &dev_<you>
# 2. Get each host's age pubkey (one per host).
# On each host as root:
ssh-to-age < /etc/ssh/ssh_host_ed25519_key.pub
# → "age1xyz..." ← paste into .sops.yaml as &host_<name>
# 3. Edit .sops.yaml — replace every "age1TODO_..." with the real pubkeys above.
# 4. Create the real secrets file from the template, fill in real values, encrypt.
cp nix/secrets/secrets.yaml.example nix/secrets/secrets.yaml
$EDITOR nix/secrets/secrets.yaml
sops -e -i nix/secrets/secrets.yaml
# 5. Commit .sops.yaml + the encrypted secrets.yaml.
git add .sops.yaml nix/secrets/secrets.yaml
git commit -m "secrets: initial sops bootstrap"
```
## Editing later
```sh
sops nix/secrets/secrets.yaml # opens $EDITOR, re-encrypts on save
```
## Rotation
- **Leaked value** → `sops nix/secrets/secrets.yaml`, change value, redeploy.
- **New host** → add its age pubkey to `.sops.yaml`, then `sops updatekeys nix/secrets/secrets.yaml`.
- **Compromised dev key** → remove from `.sops.yaml`, `sops updatekeys`, redeploy.
## Generating value material
| Field | How |
| ------------------------------ | ------------------------------------------------------------ |
| `supabase_postgres_password` | `openssl rand -hex 32` |
| `supabase_jwt_secret` | `openssl rand -hex 32` (must be ≥32 chars) |
| `supabase_anon_key` | JWT signed with `jwt_secret`, `role: anon`, long expiry |
| `supabase_service_role_key` | JWT signed with `jwt_secret`, `role: service_role` |
| `supabase_dashboard_password` | `openssl rand -hex 16` (Studio basic-auth, username `gebos`) |
| `ingester_postgres_password` | `openssl rand -hex 32` |
| `postgres_admin_password` | `openssl rand -hex 32` |
Self-hosted Supabase docs have a one-liner for signing the two JWTs offline.
## Local development
You do **not** need to bootstrap secrets for `nix run .#dev`. The dev process-compose
stack and the ingester binary both default to local-only values
(`GEBOS_POSTGRES_URL`, `GEBOS_MQTT_BROKER`, `GEBOS_POSTGRES_PASSWORD`, plus the
JWT/anon-key pair the frontend needs). See `nix/dev/process-compose.nix`.
+29
View File
@@ -0,0 +1,29 @@
# Template for nix/secrets/secrets.yaml — the sops-encrypted store.
#
# To bootstrap:
# 1. cp nix/secrets/secrets.yaml.example nix/secrets/secrets.yaml
# 2. Replace the placeholder values below with real ones.
# (See nix/secrets/README.md for how to generate JWT_SECRET / ANON_KEY /
# SERVICE_ROLE_KEY etc.)
# 3. sops -e -i nix/secrets/secrets.yaml
# (requires sops + age + the recipients in .sops.yaml to be real keys)
#
# Once encrypted, edit in place with: sops nix/secrets/secrets.yaml
#
# Local dev does NOT need this file at all — process-compose.nix bakes in
# dev defaults and the NixOS modules fall back to a writeText env file when
# nix/secrets/secrets.yaml is absent. This file is only consumed at activation
# time on real hosts.
# Supabase compose stack (consumed on app-host by gebos-supabase.service)
supabase_postgres_password: "replace-me-openssl-rand-hex-32"
supabase_jwt_secret: "replace-me-openssl-rand-hex-32-minimum-32-chars"
supabase_anon_key: "replace-me-jwt-signed-with-jwt_secret-role-anon"
supabase_service_role_key: "replace-me-jwt-signed-with-jwt_secret-role-service_role"
supabase_dashboard_password: "replace-me-studio-basic-auth-password"
# Ingester (consumed on mqtt-ingest by gebos-ingester.service)
ingester_postgres_password: "replace-me-openssl-rand-hex-32"
# Postgres bootstrap (consumed on db-host by a one-shot ALTER ROLE unit)
postgres_admin_password: "replace-me-openssl-rand-hex-32"