From d4a57cc4f54f40ab80c164f70fd648d95dc27291 Mon Sep 17 00:00:00 2001 From: migatu Date: Wed, 22 Jul 2026 23:26:38 +0200 Subject: [PATCH] feat(astrololo): wejscie po https z wlasnym CA (PRE-16) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Do tej pory aplikacja chodzila po zwyklym http, a logowanie szlo przez HTTP Basic — czyli haslo leciało siecia w postaci trywialnej do podsluchania. Poza bezpieczenstwem TLS naprawia DWIE funkcje zepsute dzis z tego samego powodu: geolokalizacja przegladarki („Tu i teraz") i kopiowanie promptu do schowka dzialaja wylacznie w secure context, wiec po http po prostu odmawialy. Wlasne CA, nie Let's Encrypt — klaster stoi w LAN (traefik trzyma LoadBalancera na 192.168.1.x), wiec walidacja HTTP-01 nie ma jak dojsc z internetu, a DNS-01 wymagalby trzymania w klastrze tokena API do domeny. Co dochodzi: - tls.yaml — Issuer selfSigned -> korzen CA (10 lat, importowany raz do przegladarki) -> Issuer CA -> certyfikat serwera (90 dni, odnawiany sam). Krotki termin certyfikatu serwera jest CELOWY: odnawianie sprawdza sie wtedy co kwartal, a nie dopiero za dekade. - ingress.yaml — wejscie po https + naglowki (HSTS rok, nosniff, frameDeny) oraz osobny Ingress na porcie 80 robiacy przekierowanie. Rozdzielone swiadomie: gdyby redirect wisial na tym samym routerze co wejscie po https, odsylalby sam do siebie w kolko. - presentation: Service NodePort -> ClusterIP. NodePort byl druga, nieszyfrowana droga do aplikacji, czyli obejsciem calego PRE-16. Zmiana wymagajaca uwagi: TRUST_PROXY=true. Za Ingressem bezposrednim rozmowca jest zawsze pod Traefika, wiec bez tego limit zadan liczylby WSZYSTKICH do jednego wiadra i pierwsza osoba, ktora go wyklika, odcielaby pozostalych. Sprawdzone: Ingress i Middleware przechodza `kubectl apply --dry-run=server` na zywym klastrze (CRD traefik.io/v1alpha1 sa, entrypointy web/websecure sa). Obiekty cert-managera zwalidowane offline wobec oficjalnego schematu z CRD v1.21.0 z additionalProperties=false — literowka w nazwie pola zostalaby zlapana. `kubectl kustomize` przechodzi dla obu profili (bazowego i swisseph). Co-Authored-By: Claude Opus 4.8 --- astrololo/README.md | 84 +++++++++++++++++++++++++++++++++--- astrololo/ingress.yaml | 75 ++++++++++++++++++++++++++++++++ astrololo/kustomization.yaml | 2 + astrololo/presentation.yaml | 12 +++++- astrololo/tls.yaml | 65 ++++++++++++++++++++++++++++ 5 files changed, 230 insertions(+), 8 deletions(-) create mode 100644 astrololo/ingress.yaml create mode 100644 astrololo/tls.yaml diff --git a/astrololo/README.md b/astrololo/README.md index c6e2900..a3b726a 100644 --- a/astrololo/README.md +++ b/astrololo/README.md @@ -3,8 +3,10 @@ Manifesty k8s składane Kustomize. Obrazy podbija automatycznie image-updater (`kustomization.yaml` → `images: newTag`) po każdym buildzie z repo aplikacji. -Warstwy: `presentation` (NodePort, wejście z przeglądarki) → `logic` → `data` -(pliki Excel montowane **z NFS**, nie z obrazu). +Warstwy: `presentation` (wejście z przeglądarki przez **Ingress po https**) → +`logic` → `data` (pliki Excel montowane **z NFS**, nie z obrazu). + +Adres aplikacji: **https://astrololo.czernobog.pl** — patrz [TLS](#-tls--wymagane-kroki-przed-wdrożeniem). ## ⚠️ Sekret `astrololo-auth` — utwórz PRZED wdrożeniem @@ -50,12 +52,79 @@ trzech** usług naraz, inaczej przestaną się dogadywać.) ### Sprawdzenie po wdrożeniu ```bash kubectl -n astrololo rollout status deploy/presentation deploy/logic deploy/data -NODE_PORT=$(kubectl -n astrololo get svc presentation -o jsonpath='{.spec.ports[0].nodePort}') -curl -s -o /dev/null -w "bez hasła: %{http_code}\n" http://:$NODE_PORT/ -curl -s -o /dev/null -w "z hasłem: %{http_code}\n" -u astrololo:'' http://:$NODE_PORT/ +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/ ``` 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) +```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 +``` +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 +```bash +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: +```bash +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: +```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 — tam import przez *Ustawienia → Prywatność → +Wyświetl certyfikaty → Organy certyfikacji*. + +### Sprawdzenie samego TLS +```bash +# 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:'' 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. + ## 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 @@ -94,8 +163,9 @@ gdzie faktycznie stoi Ollama/vLLM. Konfiguracja jest **per dostawca** > Retention — patrz LOG-32. ## Czego to nie załatwia -- **Brak TLS** — Basic Auth idzie po sieci w postaci łatwej do podsłuchania. Przy - niezaufanej sieci potrzebny ingress z certyfikatem. +- **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. - **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). diff --git a/astrololo/ingress.yaml b/astrololo/ingress.yaml new file mode 100644 index 0000000..01d4f8a --- /dev/null +++ b/astrololo/ingress.yaml @@ -0,0 +1,75 @@ +# Wejście do aplikacji po HTTPS (PRE-16). +# +# Poza bezpieczeństwem — Basic Auth po zwykłym http leci siecią czytelny dla +# każdego, kto słucha — TLS naprawia DWIE funkcje zepsute dziś z tego samego +# powodu: geolokalizacja przeglądarki („Tu i teraz") oraz kopiowanie promptu do +# schowka działają wyłącznie w tzw. secure context i po http po prostu odmawiają. +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: security-headers + namespace: astrololo +spec: + headers: + stsSeconds: 31536000 # HSTS: rok + stsIncludeSubdomains: false # dotyczy tylko tego hosta + stsPreload: false # preload to wpis na listę w przeglądarkach + # — praktycznie nieodwracalny, nie wchodzimy + forceSTSHeader: true # nagłówek także przy odpowiedziach z backendu + contentTypeNosniff: true + frameDeny: true + referrerPolicy: same-origin +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-https + namespace: astrololo +spec: + redirectScheme: + scheme: https + permanent: true +--- +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: astrololo + namespace: astrololo + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: websecure + traefik.ingress.kubernetes.io/router.middlewares: astrololo-security-headers@kubernetescrd +spec: + tls: + - hosts: [astrololo.czernobog.pl] + secretName: astrololo-tls # wypełnia cert-manager (tls.yaml) + rules: + - host: astrololo.czernobog.pl + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: presentation, port: { number: 8000 } } +--- +# Osobny Ingress wyłącznie na porcie 80 — istnieje po to, żeby odesłać na https. +# Rozdzielone świadomie: gdyby przekierowanie wisiało na tym samym routerze, co +# wejście po https, odsyłałoby samo do siebie w kółko. Backend jest tu formalnością, +# bo żadne żądanie do niego nie dojdzie. +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: astrololo-redirect + namespace: astrololo + annotations: + traefik.ingress.kubernetes.io/router.entrypoints: web + traefik.ingress.kubernetes.io/router.middlewares: astrololo-redirect-https@kubernetescrd +spec: + rules: + - host: astrololo.czernobog.pl + http: + paths: + - path: / + pathType: Prefix + backend: + service: { name: presentation, port: { number: 8000 } } diff --git a/astrololo/kustomization.yaml b/astrololo/kustomization.yaml index 1cd6e1f..f880cc5 100644 --- a/astrololo/kustomization.yaml +++ b/astrololo/kustomization.yaml @@ -5,6 +5,8 @@ resources: - data.yaml - logic.yaml - presentation.yaml + - tls.yaml # certyfikat z własnego CA (wymaga cert-managera) + - ingress.yaml # wejście po https + przekierowanie z http images: - name: gitea.czernobog.pl/gitea/astrololo-data newTag: 877ec91f diff --git a/astrololo/presentation.yaml b/astrololo/presentation.yaml index 89841e4..43dbb9e 100644 --- a/astrololo/presentation.yaml +++ b/astrololo/presentation.yaml @@ -30,6 +30,13 @@ spec: value: "astrololo" - name: RATE_LIMIT_PER_MIN value: "120" # 0 = bez limitu + # Aplikacja stoi za Ingressem, więc bezpośrednim rozmówcą jest zawsze + # pod Traefika. Bez tego limit żądań liczyłby WSZYSTKICH do jednego + # wiadra i pierwsza osoba, która go wyklika, odcięłaby pozostałych. + # Włączać wyłącznie gdy ruch faktycznie idzie przez proxy — inaczej + # nagłówek staje się furtką do ominięcia limitu. + - name: TRUST_PROXY + value: "true" resources: requests: { cpu: "100m", memory: "128Mi" } limits: { cpu: "300m", memory: "256Mi" } @@ -40,7 +47,10 @@ metadata: name: presentation namespace: astrololo spec: - type: NodePort # wejście z przeglądarki + # Wejście z przeglądarki idzie teraz WYŁĄCZNIE przez Ingress po https + # (ingress.yaml). NodePort był drugą, nieszyfrowaną drogą do środka — czyli + # obejściem całego PRE-16 — więc znika. + type: ClusterIP selector: { app: presentation } ports: [{ port: 8000, targetPort: 8000 }] diff --git a/astrololo/tls.yaml b/astrololo/tls.yaml new file mode 100644 index 0000000..771ae16 --- /dev/null +++ b/astrololo/tls.yaml @@ -0,0 +1,65 @@ +# Certyfikat dla wejścia do aplikacji (PRE-16). +# +# Dlaczego WŁASNE CA, a nie Let's Encrypt: klaster stoi w LAN (traefik trzyma +# LoadBalancera na 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, a certyfikaty odnawia samo. +# +# WYMAGANIE WSTĘPNE: cert-manager musi być w klastrze PRZED synchronizacją tego +# katalogu — inaczej API odrzuci poniższe obiekty jako nieznane rodzaje zasobów. +# Instalacja jednorazowa: patrz README.md. +# +# Wszystko jest w namespace `astrololo` (Issuer, nie ClusterIssuer) świadomie: +# ArgoCD ma tu włączone `prune`, a zasoby o zasięgu całego klastra chcemy trzymać +# poza aplikacją, która może zostać skasowana. +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: selfsigned + namespace: astrololo +spec: + selfSigned: {} +--- +# Korzeń zaufania. TO JEST certyfikat, który raz importujesz do przeglądarki +# i systemu — patrz README. Stąd długi termin ważności: importu nie chcemy +# powtarzać co kwartał. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: astrololo-ca + namespace: astrololo +spec: + isCA: true + commonName: astrololo internal CA + secretName: astrololo-ca # tu lądują klucz i certyfikat korzenia + duration: 87600h # 10 lat + renewBefore: 8760h # odnowienie na rok przed końcem + privateKey: { algorithm: ECDSA, size: 256 } + issuerRef: { name: selfsigned, kind: Issuer, group: cert-manager.io } +--- +apiVersion: cert-manager.io/v1 +kind: Issuer +metadata: + name: astrololo-ca + namespace: astrololo +spec: + ca: { secretName: astrololo-ca } +--- +# Właściwy certyfikat serwera. Krótki termin CELOWO: przy 90 dniach odnawianie +# jest sprawdzane w praktyce co kwartał, więc awaria wyjdzie od razu, a nie za +# dziesięć lat, gdy nikt już nie będzie pamiętał, jak to działa. +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: astrololo-tls + namespace: astrololo +spec: + secretName: astrololo-tls # tego szuka Ingress + commonName: astrololo.czernobog.pl + dnsNames: + - astrololo.czernobog.pl + duration: 2160h # 90 dni + renewBefore: 720h # 30 dni zapasu + privateKey: { algorithm: ECDSA, size: 256, rotationPolicy: Always } + issuerRef: { name: astrololo-ca, kind: Issuer, group: cert-manager.io }