Files
deploy/astrololo
gitea a30a4afece feat(astrololo): klucze szyfrowania lacz miedzy warstwami (PRE-16)
Domyka PRE-16 po stronie manifestow. Kod (astrololo#21) potrafi juz szyfrowac
ruch miedzy warstwami AES-256-GCM; tu dokladamy klucze i wymuszenie.

- astrololo-link: nowy sekret z dwoma kluczami (LINK_KEY_PRESENTATION_LOGIC,
  LINK_KEY_LOGIC_DATA), tworzony POZA repo jak pozostale. Osobny klucz na pare
  rozmowcow: przejecie klucza prezentacji nie otwiera warstwy danych. Logika
  bierze oba, prezentacja i dane wylacznie swoj (secretKeyRef).
- LINK_ENCRYPTION_REQUIRED=true we wszystkich trzech: bez klucza pod NIE wstaje,
  a klient nie wysyla niczego. Fail-closed w obie strony jest celowy — usluga,
  ktora wstala i po cichu nie szyfruje, jest gorsza niz CrashLoop, bo awarii
  nie widac.

README: sekcja o sekrecie astrololo-link (tworzenie, wymiana, restart calej
trojki naraz) oraz uczciwa nota, ze szyfrowane sa ciala, nie naglowki — sciezka
i token jada czytelnie, ale sam token bez klucza nic nie daje.

Sprawdzone: kubectl kustomize + apply --dry-run=server przechodza dla obu
profili. Pelna instrukcja wdrozenia: docs/wdrozenie-pre16.md w repo astrololo.

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

astrololo — deploy (namespace astrololo)

Manifesty k8s składane Kustomize. Obrazy podbija automatycznie image-updater (kustomization.yamlimages: newTag) po każdym buildzie z repo aplikacji.

Warstwy: presentation (wejście z przeglądarki przez Ingress po https) → logicdata (pliki Excel montowane z NFS, nie z obrazu).

Adres aplikacji: https://astrololo.czernobog.pl — patrz TLS.

⚠️ Sekret astrololo-auth — utwórz PRZED wdrożeniem

Aplikacja wystawia treść oryginalnych baz interpretacyjnych, dlatego wymaga logowania i tokenu międzywarstwowego (LOG-32). Manifesty odwołują się do sekretu astrololo-auth i celowo nie zawierają jego wartości — to repo GitOps, więc cokolwiek by tu wpadło, zostałoby w historii gita na zawsze.

To ta sama konwencja co gitea-registry: sekret tworzymy poza repo.

Pody nie wstaną bez tego sekretu — i tak ma być. Wolimy widoczną awarię niż cichy start aplikacji bez ochrony.

# hasło wczytane bez zapisu w historii powłoki
read -rs -p "Hasło do aplikacji (APP_PASSWORD): " APP_PASSWORD; echo

kubectl -n astrololo create secret generic astrololo-auth \
  --from-literal=APP_PASSWORD="$APP_PASSWORD" \
  --from-literal=INTERNAL_TOKEN="$(openssl rand -hex 32)"

unset APP_PASSWORD

INTERNAL_TOKEN jest losowany i nikt go nigdy nie musi oglądać — służy tylko usługom do rozmowy między sobą. APP_PASSWORD wpisujesz w przeglądarce (użytkownik: astrololo, zmienny przez APP_USER w presentation.yaml).

Zmiana hasła

read -rs -p "Nowe hasło: " NEW; echo
kubectl -n astrololo create secret generic astrololo-auth \
  --from-literal=APP_PASSWORD="$NEW" \
  --from-literal=INTERNAL_TOKEN="$(kubectl -n astrololo get secret astrololo-auth \
      -o jsonpath='{.data.INTERNAL_TOKEN}' | base64 -d)" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n astrololo rollout restart deploy/presentation
unset NEW

(Zachowujemy istniejący INTERNAL_TOKEN; jego zmiana wymaga restartu wszystkich trzech usług naraz, inaczej przestaną się dogadywać.)

Sprawdzenie po wdrożeniu

kubectl -n astrololo rollout status deploy/presentation deploy/logic deploy/data
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/

Oczekiwane: 401 bez hasła, 200 z hasłem. /health zostaje publiczny (sondy k8s).

🔒 TLS — wymagane kroki przed wdrożeniem

Do tej pory Basic Auth szedł siecią po zwykłym http, czyli hasło dało się podsłuchać (base64 to nie szyfrowanie). Poza tym http blokował dwie funkcje, bo przeglądarki udostępniają je wyłącznie w tzw. secure context: geolokalizacja („Tu i teraz") i kopiowanie do schowka. Certyfikat załatwia oba tematy naraz.

Certyfikat wystawia własne CA trzymane w klastrze (cert-manager). Let's Encrypt odpada, bo klaster stoi w LAN — walidacja HTTP-01 nie ma jak do niego dojść z internetu, a DNS-01 wymagałby trzymania tu tokena API do domeny.

Kolejność ma znaczenie — oba kroki PRZED synchronizacją ArgoCD.

1. cert-manager (jednorazowo, 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

Bez tego API odrzuci Certificate i Issuer jako nieznane rodzaje zasobów, a ArgoCD pokaże aplikację jako niezsynchronizowaną.

2. DNS: astrololo.czernobog.pl → adres Traefika

kubectl -n kube-system get svc traefik -o jsonpath='{.status.loadBalancer.ingress[*].ip}'; echo

Wpis w routerze / lokalnym DNS-ie (albo doraźnie /etc/hosts). Service presentation jest teraz ClusterIP — NodePort był drugą, nieszyfrowaną drogą do aplikacji, więc został zdjęty. Zanim DNS zacznie odpowiadać, jedyne wejście to:

kubectl -n astrololo port-forward svc/presentation 8000:8000   # awaryjnie, http://localhost:8000

3. Zaufanie do własnego CA (raz na urządzenie)

Bez tego przeglądarka pokaże ostrzeżenie o certyfikacie. Korzeń ważny 10 lat, więc import robisz 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 — tam import przez Ustawienia → Prywatność → Wyświetl certyfikaty → Organy certyfikacji.

Sprawdzenie samego TLS

# certyfikat wystawiony i gotowy
kubectl -n astrololo get certificate
# oczekiwane: astrololo-ca True, astrololo-tls True

# http odsyła na https
curl -sI http://astrololo.czernobog.pl/ | head -2       # 301 + Location: https://...

# HSTS obecny
curl -skI -u astrololo:'<hasło>' https://astrololo.czernobog.pl/ | grep -i strict-transport

Odnawianie

Certyfikat serwera żyje 90 dni i odnawia się sam 30 dni przed końcem. Termin jest krótki celowo — odnawianie sprawdza się wtedy w praktyce co kwartał, a nie dopiero za dziesięć lat, gdy nikt nie będzie pamiętał, jak to było skonfigurowane.

Token międzywarstwowy mówi kto pyta, ale nie ukrywa czego dotyczy odpowiedź — a płyną nią surowe wiersze oryginalnych baz. Kto podsłuchał ruch wewnątrz sieci (drugi pod, mirror portu na switchu, zrzut z węzła), miał je w całości. Łącza są więc szyfrowane AES-256-GCM, osobnym kluczem na każdą parę rozmówców.

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. Rozdzielenie jest celowe: przejęcie klucza prezentacji nie daje dostępu do warstwy danych, gdzie leżą całe bazy. Logika trzyma oba, bo rozmawia w obie strony; prezentacja i dane dostają przez secretKeyRef wyłącznie swój.

Pody nie wstaną bez tych kluczy (LINK_ENCRYPTION_REQUIRED=true) — i tak ma być. Usługa, która wstała i po cichu nie szyfruje, jest gorsza niż pod w CrashLoop, bo awarii nie widać.

Wymiana kluczy

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)" \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n astrololo rollout restart deploy/presentation deploy/logic deploy/data

Restart wszystkich trzech naraz — w trakcie wymiany warstwy chwilowo się nie dogadują (klucze muszą być zgodne po obu stronach łącza).

Klucze do modeli w chmurze — sekret astrololo-llm (opcjonalny)

Domyślnie działa model lokalny i nic nie opuszcza sieci. Żeby móc wybrać w UI OpenAI lub Anthropic, potrzebne są ich klucze. Ten sekret jest opcjonalny (optional: true) — bez niego pody startują normalnie, tylko chmura jest niedostępna.

read -rs -p "OPENAI_API_KEY (Enter = pomiń): " OPENAI_KEY; echo
read -rs -p "ANTHROPIC_API_KEY (Enter = pomiń): " ANTHROPIC_KEY; echo

kubectl -n astrololo create secret generic astrololo-llm \
  --from-literal=OPENAI_API_KEY="$OPENAI_KEY" \
  --from-literal=ANTHROPIC_API_KEY="$ANTHROPIC_KEY"

unset OPENAI_KEY ANTHROPIC_KEY
kubectl -n astrololo rollout restart deploy/logic     # klucze wstrzykują się przy starcie

Podmiana pojedynczego klucza (bez kasowania drugiego):

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='NOWY-KLUCZ' \
  --dry-run=client -o yaml | kubectl apply -f -
kubectl -n astrololo rollout restart deploy/logic

Adres modelu lokalnego ustawia LOCAL_BASE_URL w logic.yaml — dopasuj do miejsca, gdzie faktycznie stoi Ollama/vLLM. Konfiguracja jest per dostawca (LOCAL_*, OPENAI_*, ANTHROPIC_*), więc ustawienia lokalnego modelu nie przejmują żądań do chmury.

Wybór dostawcy w chmurze oznacza, że oryginalne opisy z baz opuszczają naszą sieć. Prompt można obejrzeć przed wysłaniem. Warto zawnioskować u dostawcy o Zero Data Retention — patrz LOG-32.

Czego to nie załatwia

  • Własne CA to nie to samo co publiczne zaufanie — każde nowe urządzenie wymaga importu korzenia (krok 3 powyżej). Gdyby aplikacja miała kiedyś wyjść na świat, właściwą drogą jest Let's Encrypt przez DNS-01.
  • 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ą. Pełne ukrycie metadanych wymagałoby mTLS.
  • NFS 192.168.1.34:/mnt/Tank1/astrololo — kto ma dostęp do share'u, bierze pliki baz z pominięciem całej aplikacji. Do zamknięcia po stronie infrastruktury (eksport tylko dla IP węzłów, root_squash, najlepiej read-only).
  • Sekret w etcd jest tylko zakodowany base64. Docelowo: szyfrowanie etcd at-rest albo Sealed Secrets / SOPS, jeśli chcecie trzymać sekrety deklaratywnie w repo.

Profil ze swissephem

Wariant z silnikiem B: ../astrololo-swisseph/. Dziedziczy powyższe zmienne z tej bazy, więc też wymaga sekretu astrololo-auth.