# 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:'' https://astrololo.czernobog.pl/ curl -sI -u astrololo:'' 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.