Playbook: SOPS Environment Bootstrap — tworzenie świeżych plików od zera

Owner: p24-infra · Updated: 2026-07-05
Status: specyfikacja — częściowo zaimplementowana (patrz §Luki)


Zakres

Używaj tego playbooka gdy:

  • Tworzysz plik SOPS dla nowego środowiska (np. secrets/et-lager.env.sops)
  • Chcesz zastąpić istniejący plik SOPS świeżymi kluczami (pełna rotacja wszystkich kluczy w jednym przejściu)
  • Budujesz nowy projekt i potrzebujesz wygenerować wszystkie poświadczenia od zera

Dla rotacji pojedynczego klucza w istniejącym pliku → użyj docs/playbooks/secret-manager.md §Update an existing key value.


Inwentarz istniejących skryptów

Co MAMY (i do czego służy)

SkryptCelUżywa w bootstrap?
scripts/rotate/update-sops-keys.ps1Czyta klucze z plików tymczasowych i łata istniejący SOPS✅ TAK — krok 3 (patch individual keys)
scripts/rotate/rotate-now.ps1Auto-rotuje SMTP_PASSWORD (Mailgun API) + discord_radieu_password (Playwright)✅ TAK — Tier 1/2 keys, po stworzeniu pliku
scripts/rotate/n8n-bms4-api-key.ps1Rotuje N8N_API_KEY przez REST✅ TAK — dla n8n-bms4 środowiska
.github/workflows/secrets-sync.ymlDeploys SOPS → serwery, Vercel, GH Secrets✅ TAK — krok końcowy (dystrybucja)
.github/workflows/rotate-schedule.ymlTygodniowy cron dla Tier 1 kluczy❌ NIE — nie nadaje się do bootstrap
docs/playbooks/secret-manager.md §Add a new keyBezpieczny wzorzec PowerShell: TEMP_PLAIN + TEMP_ENC + canary + Move-Item✅ TAK — krok 2 (tworzenie pliku)

Czego NIE MAMY (luki — patrz §Co zostało do zrobienia)

Brakujący elementOpis
scripts/bootstrap/new-sops-env.ps1Skrypt tworzący pusty plik SOPS z szablonu kluczy
docs/sops-templates/<env>.keysMaszyna-czytelne szablony listy kluczy per środowisko
Bootstrap orchestratorSkrypt iterujący przez klucze, wywołujący Tier 1 API / Playwright / prompt dla Tier 3
Walidator kompletnościSprawdza czy wszystkie wymagane klucze są obecne w danym pliku SOPS

Workflow bootstrap — specyfikacja

Faza 0 — przygotowanie

$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
$ENV_NAME = "et-lager"   # np. monitoring, n8n-bms4, et-lager, art-agency
$SOPS_FILE = "C:\code_2026\p24-infra\secrets\$ENV_NAME.env.sops"
$TEMP_PLAIN = "C:\code_2026\p24-infra\secrets\$ENV_NAME-bootstrap-tmp.env.sops"
$TEMP_ENC   = "C:\code_2026\p24-infra\secrets\$ENV_NAME-bootstrap-enc-tmp.env.sops"

Jeśli plik już istnieje: zdecyduj czy:

  • a) Patchujesz istniejący (dekrypt → modyfikacja → re-encrypt) — standardowa ścieżka
  • b) Budujesz od zera (pusty $TEMP_PLAIN, następnie dodajesz klucze jeden po jednym)

Ścieżka (b) jest destrukcyjna — stare wartości zostaną utracone. Upewnij się, że stary plik jest zarchiwizowany lub że rotacja była zaplanowana.

Faza 1 — stworzenie szkieletu SOPS

Jeśli plik SOPS nie istnieje:

# Stwórz pusty plik tymczasowy (jeden placeholder key żeby SOPS miał co zaszyfrować)
[System.IO.File]::WriteAllText(
  $TEMP_PLAIN,
  "BOOTSTRAP_PLACEHOLDER=__replace_me__`n",
  [System.Text.UTF8Encoding]::new($false)
)
 
# Zaszyfruj — .sops.yaml automatycznie dobiera recipients z path_regex
sops --encrypt --input-type dotenv --output-type dotenv --output $TEMP_ENC $TEMP_PLAIN
if ($LASTEXITCODE -ne 0) { throw "Encrypt failed on empty bootstrap" }
 
# Canary
sops --decrypt --input-type dotenv --output-type dotenv $TEMP_ENC | Out-Null
if ($LASTEXITCODE -ne 0) { Remove-Item $TEMP_PLAIN, $TEMP_ENC -Force -ErrorAction SilentlyContinue; throw "SOPS corrupt" }
 
Move-Item $TEMP_ENC $SOPS_FILE -Force
Remove-Item $TEMP_PLAIN -Force -ErrorAction SilentlyContinue
Write-Host "Szkielet SOPS: $SOPS_FILE"

Faza 2 — wypełnianie kluczami

Dla każdego wymaganego klucza, stosuj tier-routing:

Klucz do bootstrapu
│
├─ Tier 1 (REST API, pełna automatyzacja)
│   └─ Wywołaj provider API → zapisz do $env:NEW_VALUE → SOPS patch
│
├─ Tier 2 (Playwright, admin API z prereqvisites)
│   └─ Wywołaj scripts/rotate/<key>.js z TOKEN_OUT_FILE → odczytaj → SOPS patch
│
└─ Tier 3 (UI-only, 2FA, OAuth)
    └─ Człowiek generuje klucz → wkleja w TERMINALU do $env:NEW_VALUE (nie w chat)
    └─ secret-manager session robi SOPS patch

Wzorzec SOPS patch dla każdego klucza (z secret-manager.md §Update an existing key value):

# $env:NEW_VALUE zawiera nowy klucz — nigdy nie drukuj jego wartości
$plain = sops --decrypt --input-type dotenv --output-type dotenv $SOPS_FILE
if ($LASTEXITCODE -ne 0) { throw "Decrypt failed" }
 
if ($plain -match "(?m)^KEY_NAME=") {
  # Klucz istnieje — zaktualizuj
  $content = ($plain -join "`n") + "`n"
  $content = $content -replace "(?m)^KEY_NAME=.*$", "KEY_NAME=$env:NEW_VALUE"
} else {
  # Klucz nie istnieje — dodaj (i usuń placeholder jeśli nadal jest)
  $filtered = $plain | Where-Object { $_ -notmatch "^BOOTSTRAP_PLACEHOLDER=" }
  $content = ($filtered -join "`n") + "`nKEY_NAME=$env:NEW_VALUE`n"
}
 
[System.IO.File]::WriteAllText($TEMP_PLAIN, $content, [System.Text.UTF8Encoding]::new($false))
$content = ""; $plain = @()
 
sops --encrypt --input-type dotenv --output-type dotenv --output $TEMP_ENC $TEMP_PLAIN
if ($LASTEXITCODE -ne 0) {
  @($TEMP_PLAIN, $TEMP_ENC) | Where-Object { $_ -and (Test-Path $_) } | ForEach-Object { Remove-Item $_ -Force -ErrorAction SilentlyContinue }
  throw "Encrypt failed at KEY_NAME"
}
Remove-Item $TEMP_PLAIN -Force -ErrorAction SilentlyContinue
 
sops --decrypt --input-type dotenv --output-type dotenv $TEMP_ENC | Out-Null
if ($LASTEXITCODE -ne 0) {
  Remove-Item $TEMP_ENC -Force -ErrorAction SilentlyContinue
  throw "SOPS corrupt after KEY_NAME — NOT replacing production file"
}
Move-Item $TEMP_ENC $SOPS_FILE -Force
$env:NEW_VALUE = ""
Write-Host "KEY_NAME: OK"

Faza 3 — walidacja kompletności

Po wypełnieniu wszystkich kluczy:

# Wylistuj wszystkie klucze (bez wartości) i porównaj z oczekiwaną listą
$actualKeys = (sops --decrypt --input-type dotenv --output-type dotenv $SOPS_FILE) |
  Where-Object { $_ -match "^[A-Z_]+=." } |
  ForEach-Object { $_.Split("=")[0] }
 
Write-Host "Klucze obecne: $($actualKeys.Count)"
$actualKeys | Sort-Object
 
# Porownaj z szablonem:
# .\scripts\bootstrap\validate-sops-env.ps1 -Env <env>

Faza 4 — commit i dystrybucja

git add secrets\<env>.env.sops
git commit -m "feat(#ISSUE): bootstrap secrets/<env>.env.sops — <N> keys"
git push origin HEAD  # branch → PR → merge → secrets-sync auto-trigger

Po merge na mainsecrets-sync.yml auto-triggeruje i deployuje na wszystkie cele zdefiniowane dla danego środowiska.


Mapowanie środowisk → cele dystrybucji

Plik SOPSCel dystrybucji
monitoring.env.sopsvps-i1 /opt/p24-infra/monitoring/.env
n8n-bms4.env.sopsbms-4 /opt/p24-infra/bms-4/.env
vps-h1.env.sopsvps-h1 /root/.env (uwaga: SSH broken — issue #1474)
bms-servers.env.sopsOperator decrypt on demand — NIE deployowane automatycznie
art-agency.env.sopsArt-Agency/.env.local (manual sync)
brandpilot.env.sopsVercel prj_brandpilot
et-operational-platform.env.sopsVercel prj_ziLl911FOYLAeukQujL4NjxR4eWy
et-lager.env.sopsNIE SKONFIGUROWANE W secrets-sync.yml — patrz §Decyzje
whatsup.env.sopsApp env (manual sync)

Co zostało do zrobienia

Skrypty — brakuje

  • scripts/bootstrap/new-sops-env.ps1 — skrypt tworzący nowy plik SOPS z szablonu:

    • przyjmuje $Environment (nazwa pliku)
    • wczytuje docs/sops-templates/<env>.keys (lista kluczy + tier)
    • iteruje: Tier 1 → API call, Tier 2 → Playwright, Tier 3 → czeka na $env:VALUE_<KEY>
    • po każdym kluczu robi canary
    • na końcu robi listę kluczy + walidację kompletności
  • docs/sops-templates/<env>.keys — szablony per środowisko (format: KEY_NAME|tier|provider|rotation_cmd):

    SMTP_PASSWORD|1|mailgun-api|scripts/rotate/rotate-now.ps1
    SENTRY_AUTH_TOKEN|2|playwright|scripts/rotate/sentry-auth-token.js
    MAILGUN_API_KEY|3|browser-ui|manual
    

    Brakuje dla: monitoring, n8n-bms4, et-lager, art-agency, brandpilot, et-operational-platform

  • Walidator kompletności — skrypt sprawdzający czy wszystkie wymagane klucze (z szablonu) są obecne w pliku SOPS; uruchamiany jako pre-commit hook lub CI check

Workflow — brakuje

  • secrets-sync.yml obsługa et-lager — brak job sync-et-lager (patrz §Decyzje)
  • Cel dystrybucji dla nowych środowisk — gdy dodajesz nowy plik SOPS, musisz ręcznie dodać odpowiedni job do secrets-sync.yml

Otwarte decyzje

D1 — Co to znaczy “od nowa” w kontekście istniejących plików?

Dwie interpretacje, różne konsekwencje:

OpcjaCo się dziejeKiedy stosować
A: patch istniejącegoDekrypt → rotacja wybranych kluczy → re-encryptRutynowa rotacja, rotacja grupowa
B: buduj od zeraPusty TEMP_PLAIN, dodajesz klucze od nowaPodejrzenie kompromitacji wszystkich kluczy, nowe środowisko

Opcja B niszczy bieżące wartości — stary plik musi być zarchiwizowany zanim go usuniesz.

D2 — et-lager.env.sops — gdzie deployować?

Plik secrets/et-lager.env.sops istnieje ale secrets-sync.yml nie ma dla niego job. Potrzebna decyzja:

  • Jaki serwer lub platforma jest celem? (vps-i1? bms-4? Vercel?)
  • Jaka ścieżka docelowa?
  • Czy dodać job sync-et-lager do secrets-sync.yml?

D3 — Szablony kluczy (docs/sops-templates/) — czy tworzyć?

Plusy: umożliwia walidację kompletności, dokumentuje wymagane klucze, upraszcza bootstrap nowych środowisk.
Minusy: dodatkowy plik do utrzymania, może się rozjechać z rzeczywistością.

Rekomendacja: tak — przynajmniej dla monitoring i n8n-bms4 (największe pliki, najwięcej kluczy).

D4 — Bootstrap orchestrator — skrypt lokalny czy GH Issue workflow?

OpcjaOpis
Lokalny PowerShellscripts/bootstrap/new-sops-env.ps1 — dev machine, interaktywny, może promptować o Tier 3 keys
GH Issue + secrets-manager workerIssue z listą kluczy → worker na bms-4 generuje co może (Tier 1/2) → komentuje co czeka na człowieka (Tier 3)

Lokalny PS jest szybszy dla Tier 3 (brak opóźnienia z GH Issue). Worker lepszy dla Tier 1/2 batch. Rekomendacja: oba — lokalny dla bootstrap interaktywny, worker dla rotacji grupowej Tier 1/2.

D5 — Kolejność bootstrapu (DAG zależności per środowisko)

monitoring.env.sops

KROK 1 — brak zależności (generuj pierwsze):
  SMTP_PASSWORD         [Tier 1: MAILGUN_ADMIN_API_KEY wymagany → musi być w SOPS]
  GRAFANA_ADMIN_PASSWORD [Tier 1: bcrypt + grafana-cli]
  P24_INFRA_WASABI_*    [Tier 1: WASABI_ADMIN_ACCESS_KEY wymagany]
  WASABI_ACCESS_KEY/SECRET [Tier 1: jak wyżej]
  CF_API_TOKEN + inne CF [Tier 1: CF_GLOBAL_API_KEY wymagany]
  LOKI / PROMETHEUS / STATUS basic auth [Tier 1: random gen]
  EXTERNAL_DB_SYM_KEY   [Tier 1: random gen]

KROK 2 — zależy od Kroku 1:
  N8N_BMS4_API_KEY      [Tier 1: n8n musi byc uruchomione; wymaga BMS4_N8N_API_KEY z n8n-bms4]
  SUPABASE_GRAFANA_PASSWORD [Tier 1: wymaga działającego SUPABASE_ACCESS_TOKEN]

KROK 3 — human-action (rownolegle do Kroku 1/2):
  SUPABASE_SERVICE_KEY/ROLE_KEY [Tier 3: Supabase dashboard]
  GH_TOKEN              [Tier 3: GitHub PAT]
  ATRAX_*               [Tier 3: Atrax portal]
  VERCEL_TOKEN          [Tier 3: Vercel dashboard]

KROK 4 — po Kroku 3 (uzywa human-provided values):
  WAP_OPENAI_KEY_MINI   [Tier 1: wymaga OPENAI_ADMIN_KEY z Kroku 3]
  CLOUDFLARE_TOKEN_* dalsze [Tier 1: wymaga CF_GLOBAL_API_KEY]

n8n-bms4.env.sops

UWAGA: N8N_ENCRYPTION_KEY — NIGDY nie rotowac bez migracji credentials n8n.
       Jesli trzeba zmienic: dokumentuj wszystkie credentials przechowywane w n8n
       i migruj reczne po zmianie klucza.

KROK 1 — brak zależności:
  REDIS_PASSWORD         [Tier 3: human — przed restart kontenera]
  N8N_DB_PASSWORD        [Tier 3: human — wymaga ALTER USER + restart]
  MONGODB_RS0_ADMIN_PASSWORD [Tier 3: human — P0, sprawdz bms-servers.env.sops]
  MONGODB_RS0_PROMETHEUS_PASSWORD [Tier 1: po zmianie admin password]
  ANTHROPIC_API_KEY      [Tier 1: Anthropic API]
  TELEGRAM_BOT_TOKEN     [Tier 1: BotFather]

KROK 2 — zalezy od uruchomionego n8n (Krok 1 gotowy + n8n zrestartowany):
  BMS4_N8N_API_KEY      [Tier 1: n8n REST API]
  N8N_GPS_SYNC_WEBHOOK_URL [Tier 1: n8n webhook]
  N8N_HU_SP_REPORT_SECRET [Tier 1: random]

KROK 3 — human-action (rownolegle):
  GH_TOKEN / GITHUB_PAT_* [Tier 3: GitHub]
  LINKEDIN_ACCESS_TOKEN  [Tier 2: OAuth refresh]
  RABBITMQ_DEFAULT_PASS  [Tier 3: human]
  GITHUB_APP_PRIVATE_KEY_B64 [Tier 3: GitHub App settings]

et-lager.env.sops

STATUS: niekompletny szablon — uzupelnij docs/sops-templates/et-lager.keys
        zanim zaczniesz bootstrap.

KROK 0 — sprawdz et-lager repo:
  gh api repos/radieu/et-lager/contents/.env.example | jq -r '.content' | base64 -d
  lub: vercel env ls --project <et-lager-project-name>
  Uzupelnij docs/sops-templates/et-lager.keys o wszystkie klucze.

KROK 1 — zalezy od zewnetrznych serwisow (musza istniec przed bootstrapem):
  MONGODB_URI            [Tier 3: MongoDB Atlas → Connect → Application]
                         UWAGA: URI zawiera uzytkownika i haslo — wygeneruj
                         dedykowanego database usera w Atlas dla et-lager.

KROK 2 — Supabase (jesli uzywany):
  SUPABASE_SERVICE_ROLE_KEY [Tier 3: Supabase dashboard → Settings → API]
  NEXT_PUBLIC_SUPABASE_ANON_KEY [Tier 3: jak wyzej]

et-operational-platform.env.sops

KROK 1 — brak zaleznosci:
  CRON_SECRET            [Tier 1: random]
  EXTERNAL_DB_SYM_KEY    [Tier 1: sync z monitoring.env.sops — ta sama wartosc]
  ANTHROPIC_API_KEY      [Tier 1: Anthropic API]

KROK 2 — wymaga dzialajacego n8n (n8n-bms4 musi byc gotowe):
  N8N_ATRAX_REPORT_WEBHOOK_URL [Tier 1: n8n webhook — sync z monitoring]
  N8N_GPS_SYNC_SECRET    [Tier 1: sync z n8n-bms4]
  N8N_HU_SP_REPORT_SECRET [Tier 1: sync z n8n-bms4]

UWAGA synchronizacja: kilka kluczy musi byc identycznych w wielu plikach SOPS:
  N8N_ATRAX_REPORT_SECRET    — monitoring + et-op (ta sama wartosc)
  N8N_GPS_SYNC_SECRET        — n8n-bms4 + et-op (ta sama wartosc)
  EXTERNAL_DB_SYM_KEY        — monitoring + et-op (ta sama wartosc)
  P24_DISCORD_*_WEBHOOK_URL  — monitoring + et-op (ta sama wartosc)
  QUEUE_API_KEY              — monitoring + et-op + brandpilot + art-agency (ta sama)

  ENFORCEMENT (#5276): powyzsze niezmienniki "ta sama wartosc" sa pilnowane przez
  scripts/check-secret-invariants.sh (deklaracja w scripts/secret-invariants.conf),
  uruchamiane w CI przez .github/workflows/lint-secret-invariants.yml na kazdym PR/pushu
  dotykajacym secrets/**. Porownuje SHA-256 wartosci (nigdy nie drukuje wartosci) i failuje
  gdy ktorykolwiek mirror sie rozjedzie — to jest guard na klase outage'u #5273. Dodajac tu
  nowy klucz cross-project, dopisz go tez do scripts/secret-invariants.conf.

KROK 3 — human-action:
  SUPABASE_SERVICE_ROLE_KEY  [Tier 3: Supabase dashboard]
  NEXT_PUBLIC_SUPABASE_ANON_KEY [Tier 3: Supabase dashboard]
  PINBOX24_MONGODB_URI       [Tier 3: MongoDB Atlas]

Kto musi dzialac zanim cokolwiek ruszysz

Przed bootstrapem ANY srodowiska — potrzebne master keys:

Master keyGdzieUmozliwia
WASABI_ADMIN_ACCESS_KEYadministration.env.sopsWasabi IAM Tier 1 keys
CF_GLOBAL_API_KEYadministration.env.sopsCloudflare Tier 1 keys
MAILGUN_ADMIN_API_KEYmonitoring.env.sopsSMTP_PASSWORD Tier 1
BMS4_N8N_API_KEY (po uruchomieniu n8n)n8n-bms4.env.sopsn8n webhook Tier 1
Playwright browserlokalnySENTRY, discord_radieu_password, VERCEL Tier 2

Powiązane playbooki

TematPlik
Bezpieczny zapis SOPS na Windowsdocs/playbooks/sops-windows-patterns.md
Dodanie pojedynczego kluczadocs/playbooks/secret-manager.md §Add a new key
Dystrybucja do serwerówdocs/playbooks/secret-manager.md §Distribution chain
Tier-klasyfikacja kluczydocs/playbooks/secret-rotation-access-matrix.md
Bootstrap master keys (age, GH_PAT)docs/playbooks/master-keys-bootstrap.md