feat(bezpieczenstwo): szyfrowanie lacz miedzy warstwami AES-256-GCM (PRE-16)
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
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
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>
This commit is contained in:
@@ -0,0 +1,296 @@
|
||||
# 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](https://gitea.czernobog.pl/gitea/astrololo/pulls/21) | kod: szyfrowanie łączy, limit żądań za proxy |
|
||||
| `gitea/deploy` | [#4](https://gitea.czernobog.pl/gitea/deploy/pulls/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
|
||||
|
||||
```bash
|
||||
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ą**.
|
||||
|
||||
```bash
|
||||
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ź:
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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ź:
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
kubectl -n astrololo port-forward svc/presentation 8000:8000 # http://localhost:8000
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Krok 4 — merge PR-a aplikacji (astrololo #21)
|
||||
|
||||
Teraz, **przed** manifestami.
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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)
|
||||
|
||||
```bash
|
||||
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.
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
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**:
|
||||
|
||||
```bash
|
||||
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:
|
||||
```bash
|
||||
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
|
||||
```bash
|
||||
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.
|
||||
Reference in New Issue
Block a user