fix: weiger opstart bij onveilige SECRET_KEY in productie#73
Conversation
|
LGTM. Failure-semantics zijn correct (validator op Drie niet-blokkerende observaties1. Lengte-drempel zonder entropie-checkBewuste vereenvoudiging die ik onderschrijf (entropie-meting in een opstart-validator wordt brittle, en de echte aanval is de gepubliceerde constante zelf). 2.
|
|
Eén commit toegevoegd ( 🚨 Kritieke voorgrondsfeit — productie draait NU op de publieke dev-default key# opi/core/config.py:
SECRET_KEY: str = DEV_DEFAULT_SECRET_KEY # = "default-secret-key-for-development-change-in-production"In Gevolg vandaag: productie OPI signeert sessie-cookies met de publieke gepubliceerde string. Iedereen die de repo kan lezen kan een Starlette Dit is geen kwetsbaarheid die deze PR introduceert. Deze PR maakt 'm zichtbaar door fail-closed te worden. Ook zonder deze PR mergen is dit een acute incident-class bug. Verplicht vóór mergeToevoegen aan Daarna regenereren van de SOPS-encrypted yaml via de Taskfile: task generate-env-secrets-for-operations-manager CLUSTER_TYPE=odcn-productionDat updatet Side-effect waar je je gebruikers moet voorbereiden: bestaande session-cookies werken NIET meer na rotatie. Alle ingelogde gebruikers worden geforceerd opnieuw in te loggen. Bewust acceptabel? Mocht dat reden zijn voor een geplande deploy-window, dan moet dat met communicatie. Onze commit (
|
| Locatie | Gebruik |
|---|---|
opi/server.py:340 |
Starlette SessionMiddleware — sessie-cookie signing |
opi/api/logs_websocket_router.py:146 |
itsdangerous.TimestampSigner voor websocket log-stream session |
Eén doel (sessions), gedeeld over twee endpoints. Geen JWT in OPI zelf; CSRF gebruikt secrets.token_urlsafe (niet SECRET_KEY). Geen OWASP-overtreding van "separate keys per purpose".
Audit: één commit gepushed, pre-push pytest groen. Belangrijkste actiepunt voor jou: lokaal SECRET_KEY toevoegen + regenereren + committen vóór merge.
De SECRET_KEY had een vaste default ("default-secret-key-for-development-
change-in-production") die in de publieke repository staat. Deze sleutel
ondertekent zowel de sessiecookie (Starlette SessionMiddleware) als de
websocket log-stream sessie. De enige controle was op een lege string,
dus de bekende default kwam er ongehinderd doorheen. Een aanvaller die de
publieke constante kent kan daarmee een sessiecookie met een willekeurige
gebruiker vervalsen: volledige HTTP- en websocket-authenticatiebypass.
Toegevoegd: een fail-closed validatie die bij opstart weigert te starten
wanneer SECRET_KEY ontbreekt, gelijk is aan de bekende dev-default, of
korter is dan 32 tekens, terwijl DEBUG=False (het bestaande dev/prod-
signaal). In DEBUG-modus blijft de default toegestaan met een luide
waarschuwing, zodat lokale ontwikkeling blijft werken.
De validatie zit in een losse module zonder pydantic-afhankelijkheid en
wordt aangeroepen vanuit een model_validator op de Settings. De default
wordt niet stilletjes door een willekeurige waarde vervangen: het doel is
luid falen, niet de misconfiguratie verbergen.
Operators moeten een sterke, unieke SECRET_KEY meegeven via het productie-
secret (rig-system operations-manager env secret).
De oorspronkelijke bewaking faalde open in productie en brak lokale ontwikkeling: - DEBUG was de verkeerde productie/dev-schakelaar. Productie zet alleen DEBUG_MODE (string voor de uvicorn run-mode), nooit de boolean DEBUG, dus DEBUG defaultte naar True en de bewaking gaf in productie enkel een waarschuwing met de publieke default-sleutel. De gecommitte lokale .env zet juist DEBUG=false, waardoor de oude bewaking elke lokale run en pytest-import van opi.core.config liet crashen. - De annotatie `-> Settings` werd op class-body-niveau geevalueerd zonder `from __future__ import annotations`, wat een NameError gaf bij import van opi.core.config (app-start en alle config-importerende tests kapot). De bewaking gaat nu op CLUSTER_MANAGER (consistent gezet per omgeving: local/sandboxed-local voor dev, odcn-production voor productie). Een zwakke, lege of publieke sleutel is alleen toegestaan met luide waarschuwing op een bekende local/sandbox-cluster; elke andere waarde (productie of onbekend) weigert op te starten. Tests oefenen nu ook de echte Settings-validator.
…literal The e2e conftest had the dev-default SECRET_KEY hard-coded as a string literal in the os.environ.get() fallback, duplicating the canonical value in opi/core/secret_key.py. If the central constant is ever rotated, the e2e fallback would silently desync. Import the constant directly so one source of truth owns the value.
Vervangt het 'gecommitte dev-default + CLUSTER_MANAGER gate' patroon door
een rotating per-process default. Geen publieke literal meer in source,
geen cluster-specifieke uitzonderingen.
- SECRET_KEY default via Field(default_factory=generate_secret_key) →
fresh secrets.token_urlsafe per process. Eén WARNING op factory-call
('sessies invalideren bij restart, zet SECRET_KEY voor stabiliteit').
- validate_secret_key: alleen lengte-check (>=32 chars), geen cluster-gate.
Vuurt alleen als operator zelf een te korte key heeft gezet.
- Tests herschreven: weg met dev-default/local-cluster matrix, op met
factory-uniqueness + factory-passes-validator + short-env-raises.
- e2e conftest: weg met DEV_DEFAULT_SECRET_KEY fallback (constant
bestaat niet meer). e2e testserver SECRET_KEY verlengd naar >=32 chars.
70063f1 to
5ccebbb
Compare
Update — rotating SECRET_KEY default i.p.v. gecommitte dev-default (commit 5ccebbb)Tijdens review kwam de vraag op: waarom überhaupt een dev-default in source? Het cluster-gate patroon vermeed wel een crash voor lokale ontwikkelaars, maar zette een publiek bekende literal in de repo en introduceerde een tweede code-pad ("tolerated on local cluster") dat eigen edge-cases (typo'd Cleaner: rotating per-process random key als default, geen literal. Operators die stabiele sessies over reboots willen zetten Wat is veranderd
Design-eigenschappen
Pre-push pytest equivalentSpecifiek Net diff5 files, +94/-176 — netto kleiner dan de oorspronkelijke fix omdat cluster-gate logic + bijbehorende tests verdwijnen. |
Kwetsbaarheid
opi/core/config.pydefinieerdeSECRET_KEYmet een vaste default die in depublieke repository staat:
default-secret-key-for-development-change-in-production.Die sleutel ondertekent twee dingen:
SessionMiddleware(opi/server.py,SessionMiddleware(secret_key=settings.SECRET_KEY))itsdangerous.TimestampSigner(opi/api/logs_websocket_router.py)De enige aanwezige controle was
if not secret_key(lege string). De bekendedefault is geen lege string en kwam er dus ongehinderd doorheen. Niets faalde
gesloten wanneer
SECRET_KEYongezet of de default was terwijl de applicatieproductie-achtig draaide.
Gevolg: een aanvaller die de publieke constante kent ondertekent zelf een
session-cookie met een willekeurigeuseren omzeilt daarmee zowel deHTTP-authenticatie als de websocket log-stream-authenticatie. Volledige
authenticatiebypass.
Bevestigd dat het productie-secret (
bootstrap/.../odcn-production/operations-manager-env-secrets.yaml)momenteel geen
SECRET_KEYzet. Productie draait dus feitelijk op de publiekedefault totdat operators dit instellen (zie onder).
Fix: fail-closed
opi/core/secret_key.pymet de pure functievalidate_secret_key(secret_key, debug),zonder pydantic-afhankelijkheid zodat ze los te testen is.
validate_secret_keyweigert (raisesInsecureSecretKeyError) wanneerSECRET_KEYongezet/leeg is, gelijk is aan de bekende dev-default, of korteris dan 32 tekens, en
DEBUG=False.DEBUGis het bestaande dev/prod-signaal in de codebase (server.py:265,web/router.py:1980); er is geen nieuwe env var geintroduceerd..envlevertDEBUG=false, productie zetDEBUG=False.WARNING-logregel zodat lokale ontwikkeling blijft werken.
model_validator(mode="after")opSettings, dus deapplicatie weigert te booten voordat er verkeer wordt bediend.
sessies onvoorspelbaar invalideren bij herstart en de misconfiguratie
maskeren. Het doel is luid falen.
Actie voor operators
Operators moeten een sterke, unieke
SECRET_KEY(>= 32 tekens) meegeven via hetproductie-secret (
rig-systemoperations-manager env secret). Zonder diesleutel weigert OPI in productie (
DEBUG=False) bewust op te starten.Test plan
uv run pytest --noconftest tests/test_secret_key_failclosed.py(11 passed)in productiemodus; slaagt voor een sterke sleutel of in DEBUG-modus;
controleert de WARNING-logregel in DEBUG
uv run ruff check+uv run ruff formatschoon op gewijzigde bestandenuv run pyrightschoon op gewijzigde bestanden (0 errors)Opmerking:
uv run pytest tests/faalt al bij collectie voor de hele suite dooreen pre-existing omgevingsincompatibiliteit (Python 3.14.0b4 + gepinde pydantic
2.12.5,
import fastapiraised). Dat staat los van deze wijziging en is hierniet aangeraakt. De nieuwe test mijdt die importketen via
--noconftesten delosse
secret_key-module.