Secrets Design Standard — p24-infra
Czytaj ten dokument PRZED zaprojektowaniem każdego rozwiązania które dotyka credentials. Nie po — przed.
Drzewo decyzyjne: gdzie trafiają secrets?
Dodaję nowy secret / projektuję coś co używa secrets
│
├─ Czy upstream wspiera OIDC / workload identity (np. GitHub Actions → AWS)?
│ └── TAK → użyj OIDC. Nie przechowuj klucza nigdzie. Koniec.
│
├─ Czy upstream oferuje auto-rotation (np. rotating API keys, short-lived tokens)?
│ └── TAK → wdrożenie przez auto-rotate, nie SOPS. Tylko bootstrap w SOPS.
│
├─ Czy secret jest używany WYŁĄCZNIE w GitHub Actions CI/CD?
│ └── TAK → GitHub Secrets. Nie do SOPS (zbędna duplikacja).
│ WYJĄTEK: jeśli ten sam secret jest używany też na serwerze — idzie do SOPS
│ i jest synchronizowany do GH Secrets przez secrets-sync.yml.
│
└─ We wszystkich pozostałych przypadkach → SOPS (secrets/*.env.sops)
Domyślna odpowiedź jeśli nie wiesz: SOPS.
Który plik SOPS?
| Secret dotyczy | Plik SOPS |
|---|---|
| Monitoring, Grafana, Prometheus, Wasabi p24-infra, SMTP, CF scoped, OVH | secrets/monitoring.env.sops |
| n8n, Redis, MongoDB prometheus user, GH_TOKEN (repo-write) | secrets/n8n-bms4.env.sops |
| WAHA, Traefik, SSL, vps-h1 services | secrets/vps-h1.env.sops |
| BMS root passwords, MongoDB rs0 admin | secrets/bms-servers.env.sops |
| Art Agency (Supabase, PayPal, KDP, Drive) | secrets/art-agency.env.sops |
| BrandPilot (Resend, Wasabi, Anthropic, Supabase) | secrets/brandpilot.env.sops |
| Infra admin (IONOS API, CF Global, WASABI_ADMIN, GH_PAT_ADMIN) | secrets/administration.env.sops |
Jeśli secret należy do nowego projektu którego tu nie ma — stwórz nowy plik secrets/<project>.env.sops według wzoru z docs/secrets-management.md §Adding a new secret.
Zasada izolacji: jeden secret na (usługę × host)
Reguła: każdy secret który da się rotować autonomicznie (własny klucz/token po stronie upstreamu, np. Wasabi IAM, klucze ingestion, API keys) i który jest konsumowany przez więcej niż jedną usługę lub więcej niż jeden host MUSI mieć osobny, unikalnie nazwany secret dla każdej pary (usługa × host). Nigdy nie współdziel jednego klucza między konsumentami, jeśli upstream potrafi wydać wiele niezależnych poświadczeń.
Dlaczego (blast radius):
- Izolacja wycieku — wyciek dotyka tylko jednego konsumenta; rotujesz jeden klucz, nie budzisz wszystkich pozostałych usług.
- Izolacja rotacji — rotacja jednego klucza nie może zepsuć niepowiązanej usługi współdzielącej ten sam sekret. Auto-rotation staje się bezpieczna „per usługa”.
- Least privilege — osobny klucz można związać z węższą polityką IAM (tylko bucket/ scope potrzebny temu hostowi).
- Czytelny audyt — logi dostępu jednoznacznie wskazują który host/usługa użył klucza.
Kiedy dzielić, a kiedy zostawić jeden secret
Secret jest autonomicznie rotowalny (upstream wydaje wiele niezależnych poświadczeń)?
│
├─ NIE → jeden secret; udokumentuj sprzężenie konsumentów w dev_r_services.
│ (np. współdzielony sekret podpisu webhooka, którego upstream nie potrafi rozmnożyć)
│
└─ TAK → konsumuje go 2+ usług LUB 2+ hostów?
├─ NIE (jeden konsument) → jeden secret wystarczy.
└─ TAK → PODZIEL: jeden nazwany secret na każdą parę (usługa × host).
Konwencja nazewnictwa
Format: {USŁUGA}_{CEL}_{HOST}_{RODZAJ} — wielkie litery, człony rozdzielone _.
Host to etykieta serwera (BMS3, BMS4, VPSI1, VPSH1).
| Scenariusz | ŹLE (współdzielony) | DOBRZE (per usługa × host) |
|---|---|---|
s3-microservice na bms-3 i bms-4, dostęp do Wasabi | WASABI_ACCESS_KEY_ID | S3MICROSERVICE_WASABI_BMS3_ACCESS_KEY_ID + S3MICROSERVICE_WASABI_BMS4_ACCESS_KEY_ID (i analogicznie _SECRET_ACCESS_KEY) |
| Klucz ingestion logów na kilku hostach | MEZMO_INGESTION_KEY (legacy, shared) | MEZMO_INGESTION_KEY_VPSI1 + MEZMO_INGESTION_KEY_VPSH1 |
Precedens w repo: docs/playbooks/secret-rotation-access-matrix.md już oznacza
MEZMO_INGESTION_KEY (legacy shared) do wymiany na klucze per-host —
patrz mezmo-key-rotation.md §B-legacy. Ta reguła podnosi ten wzorzec do rangi standardu.
Klucze zastane (grandfathering)
Istniejące współdzielone sekrety (np. legacy MEZMO_INGESTION_KEY) migrują do wariantu
per-host przy najbliższej planowej rotacji — nie wymuszamy natychmiastowej wymiany,
która byłaby breaking change. Nowe secrety od razu stosują regułę izolacji.
Wzorzec runtime: jak secret trafia do skryptu/kontenera?
Skrypt bash (serwer Linux)
# POPRAWNIE — sops exec-env wstrzykuje zmienne tylko na czas procesu
export SOPS_AGE_KEY_FILE="/home/claude-runner/.age/p24-infra-keys.txt"
sops exec-env secrets/monitoring.env.sops \
'bash scripts/moj-skrypt.sh'
# Dwa pliki SOPS jednocześnie (zagnieżdżone exec-env):
sops exec-env secrets/monitoring.env.sops \
'sops exec-env secrets/bms-servers.env.sops \
"bash scripts/moj-skrypt.sh --compare-prod"'
# Jeden klucz bez exec-env (czysty ekstrakt, nigdy nie drukuj $VAL):
THE_SECRET=$(sops --decrypt --input-type dotenv --output-type dotenv secrets/monitoring.env.sops \
| grep "^KEY_NAME=" | cut -d= -f2-)
some-command --token "$THE_SECRET"
unset THE_SECRETKontener Docker (docker-compose)
# POPRAWNIE — referencja do zmiennej, wartość pochodzi z .env (zdekryptowany przez secrets-sync)
services:
moj-service:
environment:
- MY_SECRET=${MY_SECRET} # wartość inject przez secrets-sync.yml lub sops exec-env
# BŁĘDNIE — hardkodowanie wartości
environment:
MY_SECRET: "abc123" # NIGDYSkrypt PowerShell (Windows dev)
# NAJLEPIEJ (#5298, ADR 003) — broker: dekryptuje we własnym procesie, udostępnia
# wartość tylko jako $env:MY_SECRET na czas komendy, i redaguje ją (raw + URL +
# base64 + JSON) z outputu. Zastępuje ręczny Select-String → $VAR → cmd.
$env:SOPS_AGE_KEY_FILE = "C:\Users\konar\.age\p24-infra-keys.txt"
.\scripts\sops-invoke.ps1 -SopsFile secrets\monitoring.env.sops `
-Keys '{"MY_SECRET":"MY_KEY"}' -Command 'some-command --token $env:MY_SECRET'
# POPRAWNIE (fallback, gdy broker niedostępny — np. bash worker do czasu bash-sibling)
# exec-env odpowiednik — sops exec-env nie istnieje w PS, użyj:
$env:MY_SECRET = (sops --decrypt --input-type dotenv --output-type dotenv secrets\monitoring.env.sops `
| Select-String "^MY_KEY=").ToString().Split("=",2)[1]
some-command --token $env:MY_SECRET
$env:MY_SECRET = "" # czyść po użyciuUI-only (login do przeglądarki, np. GitLab SaaS) → broker Playwright:
scripts/secret-broker/Invoke-PlaywrightWithSecret.ps1(gitlab-token-playwright-broker.md).
GitHub Actions
# POPRAWNIE — secret z GH Secrets (synced from SOPS via secrets-sync.yml)
- name: Run script
env:
MY_SECRET: ${{ secrets.MY_SECRET }}
run: bash scripts/moj-skrypt.shAnty-wzorce — NIGDY nie rób tego
| Anty-wzorzec | Dlaczego złe | Jak naprawić |
|---|---|---|
source /root/.backup-env w skrypcie | Plaintext na dysku, ominięcie SOPS | sops exec-env secrets/X.env.sops 'bash skrypt.sh' |
export SECRET=abc123 w skrypcie | Hardkodowanie, wyciek do ps aux | SOPS + exec-env |
cat .env | grep KEY w skrypcie | Drukuje wartość do logu | sops ... | grep KEY | cut -d= -f2- + unset |
Czytanie z /root/.env.bak jako primary | .env.bak to stary fallback z CI, może być przestarzały | SOPS jest source of truth |
| Wstrzykiwanie secret przez argument CLI | Widoczny w ps aux, logu systemowym | Przez zmienną środowiskową z exec-env |
echo $SECRET / print(secret) w logach | Wyciek do Mezmo, logów CI, pull requestów | Nigdy nie loguj wartości — tylko [SECRET SET] / [SECRET MISSING] |
Fallback do .env.local jeśli SOPS niedostępny | SOPS jest zawsze dostępny jeśli masz age key | Napraw age key zamiast omijać SOPS |
SOPS_AGE_KEY inline w skrypcie | Prywatny klucz age w plaintext | SOPS_AGE_KEY_FILE + klucz w bezpiecznej lokalizacji |
Checklist przed commitowaniem nowego rozwiązania
Przed każdym PR który dodaje lub modyfikuje obsługę secrets:
□ Secret idzie do właściwego pliku SOPS (patrz tabela wyżej)
□ Skrypt/kontener używa sops exec-env lub zmiennej z docker-compose (nie source .env)
□ Żadna wartość secret nie jest hardkodowana w kodzie, config, README ani komentarzu
□ Żadna wartość secret nie pojawia się w logach (grep "SECRET\|KEY\|PASS\|TOKEN" w output)
□ Po rotacji: zaktualizowane dev_r_ops_events + secrets-rotation-log.md
□ Nowy secret ma wpis w dev_r_services (element_type='credential')
□ Secret rotowalny autonomicznie i używany przez 2+ usług/hostów → osobny nazwany
secret per (usługa × host) wg konwencji `{USŁUGA}_{CEL}_{HOST}_{RODZAJ}` (nie współdziel)
□ Jeśli secret ma manual rotation: istnieje playbook lub jest on rozszerzony
Weryfikacja: czy SOPS działa na tym serwerze?
# Sprawdź czy age key jest dostępny
ls -la /home/claude-runner/.age/p24-infra-keys.txt
# Test decrypt (nie drukuje wartości — tylko sprawdza exit code)
export SOPS_AGE_KEY_FILE="/home/claude-runner/.age/p24-infra-keys.txt"
sops --decrypt --input-type dotenv --output-type dotenv \
/opt/p24-infra/secrets/monitoring.env.sops > /dev/null && echo "SOPS OK" || echo "SOPS FAIL"Jeśli SOPS nie działa: nie fallbackuj do .env.local. Napraw age key (patrz docs/playbooks/master-keys-bootstrap.md).
Dlaczego SOPS, nie Infisical/Doppler/Vault?
- Infisical CE: decommissioned 2026-06-21 — centralizowany serwer = single point of failure
- Doppler/Vault: zewnętrzna zależność, wymaga network call przy każdym uruchomieniu
- SOPS+age: secrets są w git (encrypted), działają offline, audytowalny diff, zero dodatkowej infrastruktury
- GitHub Secrets: CI/CD only — nie ma dostępu z serwera bez dodatkowego workflow
SOPS jest “boring tech” — prosty, niezawodny, zero zależności poza binarką i age key.
Powiązane dokumenty
docs/secrets-management.md— jak dodać/rotować/odtworzyć kluczedocs/secrets-sops-age.md— techniczny setup SOPS+agedocs/playbooks/sops-windows-crlf.md— Windows CRLF/BOM gotchasdocs/playbooks/sops-edit-operations.md— bezpieczna edycja plików SOPSdocs/playbooks/static-api-key-incident-rotation.md— co robić przy wyciekudocs/infrastructure-standard.md §Credentials— rejestracja w dev_r_services