Files
astrololo/docs/wdrozenie-pre16.md
gitea 3ce3911f55
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m50s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m50s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 30s
Testy / Kontrola składni wszystkich warstw (push) Successful in 21s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m47s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m52s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 35s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 17s
feat(bezpieczenstwo): szyfrowanie lacz miedzy warstwami AES-256-GCM (PRE-16)
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.

Najgrozniejszy blad wyszedl dopiero z PODSLUCHU prawdziwego gniazda, nie
z testow: klient BEZ klucza wysylal pytanie jawnym tekstem, ZANIM serwer zdazyl
odmowic. Odpowiedz byla chroniona, zapytanie juz nie — a to wlasnie ono niesie
sygnifikatory. Stad LINK_ENCRYPTION_REQUIRED: klient nie wysyla niczego, a
usluga nie wstaje, jesli klucza brak. Ta sama zasada co przy sekrecie logowania
— wolimy pod w CrashLoop niz usluge, ktora wstala i po cichu nie chroni niczego.

Klient prezentacji przepuszczony przez jeden punkt `_post()`: 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 rozszerzony: kazde wyjscie w dol musi miec i token, i klucz lacza,
a surowe httpx wolno tylko na sciezkach wyjetych spod szyfrowania (/health).

Weryfikacja:
- 23 testy link_crypto, w tym dowod, ze tajny opis NIE wystepuje w bajtach
  lecacych po sieci, oraz odrzucenie: obcego klucza, przestawionego bitu,
  przekleconej sciezki, przestawionej ramki, przeterminowanej koperty, urwanego
  strumienia i absurdalnej dlugosci ramki.
- E2E na prawdziwym uvicornie z proxy zrzucajacym gniazdo do pliku: przy
  wlaczonym wymaganiu tresci baz NIE MA na kablu (grep = 0) w obie strony;
  bez klucza / ze zlym kluczem — odmowa; klucz jednej pary nie otwiera drugiej.
- Calosc: logika 200 passed/1 skipped, prezentacja 25 passed.

docs/wdrozenie-pre16.md: instrukcja krok po kroku (klucze -> cert-manager ->
DNS -> merge aplikacji -> merge manifestow -> zaufanie CA -> weryfikacja),
z uzasadnieniem kolejnosci i tabela diagnostyki.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:52:18 +02:00

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.