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 dotyczyPlik SOPS
Monitoring, Grafana, Prometheus, Wasabi p24-infra, SMTP, CF scoped, OVHsecrets/monitoring.env.sops
n8n, Redis, MongoDB prometheus user, GH_TOKEN (repo-write)secrets/n8n-bms4.env.sops
WAHA, Traefik, SSL, vps-h1 servicessecrets/vps-h1.env.sops
BMS root passwords, MongoDB rs0 adminsecrets/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 WasabiWASABI_ACCESS_KEY_IDS3MICROSERVICE_WASABI_BMS3_ACCESS_KEY_ID + S3MICROSERVICE_WASABI_BMS4_ACCESS_KEY_ID (i analogicznie _SECRET_ACCESS_KEY)
Klucz ingestion logów na kilku hostachMEZMO_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_SECRET

Kontener 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"            # NIGDY

Skrypt 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życiu

UI-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.sh

Anty-wzorce — NIGDY nie rób tego

Anty-wzorzecDlaczego złeJak naprawić
source /root/.backup-env w skrypciePlaintext na dysku, ominięcie SOPSsops exec-env secrets/X.env.sops 'bash skrypt.sh'
export SECRET=abc123 w skrypcieHardkodowanie, wyciek do ps auxSOPS + exec-env
cat .env | grep KEY w skrypcieDrukuje wartość do logusops ... | grep KEY | cut -d= -f2- + unset
Czytanie z /root/.env.bak jako primary.env.bak to stary fallback z CI, może być przestarzałySOPS jest source of truth
Wstrzykiwanie secret przez argument CLIWidoczny w ps aux, logu systemowymPrzez zmienną środowiskową z exec-env
echo $SECRET / print(secret) w logachWyciek do Mezmo, logów CI, pull requestówNigdy nie loguj wartości — tylko [SECRET SET] / [SECRET MISSING]
Fallback do .env.local jeśli SOPS niedostępnySOPS jest zawsze dostępny jeśli masz age keyNapraw age key zamiast omijać SOPS
SOPS_AGE_KEY inline w skrypciePrywatny klucz age w plaintextSOPS_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ć klucze
  • docs/secrets-sops-age.md — techniczny setup SOPS+age
  • docs/playbooks/sops-windows-crlf.md — Windows CRLF/BOM gotchas
  • docs/playbooks/sops-edit-operations.md — bezpieczna edycja plików SOPS
  • docs/playbooks/static-api-key-incident-rotation.md — co robić przy wycieku
  • docs/infrastructure-standard.md §Credentials — rejestracja w dev_r_services