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:
@@ -10,6 +10,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
|
||||
from app import link_crypto
|
||||
from app.config import settings
|
||||
|
||||
|
||||
@@ -19,16 +20,29 @@ def _auth_headers() -> dict[str, str]:
|
||||
return {"X-Astrololo-Token": token} if token else {}
|
||||
|
||||
|
||||
def _link() -> link_crypto.Link | None:
|
||||
"""Klucz łącza prezentacja↔logika. Czytany przy każdym wywołaniu, bo
|
||||
konfiguracja może się zmienić bez restartu procesu (testy, podmiana sekretu)."""
|
||||
key = link_crypto.key_from_env(link_crypto.ENV_PRESENTATION_LOGIC)
|
||||
return link_crypto.Link(key) if key else None
|
||||
|
||||
|
||||
class LogicClient:
|
||||
def __init__(self, base_url: str | None = None) -> None:
|
||||
self.base_url = (base_url or settings.logic_url).rstrip("/")
|
||||
|
||||
def _post(self, path: str, payload: dict[str, Any], timeout: float) -> dict[str, Any]:
|
||||
"""Jedyna droga w dół. Celowo JEDNA: dopóki każda metoda składała żądanie
|
||||
sama, dołożenie nowej znaczyło, że łatwo zapomnieć o tokenie albo kluczu
|
||||
łącza — i tak się już raz stało (401 wyszedł dopiero na produkcji)."""
|
||||
with httpx.Client(timeout=timeout) as client:
|
||||
return link_crypto.call_json(client, "POST", f"{self.base_url}{path}",
|
||||
payload=payload, headers=_auth_headers(),
|
||||
link=_link())
|
||||
|
||||
def query(self, query: str, field: str, exact: bool, limit: int) -> dict[str, Any]:
|
||||
payload = {"query": query, "field": field, "exact": exact, "limit": limit}
|
||||
with httpx.Client(timeout=settings.http_timeout) as client:
|
||||
r = client.post(f"{self.base_url}/api/query", json=payload, headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
return self._post("/api/query", payload, settings.http_timeout)
|
||||
|
||||
def positions(
|
||||
self,
|
||||
@@ -51,20 +65,15 @@ class LogicClient:
|
||||
"zodiac": zodiac,
|
||||
}
|
||||
# stacje wymagają root-findów — dłuższy timeout
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 60.0) if stations else settings.http_timeout) as client:
|
||||
r = client.post(f"{self.base_url}/chart/positions", json=payload, headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
timeout = max(settings.http_timeout, 60.0) if stations else settings.http_timeout
|
||||
return self._post("/chart/positions", payload, timeout)
|
||||
|
||||
def report(
|
||||
self, when_utc_iso: str, lat: float, lon: float, limit: int = 5000, group: bool = False
|
||||
) -> dict[str, Any]:
|
||||
"""Sygnifikatory z obliczeń szukane w bazie — woła logic /chart/report."""
|
||||
payload = {"when_utc": when_utc_iso, "lat": lat, "lon": lon, "limit": limit, "group": group}
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 30.0)) as client:
|
||||
r = client.post(f"{self.base_url}/chart/report", json=payload, headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
return self._post("/chart/report", payload, max(settings.http_timeout, 30.0))
|
||||
|
||||
def prompt(
|
||||
self, profile: str, when_utc_iso: str, lat: float, lon: float,
|
||||
@@ -82,10 +91,7 @@ class LogicClient:
|
||||
}
|
||||
if from_date and to_date:
|
||||
payload["from_date"], payload["to_date"] = from_date, to_date
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 60.0)) as client:
|
||||
r = client.post(f"{self.base_url}/chart/prompt", json=payload, headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
return self._post("/chart/prompt", payload, max(settings.http_timeout, 60.0))
|
||||
|
||||
def horoscope(
|
||||
self, profile: str, when_utc_iso: str, lat: float, lon: float,
|
||||
@@ -106,17 +112,13 @@ class LogicClient:
|
||||
payload["model"] = model
|
||||
if from_date and to_date:
|
||||
payload["from_date"], payload["to_date"] = from_date, to_date
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 300.0)) as client:
|
||||
r = client.post(f"{self.base_url}/chart/horoscope", json=payload, headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
return self._post("/chart/horoscope", payload, max(settings.http_timeout, 300.0))
|
||||
|
||||
def llm_models(self) -> dict[str, Any]:
|
||||
"""Katalog modeli per dostawca (podpowiedzi do pola wyboru w UI)."""
|
||||
with httpx.Client(timeout=settings.http_timeout) as client:
|
||||
r = client.get(f"{self.base_url}/llm/models", headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
return link_crypto.call_json(client, "GET", f"{self.base_url}/llm/models",
|
||||
headers=_auth_headers(), link=_link())
|
||||
|
||||
def timeline(
|
||||
self, when_utc_iso: str, lat: float, lon: float,
|
||||
@@ -127,7 +129,4 @@ class LogicClient:
|
||||
"when_utc": when_utc_iso, "lat": lat, "lon": lon,
|
||||
"from_date": from_date, "to_date": to_date, "interpret": interpret,
|
||||
}
|
||||
with httpx.Client(timeout=max(settings.http_timeout, 60.0)) as client:
|
||||
r = client.post(f"{self.base_url}/chart/timeline", json=payload, headers=_auth_headers())
|
||||
r.raise_for_status()
|
||||
return r.json()
|
||||
return self._post("/chart/timeline", payload, max(settings.http_timeout, 60.0))
|
||||
|
||||
Reference in New Issue
Block a user