feat(bezpieczeństwo): HTTPS + szyfrowanie łączy między warstwami (PRE-16) #21

Open
gitea wants to merge 2 commits from feat/pre16-trusted-proxy into master
Owner

Aplikacyjna część PRE-16. Manifesty (Ingress, certyfikat, klucze) idą osobnym
PR-em w deploy (#4). Pełna
instrukcja wdrożenia obu naraz: docs/wdrozenie-pre16.md.

1. Szyfrowanie łączy między warstwami — nowość

Prezentacja, logika i dane rozmawiały ze sobą jawnym tekstem wewnątrz klastra.
Token międzywarstwowy (LOG-32) mówił kto pyta, ale nie ukrywał czego dotyczy
odpowiedź
— a płyną nią surowe wiersze oryginalnych baz. Kto podsłuchał ruch
w sieci (drugi pod, mirror portu, zrzut z węzła), miał je w całości.

Nowy link_cryptoAES-256-GCM na ciele każdego żądania i odpowiedzi:

  • Dwa niezależne klucze, po jednym na parę rozmówców. Przejęcie klucza
    prezentacji nie otwiera warstwy danych, gdzie leżą całe bazy. Z każdego klucza
    HKDF wyprowadza osobne podklucze na kierunek — żądanie i odpowiedź nigdy nie
    szyfrują się tym samym.
  • Do materiału uwierzytelnianego wchodzą kierunek, ścieżka, znacznik czasu
    i numer ramki
    — więc koperty nie da się przekleić na inny endpoint, odtworzyć
    po czasie ani przestawić w strumieniu.
  • Strona serwerowa to czyste ASGI: podmienia ciało zanim zobaczy je FastAPI
    i przepuszcza odpowiedź strumieniową kawałek po kawałku (okno postępu działa
    dalej).

Błąd, który złapał dopiero podsłuch gniazda

Testy jednostkowe były zielone, a mimo to klient bez klucza wysyłał pytanie
jawnym tekstem
, zanim serwer zdążył odmówić — odpowiedź była chroniona,
zapytanie już nie. Wyszło z proxy zrzucającego prawdziwe gniazdo do pliku
(grep significator = trafienie). Stąd LINK_ENCRYPTION_REQUIRED: klient nie
wysyła niczego, a usługa nie wstaje, jeśli klucza brak. Po poprawce ten sam
podsłuch pokazuje na kablu zero trafień treści baz, w obie strony.

2. Limit żądań po adresie klienta, nie proxy

Za Ingressem request.client.host to pod Traefika, jednakowy dla wszystkich —
limiter wrzuciłby cały ruch do jednego wiadra i pierwsza osoba, która go wyklika,
odcięłaby resztę. Nowe client_ip() czyta adres z nagłówka, ale wyłącznie przy
TRUST_PROXY
i bierze ostatni wpis X-Forwarded-For — inaczej wystarczyłoby
podstawiać własny nagłówek, żeby ominąć limit całkowicie.

Weryfikacja

  • 23 testy link_crypto: dowód, że tajny opis nie występuje w bajtach na
    sieci
    , plus odrzucenie obcego klucza / przestawionego bitu / przekleconej
    ścieżki / przestawionej ramki / przeterminowanej koperty / urwanego strumienia.
  • 6 testów limitera, w tym podszycie się pod X-Forwarded-For nie resetuje
    wiadra — sprawdzone celowym zepsuciem implementacji.
  • E2E na prawdziwym uvicornie z podsłuchem gniazda: treści baz brak na kablu;
    klucz jednej pary nie otwiera drugiej.
  • Reguła strukturalna rozszerzona: każde wyjście w dół musi nieść i token,
    i klucz łącza
    ; surowe httpx dozwolone tylko na /health.
  • Całość: logika 200 passed / 1 skipped, prezentacja 25 passed.

Uwaga do wdrożenia

TRUST_PROXY, LINK_ENCRYPTION_REQUIRED i klucze ustawia PR do deploy.
Domyślnie wyłączone — lokalnie bez sekretów aplikacja działa jak dotąd
(moduł przepuszcza ruch i głośno mówi o tym w logach). Kolejność merge'a ma
znaczenie
— najpierw ten PR (obrazy muszą umieć szyfrować), potem manifesty;
szczegóły w instrukcji.

Aplikacyjna część **PRE-16**. Manifesty (Ingress, certyfikat, klucze) idą osobnym PR-em w `deploy` ([#4](https://gitea.czernobog.pl/gitea/deploy/pulls/4)). Pełna instrukcja wdrożenia obu naraz: [`docs/wdrozenie-pre16.md`](docs/wdrozenie-pre16.md). ## 1. Szyfrowanie łączy między warstwami — nowość Prezentacja, logika i dane rozmawiały ze sobą **jawnym tekstem** wewnątrz klastra. Token międzywarstwowy (LOG-32) mówił *kto* pyta, ale nie ukrywał *czego dotyczy odpowiedź* — a płyną nią surowe wiersze oryginalnych baz. Kto podsłuchał ruch w sieci (drugi pod, mirror portu, zrzut z węzła), miał je w całości. Nowy `link_crypto` — **AES-256-GCM** na ciele każdego żądania i odpowiedzi: - **Dwa niezależne klucze**, po jednym na parę rozmówców. Przejęcie klucza prezentacji nie otwiera warstwy danych, gdzie leżą całe bazy. Z każdego klucza HKDF wyprowadza **osobne podklucze na kierunek** — żądanie i odpowiedź nigdy nie szyfrują się tym samym. - Do materiału uwierzytelnianego wchodzą **kierunek, ścieżka, znacznik czasu i numer ramki** — więc koperty nie da się przekleić na inny endpoint, odtworzyć po czasie ani przestawić w strumieniu. - Strona serwerowa to czyste **ASGI**: podmienia ciało zanim zobaczy je FastAPI i przepuszcza **odpowiedź strumieniową** kawałek po kawałku (okno postępu działa dalej). ### Błąd, który złapał dopiero podsłuch gniazda Testy jednostkowe były zielone, a mimo to **klient bez klucza wysyłał pytanie jawnym tekstem**, zanim serwer zdążył odmówić — odpowiedź była chroniona, zapytanie już nie. Wyszło z proxy zrzucającego prawdziwe gniazdo do pliku (`grep significator` = trafienie). Stąd `LINK_ENCRYPTION_REQUIRED`: klient nie wysyła niczego, a usługa nie wstaje, jeśli klucza brak. Po poprawce ten sam podsłuch pokazuje na kablu **zero** trafień treści baz, w obie strony. ## 2. Limit żądań po adresie klienta, nie proxy Za Ingressem `request.client.host` to pod Traefika, jednakowy dla wszystkich — limiter wrzuciłby cały ruch do jednego wiadra i pierwsza osoba, która go wyklika, odcięłaby resztę. Nowe `client_ip()` czyta adres z nagłówka, ale **wyłącznie przy `TRUST_PROXY`** i bierze **ostatni** wpis `X-Forwarded-For` — inaczej wystarczyłoby podstawiać własny nagłówek, żeby ominąć limit całkowicie. ## Weryfikacja - **23 testy** `link_crypto`: dowód, że tajny opis **nie występuje w bajtach na sieci**, plus odrzucenie obcego klucza / przestawionego bitu / przekleconej ścieżki / przestawionej ramki / przeterminowanej koperty / urwanego strumienia. - **6 testów** limitera, w tym podszycie się pod `X-Forwarded-For` **nie** resetuje wiadra — sprawdzone celowym zepsuciem implementacji. - **E2E** na prawdziwym uvicornie z podsłuchem gniazda: treści baz brak na kablu; klucz jednej pary nie otwiera drugiej. - Reguła strukturalna rozszerzona: każde wyjście w dół musi nieść **i token, i klucz łącza**; surowe `httpx` dozwolone tylko na `/health`. - Całość: **logika 200 passed / 1 skipped, prezentacja 25 passed**. ## Uwaga do wdrożenia `TRUST_PROXY`, `LINK_ENCRYPTION_REQUIRED` i klucze ustawia PR do `deploy`. Domyślnie **wyłączone** — lokalnie bez sekretów aplikacja działa jak dotąd (moduł przepuszcza ruch i głośno mówi o tym w logach). **Kolejność merge'a ma znaczenie** — najpierw ten PR (obrazy muszą umieć szyfrować), potem manifesty; szczegóły w instrukcji.
gitea added 1 commit 2026-07-22 21:27:27 +00:00
fix(prezentacja): limit zadan po adresie klienta, nie proxy (PRE-16)
Testy / Testy warstwy logicznej (silnik) (push) Successful in 10m47s
Testy / Testy warstwy prezentacji (dostęp do baz) (push) Successful in 9m33s
Testy / Build obrazu silnika B (swisseph) (push) Successful in 31s
Testy / Kontrola składni wszystkich warstw (push) Successful in 21s
Testy / Testy warstwy logicznej (silnik) (pull_request) Successful in 10m46s
Testy / Testy warstwy prezentacji (dostęp do baz) (pull_request) Successful in 9m53s
Testy / Build obrazu silnika B (swisseph) (pull_request) Successful in 32s
Testy / Kontrola składni wszystkich warstw (pull_request) Successful in 20s
91644a44e3
Po wlaczeniu TLS aplikacja stanie za Ingressem, a wtedy `request.client.host`
to adres POD-a Traefika — jednakowy dla wszystkich. Limiter wrzucalby caly ruch
do jednego wiadra 120/min i pierwsza osoba, ktora go wyklika, odcielaby
pozostalych. Cicha regresja, ktora ujawnilaby sie dopiero na produkcji.

Nowe `client_ip()` czyta adres z naglowka, ale WYLACZNIE przy TRUST_PROXY —
bo inaczej wystarczyloby dopisywac wlasny X-Forwarded-For, zeby przy kazdym
zadaniu wygladac na kogos innego i ominac limit calkowicie. Z tego samego
powodu bierzemy OSTATNI wpis listy: to jedyny, ktory dopisal nasz proxy;
wczesniejsze mogl podstawic klient, wiec nie znacza nic.

Szesc testow, w tym dwa istotne:
- podszycie sie pod X-Forwarded-For NIE resetuje wiadra przy wylaczonym
  TRUST_PROXY (inaczej baze dalo by sie pompowac bez ograniczen),
- za proxy dwa rozne adresy dostaja osobne wiadra i nie odcinaja sie nawzajem.

Oba sprawdzone celowym zepsuciem implementacji (zawsze ufaj naglowkowi +
bierz pierwszy wpis) — testy wtedy czerwienieja. 23 passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
gitea added 1 commit 2026-07-22 21:52:22 +00:00
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
3ce3911f55
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>
gitea changed title from fix(prezentacja): limit żądań po adresie klienta, nie proxy (PRE-16) to feat(bezpieczeństwo): HTTPS + szyfrowanie łączy między warstwami (PRE-16) 2026-07-22 21:52:53 +00:00
Some checks are pending
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
This pull request can be merged automatically.
You are not authorized to merge this pull request.
View command line instructions

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/pre16-trusted-proxy:feat/pre16-trusted-proxy
git checkout feat/pre16-trusted-proxy
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gitea/astrololo#21