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>
7.1 KiB
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.
read -rs -p "Nowy ANTHROPIC_API_KEY: " NEW; echo
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 -
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.
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 -
kubectl -n astrololo rollout restart deploy/presentation deploy/render
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ę.
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 -
kubectl -n astrololo rollout restart deploy/data deploy/logic deploy/presentation deploy/render
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_PASSWORDzachowajAPP_USERS— patrzkonta-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:
kubectl -n astrololo get pods
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
secretKeyRefi celowo nie zawierają wartości. - Nie zostawiamy haseł w historii powłoki — stąd
read -rsi odczyt istniejących wartości przezkubectl … | base64 -dzamiast wpisywania ich ponownie.