Sekrety — SOPS + age¶
Wszystkie sekrety w repo (klucze API, hasła DB, refresh tokeny marketplace, klucze SMTP) są szyfrowane przez Mozilla SOPS kluczem age. Brak klucza age = brak dostępu do produkcji.
Krótko
- Mechanizm: SOPS + age (klucz publiczny w
.sops.yaml, prywatny w Bitwardenie). - Reguły szyfrowania:
.sops.yamlw korzeniu repo. - Klucz prywatny: Bitwarden, folder
panel-aio, pozycjasops-age-key. - OpenBao był rozważany — nie został uruchomiony (decyzja: SOPS wystarcza dla solo-operatora). Decyzja doc-013 + memory
project_task262_decisions.md.
Mapa sekretów — gdzie co leży¶
| Plik | Zawartość | Konsumenci |
|---|---|---|
secrets/secrets.sops.yaml |
Klucze API platform (Allegro/Amazon/eBay/Erli/Joom/Temu), hasła DB, backrest_restic_password, convex_prod_admin_key |
Windmill (variables refresh-flow), just prod-convex-* recipes, infra |
dashboard/.env.sops.yaml |
Sekrety dashboardu — CONVEX_DEPLOY_KEY, WINDMILL_TOKEN, OAuth client secrets per-platform |
Build dashboardu + runtime kontenera panel |
convex/.env.dev.sops.yaml |
Konfig Convex dev (CONVEX_SELF_HOSTED_URL, CONVEX_SELF_HOSTED_ADMIN_KEY dla dev) |
npx convex lokalnie po just decrypt-convex |
convex/.env.prod.sops.yaml |
jw. ale prod (convex.aiofactory.pl admin key) |
scripts/deploy-convex.sh (load in-place) |
windmill/<scope>.sops.yaml |
Sekrety windmillowe per-vendor / per-flow (przekazywane do wmill variable create) |
wmill sync push (jeśli skipVariables: false — u nas WYŁĄCZONE, patrz niżej) |
infra/secrets.sops.yaml |
Sekrety infra (API keys hostingów, DNS providerów, B2) | just decrypt-all → infra/.env.secrets; konsumowane przez infra/ (osobne repo) |
Reguły szyfrowania per ścieżka — .sops.yaml:
creation_rules:
- path_regex: dashboard/\.env\.sops\.yaml$
age: age1l7j98r3rgrwaf2gd6nnfxcna88fvwjvsl9wzj2vz7nplp3qxg3eq7adxdu
- path_regex: convex/.*\.sops\.yaml$
age: age1l7j98r3rgrwaf2gd6nnfxcna88fvwjvsl9wzj2vz7nplp3qxg3eq7adxdu
- path_regex: secrets/.*\.sops\.yaml$
age: age1l7j98r3rgrwaf2gd6nnfxcna88fvwjvsl9wzj2vz7nplp3qxg3eq7adxdu
- path_regex: windmill/.*\.sops\.yaml$
age: age1l7j98r3rgrwaf2gd6nnfxcna88fvwjvsl9wzj2vz7nplp3qxg3eq7adxdu
- age: age1l7j98r3rgrwaf2gd6nnfxcna88fvwjvsl9wzj2vz7nplp3qxg3eq7adxdu
Wszystko szyfrowane jednym kluczem age — model: jeden operator, jeden komputer, jeden Bitwarden. Nie wieloosobowy. Jeśli kiedyś dojdzie drugi developer — dodajemy drugi age recipient do każdego pliku (rerunsop), zamiast dzielić jeden klucz.
Klucz age — instalacja na nowej stacji¶
# 1. Sprawdź czy age zainstalowany
age --version || nix-env -iA nixpkgs.age # albo apt install age
# 2. Z Bitwardena (folder panel-aio, pozycja sops-age-key) skopiuj klucz prywatny
mkdir -p ~/.config/sops/age
chmod 700 ~/.config/sops/age
cat > ~/.config/sops/age/keys.txt <<'EOF'
# created: 2026-...
# public key: age1l7j98r3rgrwaf2gd6nnfxcna88fvwjvsl9wzj2vz7nplp3qxg3eq7adxdu
AGE-SECRET-KEY-1... <-- wklej z Bitwardena
EOF
chmod 600 ~/.config/sops/age/keys.txt
# 3. Test
sops --decrypt secrets/secrets.sops.yaml | head -5
SOPS automatycznie znajduje ~/.config/sops/age/keys.txt. Alternatywnie ustaw SOPS_AGE_KEY_FILE:
Utrata klucza age = utrata dostępu do produkcji
Klucz age jest jedyną drogą odszyfrowania sekretów w repo. Bez niego:
- Nie wdrożysz Convexa (
scripts/deploy-convex.shnie odczyta.env.prod.sops.yaml). - Nie pobierzesz sekretów Windmilla z
secrets/. - Nie odzyskasz backupów (
backrest_restic_passwordjest wsecrets/secrets.sops.yaml).
Backup klucza: Bitwarden cloud + dodatkowa kopia offline (np. wydruk QR + USB w sejfie). Bez niej: katastrofa nieodwracalna.
Workflow lokalny — odszyfrowanie¶
just decrypt-dashboard # dashboard/.env.sops.yaml → dashboard/.env
just decrypt-convex # convex/.env.dev.sops.yaml → convex/.env.local
just decrypt-all # infra/secrets.sops.yaml → infra/.env.secrets
Pliki *.env (po decrypt) są w .gitignore — nie wpadną przypadkiem do gita. Sprawdź git check-ignore -v dashboard/.env jeśli wątpisz.
Edycja sekretu — sops <plik>, nie ręczne odszyfrowanie + szyfrowanie
Do dodania / zmiany sekretu używaj sops secrets/secrets.sops.yaml — otworzy plik w $EDITOR w trybie odszyfrowanym, a po zapisie zaszyfruje z powrotem tym samym kluczem age. Ręczny sops -d > plain.yaml + edycja + sops -e plain.yaml > zaszyfrowany.yaml zostawia plain-text na dysku w plain.yaml — łatwo zapomnieć usunąć.
Sekrety Windmilla — skipVariables: true¶
Krytyczne — nie zmieniaj skipVariables: true w windmill/wmill.yaml
Windmillowe Variable są zarządzane out-of-band przez API (operator kliknie „dodaj zmienną" w UI lub skrypt f/tokens/refresh_* zapisuje refresh tokena). wmill sync push bez skipVariables: true wymazałby wszystkie nie-sekretne zmienne, których nie ma w repo — w tym f/tokens/convex_url, f/tokens/windmill_workspace, klucze API. Sprawdzone na 2026-04-23 — recovery wymagało ręcznego createVariable per zmienna.
Memory: feedback_wmill_sync_variables.md.
Jak dodać NOWĄ Windmillową Variable (poza syncem):
# Przez CLI:
wmill variable create f/tokens/new_var --value "secret_value" --is-secret
# Przez UI: panel.aiofactory.pl → Windmill → Variables → New
# Albo: wmill workspace switch prod → Windmill UI
# Albo MCP (z sesji Claude'a):
# mcp__windmill-prod__createVariable
Rotacja sekretów — wzorce¶
Hasło DB (np. WordPress, listmonk)¶
- SSH do hosta →
incus exec <container>→docker compose exec <db-service> mysql -u root -p→ALTER USER. sops <wpisz-relevant-plik>.sops.yaml→ zaktualizuj wartość → save.- Restart konsumenta:
incus exec <container> -- docker compose restart <app>. - Sprawdź logi:
docker compose logs --tail=50 <app>.
Klucz API marketplace (np. Allegro client_secret)¶
- Wygeneruj nowy w panelu vendor (np. Allegro Developer Console).
sops secrets/secrets.sops.yaml→ podmieńallegro_client_secret.- Wymuszone odświeżenie Windmillowych zmiennych:
# Pull nowy sekret z SOPS i upsert do Windmill variable
wmill variable update f/marketplace/allegro/client_secret --value "$(sops --extract '["allegro_client_secret"]' --decrypt secrets/secrets.sops.yaml)"
# Albo zatrigeruj refresh-flow (jeśli vendor go ma)
- Restart
paneljeśli sekret jest też wdashboard/.env.sops.yaml(część jest — np. OAuth client secrets dla redirect URI).
Klucz restic-password (backupy)¶
Rotacja restic-password = utrata starych backupów
restic szyfruje każdy snapshot kluczem chunk + master kluczem powiązanym z hasłem. Zmiana hasła nie reszyfruje istniejących snapshotów — restic key passwd doda nowe hasło ale STARE pozostaje ważne. Jeśli stare hasło wycieknie + B2 backup wycieknie — kompromitacja danych.
Pełna rotacja:
1. restic -r <repo> key add + nowe hasło → ZAPISZ w SOPS jako backrest_restic_password_new.
2. Po weryfikacji, że new key działa: restic -r <repo> key remove <old-id>.
3. Update secrets/secrets.sops.yaml — usuń stary, przemianuj nowy na backrest_restic_password.
4. Update Backrest UI plan-config (backrest.aiofactory.pl) — wklej nowe hasło.
OpenBao / Vaultwarden — przygotowane, NIE uruchomione¶
W toku projektu rozważano:
- OpenBao (fork HashiCorp Vaulta) jako runtime secret-manager — zamiast sekretów w envach. Decommissioned — solo operator + jeden komputer = SOPS-y wystarczą bez dodatkowego serwisu do utrzymania (decyzja: doc-013, memory
project_task262_decisions.md). - Vaultwarden (self-hosted Bitwarden) — magazyn haseł na własnej infra. Decyzja właściciela: zostajemy przy Bitwarden cloud (folder
panel-aio). Vaultwarden compose-config jest gotowy winfra/host-mom/dapps/vaultwarden/— uruchomienie ~5 min jeśli kiedyś trzeba.
Stan: w produkcji żaden runtime secret manager NIE działa. Sekrety są w plikach *.env wczytywanych przez compose env_file:, generowanych z SOPS w deploy time.
Powiązane¶
.sops.yaml(w korzeniu repo) — reguły szyfrowania per ścieżka.- Wdrożenia — gdzie sekrety są używane przez deploy-skrypty.
- Backupy — rola
backrest_restic_password, recovery. - Inwentarz Usług → Sekrety — szybki overview per-usługa.