Przejdź do treści

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.yaml w korzeniu repo.
  • Klucz prywatny: Bitwarden, folder panel-aio, pozycja sops-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-allinfra/.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:

export SOPS_AGE_KEY_FILE=/sciezka/do/keys.txt

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.sh nie odczyta .env.prod.sops.yaml).
  • Nie pobierzesz sekretów Windmilla z secrets/.
  • Nie odzyskasz backupów (backrest_restic_password jest w secrets/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)

  1. SSH do hosta → incus exec <container>docker compose exec <db-service> mysql -u root -pALTER USER.
  2. sops <wpisz-relevant-plik>.sops.yaml → zaktualizuj wartość → save.
  3. Restart konsumenta: incus exec <container> -- docker compose restart <app>.
  4. Sprawdź logi: docker compose logs --tail=50 <app>.

Klucz API marketplace (np. Allegro client_secret)

  1. Wygeneruj nowy w panelu vendor (np. Allegro Developer Console).
  2. sops secrets/secrets.sops.yaml → podmień allegro_client_secret.
  3. 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)
  1. Restart panel jeśli sekret jest też w dashboard/.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 w infra/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.