docs(bezpieczeństwo): runbook rotacji sekretów + poprawka nieaktualnej treści LOG-33
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m31s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m29s
Testy / Testy warstwy bazodanowej (ochrona baz) (pull_request) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 11s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 8s
build / build (push) Successful in 14s
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m31s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m28s
Testy / Testy warstwy bazodanowej (ochrona baz) (push) Successful in 9m28s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 12s
Testy / Kontrola składni wszystkich warstw (push) Successful in 8s

Wymaganie mówiło o restarcie „WSZYSTKICH TRZECH usług" przy zmianie INTERNAL_TOKEN.
Sprawdzenie żywych deploymentów pokazuje, że token czytają CZTERY: data, logic,
presentation ORAZ render (doszedł przy PRE-24, a wymaganie tego nie nadgoniło).
Kto zrobiłby rotację literalnie, zostawiłby render ze starym tokenem i usługa po
cichu przestałaby się dogadywać — dokładnie ta awaria, przed którą wymaganie
ostrzega. Treść w arkuszu poprawiona.

docs/log33-sekrety-i-rotacja.md:
- MACIERZ zależności wyliczona z deploymentów, nie z założeń. Wniosek praktyczny:
  klucze łącz rotuje się PARAMI usług (logic+data, presentation+logic,
  presentation+render), a nie całą czwórką — restartu wszystkiego wymaga tylko
  INTERNAL_TOKEN.
- Procedury per sekret, od najbezpieczniejszej do przećwiczenia (klucze LLM —
  dotykają tylko logiki, awaria widoczna i nieszkodliwa) po INTERNAL_TOKEN.
  Wszystkie zachowują pozostałe klucze przez odczyt z istniejącego sekretu —
  inaczej rotacja jednego skasowałaby resztę.
- Weryfikacja: sam „Running" NIE wystarcza (pody wstaną, nawet gdy warstwy się nie
  dogadują) — trzeba przeliczyć horoskop i wygenerować PDF, żeby dotknąć wszystkich
  łącz.
- Kroki hartowania z jasnym podziałem: co zrobione (automount tokenów — deploy #13),
  co wymaga węzła (szyfrowanie at-rest), co świadomie odłożone (Sealed Secrets —
  dziś sekrety NIE są w repo GitOps, więc ta zasada już jest spełniona; Sealed
  Secrets dodałyby odtwarzalność kosztem nowego pojedynczego punktu awarii).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit was merged in pull request #59.
This commit is contained in:
2026-08-04 17:10:23 +02:00
parent bc80745a94
commit e3114f3e7c
2 changed files with 171 additions and 0 deletions
+171
View File
@@ -0,0 +1,171 @@
# LOG-33 — sekrety w spoczynku i procedura rotacji
Sekrety (`APP_PASSWORD`/`APP_USERS`, `INTERNAL_TOKEN`, klucze łącz AES, klucze API
do dostawców LLM) trafiają do obiektów Secret w Kubernetesie, gdzie domyślnie są
**tylko zakodowane base64** — jawne dla każdego, kto przeczyta magazyn stanu k3s
albo ma prawo odczytu sekretów w namespace.
Ten dokument opisuje: **macierz rotacji** (co restartować przy zmianie czego),
**procedury rotacji per sekret** i **kroki hartowania**, które wymagają dostępu do
węzła.
---
## Macierz zależności — kto używa którego sekretu
Wyliczona z żywych deploymentów, nie z założeń:
| Sekret / klucz | Usługi, które go czytają | Restart obejmuje |
|---|---|---|
| `astrololo-auth` / **`INTERNAL_TOKEN`** | data, logic, presentation, **render** | **wszystkie cztery, równocześnie** |
| `astrololo-auth` / `APP_PASSWORD`, `APP_USERS` | presentation | tylko presentation |
| `astrololo-link` / `LINK_KEY_LOGIC_DATA` | logic, data | **para**: logic + data |
| `astrololo-link` / `LINK_KEY_PRESENTATION_LOGIC` | presentation, logic | **para**: presentation + logic |
| `astrololo-link` / `LINK_KEY_PRESENTATION_RENDER` | presentation, render | **para**: presentation + render |
| `astrololo-llm` / `OPENAI_API_KEY`, `ANTHROPIC_API_KEY` | logic | tylko logic |
> **UWAGA — częsty błąd.** Wcześniejsza wersja tego wymagania mówiła o „wszystkich
> **trzech** usługach" przy `INTERNAL_TOKEN`. To już nieprawda: `render` (PRE-24)
> również go używa. Restart trzech zostawi render ze starym tokenem i **usługa po
> cichu przestanie się dogadywać** — dokładnie ta awaria, przed którą wymaganie
> ostrzega.
Klucze łącz są **parami** — rotacja jednego wymaga restartu tylko dwóch usług, nie
całej czwórki. W trakcie wymiany para chwilowo się nie dogaduje (klucze muszą być
zgodne po obu stronach łącza), dlatego restart obu naraz.
---
## Procedury rotacji
### A. Klucze LLM (najbezpieczniejsze do przećwiczenia)
Dotykają wyłącznie logiki, a awaria jest widoczna od razu i nieszkodliwa
(niedostępna chmura, model lokalny działa dalej). **Zacznij ćwiczenie od tego.**
```bash
read -rs -p "Nowy ANTHROPIC_API_KEY: " NEW; echo
```
```bash
kubectl -n astrololo create secret generic astrololo-llm \
--from-literal=OPENAI_API_KEY="$(kubectl -n astrololo get secret astrololo-llm -o jsonpath='{.data.OPENAI_API_KEY}' | base64 -d)" \
--from-literal=ANTHROPIC_API_KEY="$NEW" \
--dry-run=client -o yaml | kubectl apply -f -
```
```bash
unset NEW && kubectl -n astrololo rollout restart deploy/logic && kubectl -n astrololo rollout status deploy/logic
```
### B. Klucz łącza (para usług)
Przykład dla `LINK_KEY_PRESENTATION_RENDER`. Pozostałe klucze zachowujemy bez zmian,
odczytując je z istniejącego sekretu — inaczej skasowalibyśmy pozostałe łącza.
```bash
kubectl -n astrololo create secret generic astrololo-link \
--from-literal=LINK_KEY_PRESENTATION_LOGIC="$(kubectl -n astrololo get secret astrololo-link -o jsonpath='{.data.LINK_KEY_PRESENTATION_LOGIC}' | base64 -d)" \
--from-literal=LINK_KEY_LOGIC_DATA="$(kubectl -n astrololo get secret astrololo-link -o jsonpath='{.data.LINK_KEY_LOGIC_DATA}' | base64 -d)" \
--from-literal=LINK_KEY_PRESENTATION_RENDER="$(openssl rand -hex 32)" \
--dry-run=client -o yaml | kubectl apply -f -
```
```bash
kubectl -n astrololo rollout restart deploy/presentation deploy/render
```
```bash
kubectl -n astrololo rollout status deploy/presentation && kubectl -n astrololo rollout status deploy/render
```
### C. `INTERNAL_TOKEN` (wszystkie cztery naraz)
Najbardziej wrażliwa rotacja: w trakcie usługi z różnymi tokenami **odrzucają się
nawzajem**, więc restart musi objąć całą czwórkę.
```bash
kubectl -n astrololo create secret generic astrololo-auth \
--from-literal=APP_PASSWORD="$(kubectl -n astrololo get secret astrololo-auth -o jsonpath='{.data.APP_PASSWORD}' | base64 -d)" \
--from-literal=INTERNAL_TOKEN="$(openssl rand -hex 32)" \
--dry-run=client -o yaml | kubectl apply -f -
```
```bash
kubectl -n astrololo rollout restart deploy/data deploy/logic deploy/presentation deploy/render
```
```bash
kubectl -n astrololo rollout status deploy/data && kubectl -n astrololo rollout status deploy/logic && kubectl -n astrololo rollout status deploy/presentation && kubectl -n astrololo rollout status deploy/render
```
> Jeśli używasz kont imiennych (PRE-17), zamiast `APP_PASSWORD` zachowaj `APP_USERS`
> — patrz [`konta-i-audyt.md`](konta-i-audyt.md).
### Weryfikacja po KAŻDEJ rotacji
Sam `Running` nie wystarczy — pody wstaną nawet, gdy warstwy się nie dogadują.
Trzeba sprawdzić **realny przelot przez wszystkie łącza**:
```bash
kubectl -n astrololo get pods
```
```bash
curl -s -o /dev/null -w "aplikacja: %{http_code}\n" -u "<login>:<hasło>" https://astrololo.czernobog.pl/
```
Policz horoskop w przeglądarce (dotyka presentation→logic→data) i wygeneruj PDF
(dotyka presentation→render). Dopiero to potwierdza, że wszystkie cztery klucze
i token są spójne.
---
## Hartowanie — kroki wymagające dostępu do węzła
### 1. Szyfrowanie sekretów w spoczynku (najważniejsze)
k3s z jednym serwerem trzyma stan w **SQLite**, nie w etcd, więc „szyfrowanie etcd"
sprowadza się do wbudowanej funkcji k3s. Kroki są w sekcji „Co musisz zrobić sam"
poniżej. Efekt: kopia pliku stanu albo snapshot VM przestaje być wyciekiem haseł
i kluczy API.
**Granica:** klucz szyfrujący leży na tym samym serwerze. Chroni przed kradzieżą
pliku/snapshotu — nie przed kimś, kto ma roota na węźle.
### 2. Ograniczenie tokenów kont serwisowych ✅
Zrobione: `automountServiceAccountToken: false` we wszystkich czterech usługach
(deploy #13). Żadna nie rozmawia z API Kubernetesa — sekrety wstrzykuje kubelet,
nie pod — więc token był zbędny, a stanowił gotowy punkt wyjścia do klastra.
### 3. RBAC
Stan sprawdzony: **zero RoleBindings** w `astrololo`, `cluster-admin` tylko dla
`system:masters` i dwóch kont Helma w `kube-system`. Nie ma rozdanych nadmiarowych
uprawnień do cofania.
Realna ekspozycja to **kubeconfig admina**. Sensowny krok: osobny, ograniczony
kubeconfig do codziennej pracy, a admin tylko wtedy, gdy naprawdę potrzebny.
### 4. Sealed Secrets / SOPS — świadomie ODŁOŻONE
Dziś sekrety tworzone są ręcznie i **nie ma ich w repo GitOps** — czyli zasada
„nie wpisywać sekretów do repozytorium" **jest już spełniona**. Kosztem jest
odtwarzalność: po utracie klastra nikt nie wie, co tam było.
Sealed Secrets pozwoliłoby trzymać je w gicie w postaci zaszyfrowanej, ale to
zmiana filozofii i **nowy pojedynczy punkt awarii**: utrata klucza kontrolera =
utrata wszystkich sekretów. Rekomendacja: dopiero po punkcie 1, i tylko jeśli
zależy Ci na odtwarzalności klastra z gita.
---
## Czego NIE robimy
- **Nie wpisujemy sekretów do logów.** Dziennik audytowy (PRE-17) niesie wyłącznie
metadane i liczby.
- **Nie wpisujemy sekretów do repo GitOps.** Manifesty odwołują się do sekretów
przez `secretKeyRef` i celowo nie zawierają wartości.
- **Nie zostawiamy haseł w historii powłoki** — stąd `read -rs` i odczyt istniejących
wartości przez `kubectl … | base64 -d` zamiast wpisywania ich ponownie.