fix(securitate): hardening prod — headere, body-cap, non-root, backup
Findings security-review P1/P2 (2026-07-03): - headere de securitate pe toate raspunsurile (nosniff, X-Frame-Options, Referrer-Policy, HSTS doar pe HTTPS) + teste - body-cap global 10MB ca middleware ASGI pur (413 inainte de parserul multipart/JSON; verificarea per-endpoint ramane strat 2) - imagine Docker non-root (uid 10001), port 8010 aliniat, loguri pe volumul /data - fail-fast la boot cu rar_env=prod fara AUTOPASS_REQUIRE_API_KEY sau AUTOPASS_SESSION_SECRET - compose: env-uri critice obligatorii (:?) ca api/worker sa nu diverga tacit; FORWARDED_ALLOW_IPS ca rate-limit-ul sa vada IP-ul real dupa Traefik - signup fara PII in stdout: log_event in loc de print cu email (idem notify degradat) - ratelimit: sterge cheile fara timestamp-uri valide (crestere monotona a memoriei pe IP-uri reale) - backup criptat SQLite (backup online API, gpg AES256) + verificare restore + docs/backup.md Suita completa verde: 1557 passed, 1 skipped (live). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -139,6 +139,12 @@ class Settings(BaseSettings):
|
||||
# conftest il dezactiveaza global, testele care-l vor il pornesc punctual.
|
||||
seed_operatii_enabled: bool = True
|
||||
|
||||
# --- Limita globala corp cerere (P1-4/P1-5, hardening 2026-07-03) ---
|
||||
# Plafon aplicat de un middleware ASGI pur (app/web/body_cap.py) pe TOATE rutele,
|
||||
# INAINTE ca FastAPI sa parseze body-ul (multipart e spooled pe disc altfel).
|
||||
# Verificarea per-endpoint existenta (import_parse.MAX_BYTES, 5MB) ramane strat 2.
|
||||
max_request_bytes: int = 10 * 1024 * 1024 # 10 MB
|
||||
|
||||
@property
|
||||
def rar_base_url(self) -> str:
|
||||
return self.rar_base_url_prod if self.rar_env == "prod" else self.rar_base_url_test
|
||||
@@ -149,6 +155,32 @@ def get_settings() -> Settings:
|
||||
return Settings()
|
||||
|
||||
|
||||
def validate_prod_invariants(settings: Settings) -> None:
|
||||
"""Fail-fast la startup: refuza boot-ul in productie cand lipsesc invariantele
|
||||
de securitate obligatorii, in loc sa lase aplicatia sa porneasca intr-o
|
||||
configuratie nesigura descoperita abia dupa deploy (mirror `crypto.validate_creds_key`).
|
||||
|
||||
Conditionat STRICT pe `rar_env=="prod"` — altfel ar opri suita de teste, care
|
||||
ruleaza implicit cu `rar_env="test"` si fara `session_secret`/`require_api_key`.
|
||||
"""
|
||||
if settings.rar_env != "prod":
|
||||
return
|
||||
if not settings.require_api_key:
|
||||
raise RuntimeError(
|
||||
"AUTOPASS_RAR_ENV=prod dar AUTOPASS_REQUIRE_API_KEY nu e activat: in "
|
||||
"productie cheia API e obligatorie pe /v1/*, altfel orice cerere "
|
||||
"neautentificata ajunge pe contul implicit id=1. Seteaza "
|
||||
"AUTOPASS_REQUIRE_API_KEY=true."
|
||||
)
|
||||
if not settings.session_secret:
|
||||
raise RuntimeError(
|
||||
"AUTOPASS_RAR_ENV=prod dar AUTOPASS_SESSION_SECRET nu este setat: fara el, "
|
||||
"secretul de sesiune e efemer per restart si toti userii sunt delogati la "
|
||||
"fiecare redeploy. Genereaza unul persistent cu:\n"
|
||||
" python3 -c \"import secrets; print(secrets.token_hex(32))\""
|
||||
)
|
||||
|
||||
|
||||
def load_test_credentials(settings_xml: Path | None = None) -> dict | None:
|
||||
"""Citeste credentialele <test> din settings.xml (dev local / probe test).
|
||||
|
||||
|
||||
Reference in New Issue
Block a user