# Security Controls ## Identity OIDC voor mensen; gehashte, roteerbare service tokens met scopes en expiry voor applications. Tokens worden slechts eenmaal getoond. ## Authorization Deny by default, explicit grants, effective-space intersection, use-case policies, database scoping, verplichte Qdrantfilters en resource reauthorization voor citations/downloads. ## Operations isolation Iedere workspace-scoped job draagt expliciete ownership: `jobs.jobs.workspace_id` is een echte kolom (migratie 0018), geen veld in `payload`. Elke lijst, teller, detailpagina en mutatie filtert daarop in de query zelf --- nooit op rijen die al opgehaald zijn. - `PLATFORM_ADMIN` krijgt de expliciete globale scope (`workspace_id=None`) en ziet dat ook op het scherm staan (`Scope: All workspaces`). - `WORKSPACE_ADMIN` en `AUDITOR` zijn gebonden aan `principal.workspace_id`, voor lijst, statustellers, filters, source-namen, detail en retry. `AUDITOR` is read-only: retry vereist een rol uit `_RETRY_ROLES`. - Een job zonder ownership (`workspace_id IS NULL`, alleen mogelijk voor rijen van vóór 0018 waarvan de eigenaar niet betrouwbaar vast te stellen was) faalt gesloten: onzichtbaar en onmuteerbaar voor iedere workspace-scoped principal, uitsluitend inspecteerbaar door een platform admin. Er is nergens een `payload.get("workspace_id") or principal.workspace_id`-fallback; die kende een ownerloze job toe aan wie er toevallig naar keek. - Tellers zijn ook tenantdata. Dashboard- en System Health-queuecijfers gebruiken dezelfde scope als de Jobs-lijst. - Ook de operatorfilters (status, type) en de paginering zijn predicaten van diezelfde query (`JobListFilter` --- migratie 0019 voegt de bijbehorende index toe). Er wordt dus geen bredere set opgehaald dan getoond wordt: elke rij die dit proces binnenkomt is er één die deze principal mag zien. De vijf tellers boven de tabel blijven bewust een scope-brede samenvatting van de wachtrij en het scherm zegt dat er ook bij. - Retry dwingt ownership af in het `UPDATE` zelf, niet in een voorafgaande read: een cross-tenant mutatie die alleen onder concurrency lukt is nog steeds een cross-tenant mutatie. Een geweigerde toegang of retry wordt vastgelegd als `SECURITY_FAILURE` met actor, workspace, actie, doel en reden --- zonder payload. Nieuwe jobs kunnen niet ownerloos ontstaan: `JobService.enqueue` eist `workspace_id` en `PostgresJobRepository.enqueue` weigert een `Job` zonder. ## Filesystem-backed sources Een Folder-source, en de lokale checkout root van een Git-source, geven de worker een leesprimitief op de host: alles onder dat pad wordt vindbare content. Dat is iets anders dan content beheren, dus het vereist `FILESYSTEM_SOURCE_MANAGE` *bovenop* `SOURCE_MANAGE`, bij zowel aanmaken als bewerken (bewerken is precies hoe een goedgekeurde source op een andere directory gericht wordt). Gekozen model: **alleen `PLATFORM_ADMIN`**. `RAGCORE_SOURCE_ROOTS` is één deployment-brede lijst zonder per-workspace mapping, dus een pad binnen de allowlist kan prima het corpus van een andere tenant zijn; een workspace admin met dit recht zou workspace B kunnen lezen door er een source op te richten. Zolang roots niet per workspace geconfigureerd zijn, is het kiezen van een serverpad een deploymentbeslissing en hoort die bij de rol die de deployment bezit. Per-workspace roots (het sterkere self-service model) blijven mogelijk als self-service filesystem sources later echt nodig blijken. Alleen de gevoelige kinds zijn beperkt: `SOURCE_MANAGE` blijft ongewijzigd voor Editors en Space Managers, en upload-sources worden er niet moeilijker door. Autorisatie en sandbox zijn twee onafhankelijke poorten, allebei altijd vereist: - `RAGCORE_SOURCE_ROOTS` blijft lexicaal afgedwongen bij opslaan, en volledig geresolved (symlinks gevolgd) in worker en connector vlak voor iedere read. Een pad buiten elke root wordt geweigerd --- ook voor een platform admin. Zonder geconfigureerde roots kan geen enkele filesystem-source bestaan. - De source mounts horen read-only te zijn. Dat is een security-invariant, niet alleen een aanbeveling: het is wat het resterende TOCTOU-venster tussen padvalidatie, `stat` en `open` onschadelijk maakt. Zijn de directories muteerbaar door niet-vertrouwde actoren, dan is die aanname geschonden. ## Network Alleen app achter reverse proxy. PostgreSQL/Qdrant/worker op internal network. Geen debug/adminpoorten publiek. Egress voor connector/modelworkers zo beperkt mogelijk. ## Content MIME sniffing, allowlists, size/time/depth limits, archive-bombdetectie, quarantine, sandboxing waar haalbaar, geen macro/scriptuitvoering en read-only source mounts. ## Secrets Environment/secrets mount, geen plaintext in database/logs/evidence, redaction processor, onafhankelijke token pepper en rotatieprocedure. ## Abuse prevention Rate limits per principal/application, request/body/token/context limits, bounded concurrency, job quotas en circuit breakers. ## Audit Append-only events voor auth, grants, source changes, publication, modelroute, adminacties en security failures. Querycontentlogbeleid is per space configureerbaar.