Warstwy rozmawialy ze soba jawnym tekstem wewnatrz klastra. Token miedzywarstwowy (LOG-32) mowil KTO pyta, ale nie ukrywal CZEGO dotyczy odpowiedz — a plyna nia surowe wiersze oryginalnych baz interpretacyjnych, czyli rdzen produktu. Kto podsluchal ruch wewnatrz sieci (drugi pod, mirror portu na switchu, zrzut z wezla), mial je w calosci. Nowy modul link_crypto (kopia w kazdej z trzech uslug — nie maja wspolnej biblioteki; test pilnuje, ze kopie sa identyczne): - AES-256-GCM na ciele kazdego zadania i odpowiedzi. GCM daje poufnosc I uwierzytelnienie naraz, wiec nie ma wariantu „zaszyfrowane, ale podatne na modyfikacje". - DWA niezalezne klucze, po jednym na pare rozmowcow (prezentacja-logika, logika-dane). Przejecie klucza prezentacji nie otwiera warstwy danych, gdzie leza cale bazy. Z kazdego klucza lacza HKDF wyprowadza osobne podklucze na kierunek, wiec zadanie i odpowiedz nigdy nie szyfruja sie tym samym kluczem. - Do materialu uwierzytelnianego (AAD) wchodza kierunek, sciezka, znacznik czasu i numer ramki — wiec ramki nie da sie przekleic na inny endpoint, odtworzyc po czasie (okno MAX_SKEW) ani przestawic w strumieniu. - Strona serwerowa to czyste ASGI: podmienia cialo zanim zobaczy je FastAPI i przepuszcza odpowiedz strumieniowa kawalek po kawalku (okno postepu dziala dalej). Fail-closed: przy ustawionym kluczu jawne zadanie dostaje odmowe. Strumien postepu (okno pisania horoskopu) tez idzie przez szyfrowane lacze: link_crypto.stream_lines() pieczetuje zadanie i odszyfrowuje odpowiedz ramka po ramce (granice ramek != granice linii NDJSON), zachowujac dostarczanie na zywo. Bez tego przy wlaczonym LINK_ENCRYPTION_REQUIRED serwer odrzucalby strumien (400) i okno postepu przestaloby dzialac. Fail-closed obejmuje takze strumien: klient bez klucza nie wysyla nic, zamiast puscic dane urodzenia jawnym tekstem, zanim serwer zdazy odmowic. Najgrozniejszy blad wyszedl z PODSLUCHU prawdziwego gniazda, nie z testow: klient bez klucza wysylal pytanie jawnym tekstem, ZANIM serwer zdazyl odmowic. Stad LINK_ENCRYPTION_REQUIRED: klient nie wysyla niczego, a usluga nie wstaje, jesli klucza brak. Ta sama zasada co przy sekrecie logowania. Klient prezentacji przepuszczony przez jeden punkt _post()/stream_lines: dopoki kazda metoda skladala zadanie sama, dolozenie nowej znaczylo, ze latwo zapomniec o tokenie albo kluczu (401 wyszedl juz raz dopiero na produkcji). Test strukturalny: kazde wyjscie w dol musi miec i token, i klucz lacza (takze strumien), a surowe httpx wolno tylko na sciezkach wyjetych spod szyfrowania. Weryfikacja: - testy link_crypto (round-trip, brak tresci baz w bajtach na sieci, odrzucenie obcego klucza / przestawionego bitu / przekleconej sciezki / przestawionej ramki / przeterminowanej koperty / urwanego strumienia; round-trip strumienia i fail-closed klienta i serwera dla strumienia), - e2e na prawdziwym uvicornie z proxy zrzucajacym gniazdo: tresci baz brak na kablu w obie strony (grep=0), takze dla strumienia horoskopu; klucz jednej pary nie otwiera drugiej, - calosc: logika 234 passed / 1 skipped, prezentacja 25 passed. docs/wdrozenie-pre16.md: instrukcja krok po kroku z uzasadnieniem kolejnosci. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 KiB
Wdrożenie PRE-16 — HTTPS na wejściu i szyfrowanie łączy między warstwami
Instrukcja krok po kroku. Kolejność ma znaczenie — punkt „Dlaczego taka kolejność" niżej tłumaczy, co się stanie, jeśli ją zamienić.
Dotyczy dwóch pull requestów:
| Repo | PR | Co wnosi |
|---|---|---|
gitea/astrololo |
#21 | kod: szyfrowanie łączy, limit żądań za proxy |
gitea/deploy |
#4 | manifesty: Ingress, certyfikat, klucze łączy |
Co się właściwie zmienia
Na wejściu do aplikacji. Dotąd logowanie szło przez HTTP Basic po zwykłym http — czyli hasło leciało siecią w postaci trywialnej do podsłuchania (base64 to nie szyfrowanie). Po zmianie wejście jest po https, a http odsyła na https. Przy okazji odblokowują się dwie funkcje zepsute dziś z tego samego powodu: geolokalizacja („Tu i teraz") i kopiowanie promptu do schowka działają wyłącznie w tzw. secure context i po http po prostu odmawiały.
Między warstwami. Prezentacja, logika i dane rozmawiały ze sobą otwartym tekstem wewnątrz klastra. Token międzywarstwowy mówił kto pyta, ale nie ukrywał czego dotyczy odpowiedź — a płyną nią surowe wiersze oryginalnych baz. Teraz każde ciało żądania i odpowiedzi jest szyfrowane AES-256-GCM, osobnym kluczem na każdą parę rozmówców.
Wejście na świat pozostaje jedno: prompt do modelu. Ta zmiana niczego tu nie rusza — dotyczy wyłącznie ruchu wewnątrz sieci i wejścia z przeglądarki.
Zanim zaczniesz — stan wyjściowy
kubectl -n astrololo get deploy,svc
kubectl -n astrololo get secret # powinny być: astrololo-auth, gitea-registry
kubectl -n kube-system get svc traefik -o jsonpath='{.status.loadBalancer.ingress[*].ip}'; echo
Zanotuj adres Traefika — będzie potrzebny w kroku 3. Sprawdź też, czy działa aplikacja w obecnej postaci (przez NodePort), żeby mieć punkt odniesienia.
Krok 1 — sekret z kluczami łączy
Przed czymkolwiek innym. Klucze muszą istnieć, zanim pody spróbują wstać z nową konfiguracją, bo bez nich celowo nie wystartują.
kubectl -n astrololo create secret generic astrololo-link \
--from-literal=LINK_KEY_PRESENTATION_LOGIC="$(openssl rand -hex 32)" \
--from-literal=LINK_KEY_LOGIC_DATA="$(openssl rand -hex 32)"
Kluczy nikt nigdy nie musi oglądać — służą tylko usługom. Nie ma ich w repo GitOps i nie ma ich tam wkładać: cokolwiek trafi do gita, zostaje w historii na zawsze.
Dwa osobne klucze to nie ozdobnik. Przejęcie klucza prezentacji nie daje dostępu do warstwy danych, gdzie leżą całe bazy. Logika dostaje oba, bo rozmawia w obie strony; prezentacja i dane dostają wyłącznie swój.
Sprawdź:
kubectl -n astrololo get secret astrololo-link -o jsonpath='{.data}' | tr ',' '\n'
# oczekiwane: dwa klucze, każdy 64 znaki po odkodowaniu (32 bajty)
Krok 2 — cert-manager
Jednorazowo, na cały klaster:
kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.21.0/cert-manager.yaml
kubectl -n cert-manager rollout status deploy/cert-manager deploy/cert-manager-webhook --timeout=180s
Poczekaj, aż webhook będzie gotowy — dopóki nie wstanie, tworzenie obiektów
Certificate kończy się błędem połączenia i wygląda jak zepsuty manifest.
Sprawdź:
kubectl get crd | grep cert-manager | head -3 # muszą się pojawić
Dlaczego własne CA, a nie Let's Encrypt. Klaster stoi w LAN (Traefik trzyma LoadBalancera na adresach 192.168.1.x), więc walidacja HTTP-01 nie ma jak dojść z internetu, a DNS-01 wymagałby trzymania w klastrze tokena API do domeny. Własne CA nie potrzebuje niczego z zewnątrz i odnawia certyfikaty samo. Cena: raz na urządzenie importujesz korzeń (krok 6).
Krok 3 — DNS
Wpis astrololo.czernobog.pl → adres Traefika z kroku „stan wyjściowy”.
W routerze, lokalnym DNS-ie albo doraźnie w /etc/hosts:
echo "192.168.1.73 astrololo.czernobog.pl" | sudo tee -a /etc/hosts
To nie jest krok opcjonalny. Service presentation przestaje być NodePortem
(był drugą, nieszyfrowaną drogą do aplikacji — czyli obejściem całego PRE-16),
więc po wdrożeniu manifestów nazwa jest jedynym wejściem. Awaryjnie zawsze zostaje:
kubectl -n astrololo port-forward svc/presentation 8000:8000 # http://localhost:8000
Krok 4 — merge PR-a aplikacji (astrololo #21)
Teraz, przed manifestami.
tea pr merge --login gitea --repo gitea/astrololo 21
Po merge'u CI zbuduje obrazy, a image-updater sam podbije tagi w repo deploy,
skąd ArgoCD wymieni pody. Poczekaj, aż to się przetoczy:
kubectl -n astrololo rollout status deploy/presentation deploy/logic deploy/data
kubectl -n astrololo get pods -o jsonpath='{range .items[*]}{.spec.containers[0].image}{"\n"}{end}'
Na tym etapie nic się jeszcze nie szyfruje — nowy kod to potrafi, ale zmienne
z kluczami dokłada dopiero PR do deploy. Aplikacja działa dokładnie jak dotąd.
To celowe: chcemy, żeby cała obsada podów umiała szyfrować, zanim ktokolwiek
tego zażąda.
Krok 5 — merge PR-a manifestów (deploy #4)
tea pr merge --login gitea --repo gitea/deploy 4
ArgoCD zsynchronizuje się sam (automated, selfHeal). Wjeżdża naraz: Ingress,
certyfikat, zmienne z kluczami, TRUST_PROXY i zdjęcie NodePortu.
kubectl -n argocd get application astrololo
kubectl -n astrololo rollout status deploy/presentation deploy/logic deploy/data
kubectl -n astrololo get certificate # astrololo-ca i astrololo-tls: READY=True
Spodziewaj się kilkudziesięciu sekund błędów w trakcie. Pody wymieniają się po kolei, więc przez chwilę stara prezentacja (jeszcze bez klucza) rozmawia z nową logiką (już z kluczem) i dostaje odmowę. To zamierzone: alternatywą byłby tryb „przyjmuj i szyfrowane, i jawne”, który zwykle zostaje włączony na zawsze.
Merge nie cofnie tagów obrazów — PR dotyka w kustomization.yaml wyłącznie listy
resources, nie bloku images, więc git złoży to z nowszymi tagami z mastera.
Krok 6 — zaufanie do własnego CA (raz na urządzenie)
Bez tego przeglądarka pokaże ostrzeżenie o certyfikacie. Korzeń jest ważny 10 lat, więc robisz to raz:
kubectl -n astrololo get secret astrololo-ca -o jsonpath='{.data.ca\.crt}' \
| base64 -d > astrololo-ca.crt
# macOS — do systemowego zaufania (poprosi o hasło administratora)
sudo security add-trusted-cert -d -r trustRoot \
-k /Library/Keychains/System.keychain astrololo-ca.crt
# Linux (Debian/Ubuntu)
sudo cp astrololo-ca.crt /usr/local/share/ca-certificates/ && sudo update-ca-certificates
Firefox ma własny magazyn certyfikatów — import przez Ustawienia → Prywatność i bezpieczeństwo → Wyświetl certyfikaty → Organy certyfikacji.
Krok 7 — sprawdzenie, że działa to, co miało zadziałać
Wejście po https
curl -sI http://astrololo.czernobog.pl/ | head -2 # 301 → https
curl -s -o /dev/null -w "bez hasła: %{http_code}\n" https://astrololo.czernobog.pl/
curl -s -o /dev/null -w "z hasłem: %{http_code}\n" -u astrololo:'<hasło>' https://astrololo.czernobog.pl/
curl -sI -u astrololo:'<hasło>' https://astrololo.czernobog.pl/ | grep -i strict-transport
Oczekiwane: 301, 401, 200, nagłówek HSTS obecny. Brak ostrzeżenia o certyfikacie w przeglądarce oznacza, że krok 6 się udał.
W przeglądarce
Kliknij „Tu i teraz" — powinno pobrać lokalizację (po http odmawiało). Wygeneruj prompt i kliknij kopiuj — schowek powinien zadziałać bez obejść.
Szyfrowanie łączy — sprawdzenie wprost
Najmocniejszy test to próba obejścia. Z wnętrza klastra, bez klucza:
kubectl -n astrololo exec deploy/presentation -- \
python -c "
import httpx, os
r = httpx.post('http://logic:8001/chart/report',
json={'when_utc':'1984-04-30T09:20:00+00:00','lat':50.06,'lon':19.94},
headers={'X-Astrololo-Token': os.environ['INTERNAL_TOKEN']})
print(r.status_code, r.text[:120])
"
Oczekiwane: 400 i Łącze międzywarstwowe wymaga szyfrowania. Zwróć uwagę, że
żądanie miało prawidłowy token — sam token już nie wystarcza, i o to chodziło.
To samo w dół, do warstwy danych:
kubectl -n astrololo exec deploy/logic -- \
python -c "
import httpx, os
r = httpx.post('http://data:8002/search',
json={'key':'significator','value':'[Sat','exact':False,'limit':5},
headers={'X-Astrololo-Token': os.environ['INTERNAL_TOKEN']})
print(r.status_code, r.text[:120])
"
Logi startowe
kubectl -n astrololo logs deploy/logic | grep -i "łącze\|UWAGA"
Powinno być łącze szyfrowane (AES-256-GCM…). Jeśli widzisz ostrzeżenie
o rozmowie jawnym tekstem — klucz nie doszedł do poda.
Dlaczego taka kolejność
| Kolejność | Skutek zamiany |
|---|---|
| Sekret przed manifestami | LINK_ENCRYPTION_REQUIRED=true bez klucza celowo wywraca start. Pody wpadną w CrashLoop i będą tak siedzieć do czasu utworzenia sekretu. |
| cert-manager przed manifestami | API odrzuci Certificate/Issuer jako nieznane rodzaje zasobów, ArgoCD pokaże aplikację jako niezsynchronizowaną i sam tego nie naprawi. |
| DNS przed manifestami | NodePort znika razem z nimi. Bez wpisu DNS zostaje tylko port-forward. |
| Aplikacja przed manifestami | Odwrotnie: manifesty włączyłyby szyfrowanie na obrazach, które go nie znają — wszystkie żądania kończyłyby się odmową do czasu przebudowy obrazów. |
Fail-closed w obie strony jest zamierzony. Usługa, która wstała i po cichu nie szyfruje, jest gorsza niż pod w CrashLoop — awarii nie widać, a bazy jadą otwartym tekstem.
Wycofanie
Manifestów: git revert merge'a w deploy — ArgoCD samo wróci do NodePortu
i ruchu bez szyfrowania. Kod aplikacji nie wymaga wycofania: bez zmiennych
LINK_KEY_* moduł przepuszcza ruch jak dotąd (i głośno o tym mówi w logach).
Certyfikat i CA zostają w namespace; usunięcie: kubectl -n astrololo delete certificate astrololo-ca astrololo-tls. cert-managera można zostawić — nie
przeszkadza.
Gdy coś nie gra
| Objaw | Przyczyna | Co zrobić |
|---|---|---|
Pody w CrashLoopBackOff, w logach LINK_ENCRYPTION_REQUIRED … nie ustawiony |
brak sekretu astrololo-link |
krok 1, potem rollout restart |
400 Łącze międzywarstwowe wymaga szyfrowania przy normalnym korzystaniu |
jedna warstwa ma klucz, druga nie (albo trwa rollout) | rollout status; sprawdź, czy wszystkie trzy pody mają zmienną |
400 Nie udało się odczytać zaszyfrowanego żądania |
klucze po obu stronach łącza są różne | wymień sekret i zrestartuj wszystkie trzy naraz |
Certificate stoi w READY=False |
webhook cert-managera jeszcze nie wstał | kubectl -n cert-manager get pods, poczekaj i sprawdź kubectl -n astrololo describe certificate astrololo-tls |
| Przeglądarka: „połączenie nie jest prywatne” | korzeń CA nieimportowany na tym urządzeniu | krok 6 (pamiętaj, że Firefox ma osobny magazyn) |
404 z Traefika pod adresem aplikacji |
DNS wskazuje gdzie indziej niż LoadBalancer Traefika | porównaj dig +short astrololo.czernobog.pl z adresem z kroku „stan wyjściowy” |
| Limit żądań odcina wszystkich naraz | brak TRUST_PROXY=true — cały ruch liczony jako jeden klient |
sprawdź zmienną w deploy/presentation |
Czego to nie załatwia
- Szyfrowane są ciała żądań, nie nagłówki. Ścieżka (
/search) i token międzywarstwowy jadą czytelnie. Sam token nikomu nic nie daje — bez klucza łącza każde żądanie kończy się odmową — ale metadanych to nie ukrywa. Pełne ukrycie wymagałoby mTLS. - Własne CA to nie publiczne zaufanie. Każde nowe urządzenie wymaga importu korzenia. Gdyby aplikacja miała kiedyś wyjść na świat, właściwą drogą jest Let's Encrypt przez DNS-01.
- NFS z plikami baz stoi obok aplikacji — kto ma dostęp do share'u, bierze
pliki z pominięciem wszystkich powyższych zabezpieczeń. Do zamknięcia po stronie
infrastruktury (eksport tylko dla IP węzłów,
root_squash, najlepiej read-only). - Sekrety w etcd są tylko zakodowane base64. Docelowo: szyfrowanie etcd at-rest albo Sealed Secrets / SOPS.