12 — GDPR & Compliance Operations (DSGVO)
Purpose
Operational playbook for handling personal data (Mandanten-Stammdaten, Fragebogen-Antworten, Original-Dokumente, AI-derived artifacts) across the Knoll Analyzer stack and the GRAG platform: what lives where, how long it lives, how to export it (Auskunft, Art. 15 DSGVO), how to delete it end-to-end (Löschung, Art. 17 DSGVO), which processors need an AVV (Art. 28), how German PII reaches LLM providers today, and which technical/organisational measures (TOMs) the platform provides vs. what Knoll must add. This is plan task 7.5 groundwork; the go/no-go gate before any real Mandanten data is plan 0.11.
Not legal advice. This document is an engineering/operations checklist. Retention periods, AVV wording, and the lawful-basis analysis must be confirmed by the Kanzlei's counsel/DSB. Where a statutory period is mentioned it is context, not a determination.
Status / verified against: 2026-07-06, repo document-processing-pipelines @ db63a95, live instance https://app.grag.ai. Cross-references: 01-architecture.md, 03-id-conventions.md, 04-provisioning-runbook.md, 05-verification-runbook.md, 10-document-pipeline.md, 13-platform-gaps-issues.md.
Roles in DSGVO terms (to be confirmed by counsel): the Kanzlei is Verantwortlicher for Mandanten data; Knoll Analyzer (the product/operator) acts as Auftragsverarbeiter; the GRAG host operator, LLM providers (via BYOK), and the e-mail provider are Unterauftragsverarbeiter and need to appear in the AVV chain (§4 below).
1. Data inventory — what lives where
Env placeholders used throughout: $GRAG_URL=https://app.grag.ai, $GRAG_API_KEY (Knoll runtime key), $GRAG_TENANT (knoll in prod, knoll-dev in dev).
| # | Data category | Personal data? | Where stored | Retention (as deployed) | Deletion path |
|---|---|---|---|---|---|
| 1 | Client master data (company, contacts, contact details) | Yes | Knoll Postgres (clients, contacts, plan 2.1) | Contract lifetime + statutory commercial/tax retention (typically §257 HGB / §147 AO — confirm with counsel) | SQL delete/pseudonymize (§3 step 8) + KG tombstone (§3 step 5) |
| 2 | Questionnaire answers (16 chapters, free text — high PII density) | Yes | Knoll Postgres (questionnaire_answers) + rendered markdown doc in the analysis-KB (→ rows 4, 5) + deterministic KG facts (row 6) | Until deletion / analysis offboarding | SQL delete + workspaces document DELETE with purge fan-out + KG reconcile (§3 steps 2, 5) |
| 3 | Original-Dokumente (Jahresabschluss, NDA, Verträge…) | Yes | Knoll file store (master, plan 2.4); GRAG keeps NO binaries — only derived text (rows 4, 5, 9) | Until Löschung; statutory retention may apply to some doc types | File-store object delete + per-document DELETE in workspaces (§3 steps 2, 7) |
| 4 | Derived chunks / vectors | Yes (chunk text = document text) | voyager collection named by kb_id behind /ai-gateway/api/v1/retrieval/* (⚠ un-prefixed: TENANT_KEYS_ENABLED=false in .env.production.example:166) + document_points registry in GRAG postgres-platform (workspaces DB) | Until document/KB purge | Purge worker deletes voyager points from the document_points registry — only when WORKSPACES_DOCUMENT_PURGE_ENABLED=true (⚠ off as deployed; plan 1.4) |
| 5 | KG document substrate (segments = full text, spans) | Yes | GRAG postgres-platform, kg logical DB (kg_documents, kg_segments, kg_spans), RLS per tenant | Until purge / kg delete | Purge worker soft-deletes via DELETE /kg-service/api/v1/documents/{id}; hard delete available with ?cascade=true |
| 6 | KG entities & edges (Mandant, Empfehlung, Scores…) | Yes (Mandant entities) | kg_entities (relational, pgvector embedding) + Apache AGE graph kg_knoll; edges live ONLY in AGE (no relational copy) | Indefinite — kg-service has NO entity delete route, edges have no delete route either | Tombstone: upsert properties.status="inaktiv" + filter in all consumers (plan 3.5); edges persist — documented platform limitation, see 13-platform-gaps-issues.md |
| 7 | Conversations (chat turns; sources JSONB embeds full chunk text) | Yes | GRAG postgres-platform, workspaces DB (conversations, conversation_messages) | Until delete; DELETE is soft-only (deleted_at; cascade_delete_supported=false) | DELETE /workspaces/api/v1/conversations/{id} (soft) + operator-level SQL hard delete for Art.-17 finality — platform gap, see §3 step 3 |
| 8 | LLM prompts/responses at providers | Yes (unredacted German text today — see §5) | Provider side (OpenAI/Anthropic/… via BYOK keys in the ai-gateway keyvault) | Per provider policy — TODO-VERIFY: retention terms of the chosen provider/endpoint (e.g. zero-data-retention option) before pilot | Not deletable by us; mitigate upstream (§5) |
| 9 | Transient job payloads (docfold markdown results, orchestrator job archive, DLQ) | Yes (document text) | GRAG Redis: docfold results (TODO-VERIFY exact TTL — "fetch promptly"), job archive 30 d (file_base64 >100 KB stripped), DLQ entries 30 d (contain original_payload + partial_results), chunking/enrichment job keys 24 h | Self-expiring (24 h–30 d) | No per-item delete; rely on TTL. Note in the Löschkonzept: up to 30 d residual |
| 10 | Kosten / Audit | Pseudonymous (tenant/ids, no content) | GRAG ledger Postgres (cost_ledger_entries — retention "keep all rows forever"), capped Redis audit streams (~1000 entries: document_purge_audit, keyvault audit, verdict streams), lineage artifact rows (CAS ids, no payload), Knoll audit_log | Ledger: indefinite; streams: rolling cap; Knoll audit: per policy | Ledger purge is an operator cron (not built); Knoll audit_log retained deliberately as Löschnachweis |
| 11 | PII vault (anonymization placeholders ↔ originals) | Yes, if used | GRAG dpp-postgres-vault (separate cluster, AES-256-GCM, per-tenant HKDF DEKs) | Currently empty for LLM traffic — firewall OFF in prod (§5) | DELETE /anonymization/api/v1/vault/{id} (GDPR hard-delete, audit-logged) |
| 12 | Agent-control event store (hashes-only transcripts) | Hash+length only by default | GRAG postgres-platform (agent_control_events) | 90 d (AGENT_CONTROL_EVENTS_RETENTION_DAYS); profile not started by the deploy workflow today | TTL-based; content capture stays off (double gate not set) |
| 13 | Transactional e-mail (notifications, Dienstleister-Anfragen, plan 2.7) | Yes (names, addresses) | E-mail provider (Resend/SES — decision pending) | Per provider policy — TODO-VERIFY once provider chosen | Provider console / API; cover in AVV (§4) |
Key takeaway: Knoll Postgres + Knoll file store are the masters; everything in GRAG is a derived projection except conversations (which are primary records of chat content) and the cost ledger (pseudonymous). The Löschung procedure must therefore fan out from grag_refs (plan 2.1), which maps every Knoll object to its GRAG ids.
2. Auskunft (Art. 15) — export procedure
Operationalisiert (2026-08-21):
apps/web/scripts/gdpr-auskunft.ts(core:lib/gdpr/auskunft.ts) implements steps 1–4 + 7 below as one audited bundle —pnpm exec tsx scripts/gdpr-auskunft.ts "<Mandant>"writesdb/*.json, the file-store originals, GRAG document inventories + conversation transcripts and a manifest, then logsdata_export. Verified live onknoll-dev(bundle incl. GRAG inventories). The manual curl steps below remain the reference for anything the script does not cover.
Trigger: Mandant (via the Kanzlei) requests a copy of their data. Target: one export bundle per Mandant. All GRAG reads are ordinary tenant-scoped calls with the Knoll runtime key. Reference plan 7.5.
Step 1 — Knoll DB export. Export all rows keyed to the client_id: clients, contacts, analyses (+ milestones, contracts, analysis_team), questionnaire_answers, checklist_items, expert_reports, projects, calculation_line_items, leads, activities, ai_runs, files, grag_refs, relevant audit_log entries. A parameterised export script/server action belongs in the Knoll backend (Partner/Admin-only, itself audit-logged).
Step 2 — File-store originals. For each files row of the client: fetch the object (originals + generated expert-report/offer PDFs) into the bundle. GRAG holds no binaries, so this is the only source of the actual files (plan 2.4).
Step 3 — GRAG document inventory (per analysis-KB, ids from grag_refs):
curl -s "$GRAG_URL/workspaces/api/v1/kbs/$KB_ID/documents" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT"
Include the returned metadata (name, mime, size, status, timestamps) as the processing record for that Analyse.
Step 4 — Conversations export (KB-chat 5.2, KIU chat 5.1b). List, then page messages ascending:
# list conversations of the Analyse-KB
curl -s "$GRAG_URL/workspaces/api/v1/conversations?kb_id=$KB_ID" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT"
# full transcript per conversation (limit 1..1000, order=asc)
curl -s "$GRAG_URL/workspaces/api/v1/conversations/$CONV_ID/messages?order=asc&limit=1000" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT"
Note: assistant messages carry sources/trace/groundedness JSONB — the sources include chunk text from the Mandant's documents; include them (they are the Mandant's data anyway).
Step 5 — KG view of the client (kg-service additionally requires X-Workspace-ID):
curl -s "$GRAG_URL/kg-service/api/v1/entities/client:$CLIENT_SLUG" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT" \
-H "X-Workspace-ID: $WORKSPACE_ID"
curl -s "$GRAG_URL/kg-service/api/v1/entities/client:$CLIENT_SLUG/neighborhood?depth=2" \
-H "Authorization: Bearer $GRAG_API_KEY" \
-H "X-Tenant-ID: $GRAG_TENANT" \
-H "X-Workspace-ID: $WORKSPACE_ID"
(These are projections of Knoll DB facts — step 1 already covers the content; include for completeness of the processing record.)
Step 6 — (optional) per-analysis cost record, if the firm passes costs through: GET $GRAG_URL/ledger/api/v1/ledger/totals?pipeline_id=$ANALYSIS_RUN_ID (same auth headers; per-analysis attribution requires the X-Pipeline-Id stamping from plan 7.3).
Step 7 — Log it. Write a data_export event to audit_log (who, when, scope, delivery channel). Deliver via a secure channel (not plain e-mail attachment).
3. Löschung (Art. 17) — ordered deletion checklist
Operationalisiert (2026-08-21):
apps/web/scripts/gdpr-loeschung.ts(core:lib/gdpr/loeschung.ts) plans and executes the checklist below — DRY-RUN by default, destructive only with--execute; every run writesdeletion_started/deletion_completedaudit entries incl. residuals. Live finding on knoll-dev (2026-08-21, filed as platform issue #406): with the purge flags ineffective the deletes are metadata-level only — the KB vanished, but its document row stayedindexedand voyager kept serving its chunks. The verification probes below (step 11) caught this — treat them as mandatory, and treat the flags precondition as hard.
Scope variants: single document, single Analyse, full Mandant offboarding (shown here — the superset; plan 3.2). Work strictly from the grag_refs inventory.
⚠ Preconditions (current-state caveats — blocking, plan 0.11 / 1.4 / 1.13):
WORKSPACES_DOCUMENT_PURGE_ENABLEDandLINEAGE_CLEANUP_ENABLEDare unset → compose defaults false as deployed: document/KB DELETE is soft-delete only — voyager points, kg segments, and lineage rows remain (deleted docs are merely hidden by the chat live-document filter). Flipping both is plan 1.4 and blocking for real data. Verify live state first (plan 0.9).- Backups: deleted data persists in backups until backup retention expires (PITR retention is 14 days once enabled, §6). State this in the Löschkonzept.
- Statutory retention (HGB/AO) may forbid deleting some Knoll DB records (Verträge, Rechnungsdaten): pseudonymize instead of delete where counsel says retention applies.
Checklist (execute in this order, record each step in audit_log):
-
Freeze Knoll-side. Mark the client
Blocked(no new runs, no chat), snapshot thegrag_refsrows for this client (they are your deletion worklist and later the deletion log). -
Per-document DELETE with purge fan-out. For every document in every analysis-KB of the client (list via
GET /workspaces/api/v1/kbs/{kb}/documents):curl -s -X DELETE "$GRAG_URL/workspaces/api/v1/documents/$DOC_ID" \ -H "Authorization: Bearer $GRAG_API_KEY" \ -H "X-Tenant-ID: $GRAG_TENANT"With the purge flag on, the workspaces-worker then (per
docs/runbooks/document-purge.md): deletes the voyager points from thedocument_pointsregistry → soft-deletes kg segments (DELETE /kg-service/api/v1/documents/{id}) → cascade-deletes the lineage subtree (DELETE /lineage/api/v1/artifact/{docfold_id}?cascade=true) → drops registry rows. Fail-closed: 5xx leaves the job in the consumer group and it replays; watchworkspaces_document_purge_total{tenant,outcome}and thet:{tenant}:document_purge_auditstream. Documents ingested before the points registry existed need the backfill/re-ingest path from the purge runbook before purge can find their points. -
KB delete per analysis.
DELETE $GRAG_URL/workspaces/api/v1/kbs/$KB_ID(same headers) — triggers the voyager/kg/lineage purge fan-out for the collection when the flag is on. -
Conversations.
DELETE $GRAG_URL/workspaces/api/v1/conversations/$CONV_IDfor each — soft-delete only (cascade_delete_supported=false; message rows incl. embedded chunk text remain in Postgres). For Art.-17 finality an operator-level SQL hard delete onconversations/conversation_messagesis currently required — tracked as a platform gap in 13-platform-gaps-issues.md. -
Lineage belt-and-braces. For any
grag_refsartifact ids not covered by step 2's fan-out (e.g. directly ingested questionnaire markdown, expert-report PDFs):curl -s -X DELETE "$GRAG_URL/lineage/api/v1/artifact/$ARTIFACT_ID?cascade=true" \ -H "Authorization: Bearer $GRAG_API_KEY" \ -H "X-Tenant-ID: $GRAG_TENANT"This marks the forward subtree; the destructive fan-out additionally requires
LINEAGE_CLEANUP_ENABLED=true(1.4). Hard delete of lineage rows is admin-only with confirm token (operator action). -
KG tombstoning — there is NO delete route. kg-service exposes no entity delete and no edge delete. Tombstone instead (plan 3.5/3.6):
curl -s -X POST "$GRAG_URL/kg-service/api/v1/entities/upsert" \ -H "Authorization: Bearer $GRAG_API_KEY" \ -H "X-Tenant-ID: $GRAG_TENANT" \ -H "X-Workspace-ID: $WORKSPACE_ID" \ -H "Content-Type: application/json" \ -d '{"entities":[{"entity_id":"client:'$CLIENT_SLUG'","entity_type":"Client","label":"[deleted]","aliases":[],"description":"","properties":{"status":"inactive","deleted_at":"2026-07-06"}}]}'Overwrite
label/description/aliaseswith neutral values (the upsert replaces them) and tombstone every client-specific entity (recommendations, projects of this client). Residual: AGE edges (SCORES,RECOMMENDS, …) persist with their properties (scores, analysis ids — pseudonymous but present); all consumers must filterstatus="inactive". Documented platform limitation → 13-platform-gaps-issues.md; a real fix needs the plan 1.11 write route to grow a delete/retract counterpart. -
Workspace delete.
DELETE $GRAG_URL/workspaces/api/v1/workspaces/$WORKSPACE_ID?cascade=true— executable with the Knoll runtime key (NOT admin-gated; in workspaces only tenant create/patch/delete are). If cascade deletion should be operator-only, enforce that as Knoll-side policy. Tenant soft-delete has NO composite purge — do not rely on it. -
Knoll file store. Delete all objects for the client's
filesrows (originals + generated PDFs). -
Knoll Postgres. Delete/pseudonymize the client's rows (respecting statutory retention, see preconditions). Keep
audit_logand a minimalgrag_refstombstone as the deletion proof. -
PII vault (only if the firewall was ever enabled for tenant
knoll):DELETE $GRAG_URL/anonymization/api/v1/vault/$VAULT_IDper recorded vault id (audit-logged hard delete). -
Verification probe (also in 05-verification-runbook.md; make it part of the 7.2 canary where feasible):
# (a) semantic probe: a distinctive phrase from a deleted document must return 0 hits curl -s -X POST "$GRAG_URL/ai-gateway/api/v1/retrieval/collections/$KB_ID/search" \ -H "Authorization: Bearer $GRAG_API_KEY" \ -H "X-Tenant-ID: $GRAG_TENANT" \ -H "Content-Type: application/json" \ -d '{"query_text":"<distinctive phrase from the deleted doc>","top_k":5}' # expect: no hit whose payload doc_id is the deleted document (or 404 if the collection was deleted) # (b) point count: collection info after KB purge curl -s "$GRAG_URL/ai-gateway/api/v1/retrieval/collections/$KB_ID/info" \ -H "Authorization: Bearer $GRAG_API_KEY" -H "X-Tenant-ID: $GRAG_TENANT" # (c) kg segment gone (expect 404) curl -s "$GRAG_URL/kg-service/api/v1/documents/$DOC_ID/segments/0/text" \ -H "Authorization: Bearer $GRAG_API_KEY" -H "X-Tenant-ID: $GRAG_TENANT" \ -H "X-Workspace-ID: $WORKSPACE_ID" # (d) inventory shows deleted curl -s "$GRAG_URL/workspaces/api/v1/documents/$DOC_ID?include_deleted=true" \ -H "Authorization: Bearer $GRAG_API_KEY" -H "X-Tenant-ID: $GRAG_TENANT"Plus operator-side:
document_purge_auditstream entries showok=1withpoints_deleted/kg_deleted/lineage_cascadedcounts. -
Deletion log. Write a summarising
deletion_completedevent toaudit_log(scope, ids, probe results, residuals: Redis TTL windows ≤30 d, backup retention, AGE edges tombstoned-not-deleted, LLM provider side per §5).
4. Processors & AVV (Art. 28)
| Processor | Role | What the AVV/DPA must cover |
|---|---|---|
GRAG host operator (single Plesk box plesk.easy-dashboard.de, 64 GB, ~90 containers; hosts postgres-platform, vault, Redis, voyager) | Unterauftragsverarbeiter (infrastructure + platform ops) | Physical location & jurisdiction — TODO-VERIFY: datacenter location/hosting contract of the Plesk box is not documented in-repo (deployment report, open question 2); admin access model (GitHub Actions self-hosted runner deploys on push to main; operator secrets in GitHub Secrets); backup handling & restore (§6); incident notification duty; subprocessors of the host |
| LLM providers via BYOK (OpenAI/Anthropic/…, keys in the ai-gateway keyvault) | Unterauftragsverarbeiter (content processing) | Which endpoints (EU data-residency options where offered), retention/zero-data-retention terms, no-training clauses, Art. 44 transfer mechanism if non-EU — TODO-VERIFY per chosen provider before pilot (plan 1.10 decision) |
| E-mail provider (Resend/SES/…, plan 2.7) | Unterauftragsverarbeiter (notifications) | Region/EU processing, retention of message content & logs, subprocessors |
| Knoll file store / object storage host (plan 2.4, D2 hosting decision) | Part of Knoll's own processing or a further subprocessor if externally hosted | Encryption at rest, region, backup handling |
Also required: Verzeichnis von Verarbeitungstätigkeiten (Art. 30) covering the flows in §1, and a Löschkonzept referencing §3 including the residuals (Redis TTLs, backups, AGE edges, provider side).
5. The German-PII-to-LLM problem (plan 1.10 — decide before pilot)
Current state (verified): every LLM call (chat, Gutachten pipeline, extraction) sends unredacted German text to the configured provider. The platform's anonymization firewall is (a) OFF in prod (GATEWAY_FIREWALL_ENABLED=false, .env.production.example:60; guardrail also off, :92) and (b) English-only at the API boundary (language: Literal["en"] in anonymization/src/anonymization/api/schemas/requests.py:24; ADR 0003 defers German to v2), even though the underlying GLiNER2 PII model is multilingual (42 labels × 7 languages incl. German).
Options from plan 1.10:
| Option | What it takes | Assessment |
|---|---|---|
(a) German anonymization v2 + firewall forced on for tenant knoll | Platform work: lift the Literal["en"] boundary (model already multilingual), soak the firewall, per-tenant enforcement (TODO-VERIFY: the exact per-tenant firewall-forcing mechanism/setting — candidates are the ADR 0030 tenant-settings firewall keys) | Cleanest end state; placeholder redaction may degrade German Gutachten fluency — needs quality eval on the 7.1 corpus |
| (b) Documented accepted risk + EU provider endpoints via BYOK + contractual guarantees (DPA, zero/short retention, no training) | Keyvault entry per provider (POST /ai-gateway/api/v1/keys), provider/endpoint selection, AVV chain (§4), Verantwortlicher sign-off | Fast; the personal data still reaches the provider — the protection is contractual + jurisdictional, not technical |
| (c) Knoll-side pseudonymization before every LLM call | Knoll backend redacts (deterministic mapping in Knoll DB) before ai-gateway, restores after; the platform's X-Anon-Already-Redacted carry-vault pattern exists for this direction | No platform dependency, but duplicates a platform capability and risks worse recall than the tuned detector stack; touches every prompt-building path (5.1/5.2/5.3) |
Recommendation: for the pilot (plan 8.3, which uses one anonymized Mandanten-Dossier anyway): option (b) — EU endpoints via BYOK + DPA + documented accepted risk, signed off by the Kanzlei as Verantwortlicher. Target end state before scaling beyond the pilot: option (a), gated on a German redaction-quality check against the 7.1 corpus. Keep (c) as fallback only if (a) stalls. Record the decision in plan 0.11's go/no-go list.
6. Backups (plan 1.13 — blocking for real data)
Current state (verified): the deploy workflow starts neither the pg_dump backup profile nor pitr/pitr-vault (COMPOSE_PROFILES: retrieval only, deploy.yml:108), despite the compose comment "Prod deployments MUST enable this profile or wire an equivalent external backup". The host-maintenance.yml schedule comment references live base-backup windows (02:00 platform / 04:00 vault), so an operator may have brought PITR up manually — ⚠ verify with docker compose ps on the box (plan 0.9/0.11). Until confirmed, assume: no automated backup of Mandanten-bearing data.
Required end state:
| Item | Target |
|---|---|
| GRAG postgres-platform (workspaces, kg, ledger, lineage…) | pitr profile: WAL-G continuous WAL + nightly base backups, libsodium-encrypted (WALG_LIBSODIUM_KEY), 14-day retention (ADR 0015). Bucket location must be documented for §4 (TODO-VERIFY: MinIO-local vs external S3 and its region) |
| GRAG PII vault cluster | pitr-vault profile: separate bucket + separate key (WALG_VAULT_LIBSODIUM_KEY); restore REQUIRES the KEK-decrypt smoke test (smoke_decrypt.py) — a restore across a KEK rotation is silently unreadable otherwise (ADR 0016) |
| Alternative/minimum | backup profile (pg_dump sidecar) or an equivalent external dump job |
| Knoll Postgres + Knoll file store | Own backup plan on the Knoll host (D2 decision explicitly includes disk sizing + backup plan, plan 0.2) — same encryption/retention discipline |
| Restore tests | Once at enablement (plan 1.13 "verify restore once"), then quarterly: platform PITR restore to the restore profile volume + row-count sanity; vault restore + KEK smoke test; Knoll DB dump restore + app boot |
| Backups vs Art. 17 | Deleted data persists in backups until retention expiry (14 d PITR). Document in the Löschkonzept; do NOT restore deleted Mandanten data without re-running §3 afterwards |
7. TOMs checklist (Art. 32) — platform-provided vs Knoll-owned
| TOM | Provided by | Status / action |
|---|---|---|
Tenant isolation: Postgres RLS (ENABLE+FORCE, SET LOCAL app.tenant_id on every pool checkout) on all tenant-bearing tables; per-tenant AGE graph; tenant in CAS pre-image | GRAG (ADR 0013/0001/0025) | Active. Caveat: Redis + voyager prefixes gated on TENANT_KEYS_ENABLED — false as deployed; cutover required before a second real tenant (plan 1.5) |
| Cross-tenant probes answer 404, never 403 (no existence leak) | GRAG | Active |
| Per-tenant cryptography: AES-256-GCM with HKDF-derived DEKs, tenant as AAD (BYOK keyvault + PII vault; separate vault Postgres cluster/trust boundary) | GRAG (ADR 0004/0029) | Active where those stores are used; BYOK secrets are write-only (last4 hint only) |
| Transport encryption | Plesk nginx terminates public TLS for app.grag.ai; Traefik internal on localhost | Active. Knoll host needs equivalent TLS |
BFF authentication — /next/api/* currently unauthenticated (trusts any inbound X-Tenant-ID, injects platform key server-side) | — (platform gap) | ⚠ BLOCKER, plan 1.8; regression probe expecting 401 in plan 0.10a / 7.2 |
Key custody: admin/runtime split via singular env names WORKSPACES_ADMIN_API_KEY / MCP_GATEWAY_ADMIN_API_KEY (setting the plural names in .env does nothing); dedicated Knoll key in PIPELINE_API_KEY list + named GATEWAY_CALLERS entry; never the service key | Operator + Knoll (plan 1.1/1.2, D5) | To do; verify post-deploy: tenant create with runtime key must 403 |
GRAG key never reaches the browser; server-side only (packages/grag-client) | Knoll (plan 2.8/7.7) | To build; grag-client hard-fails on missing X-Tenant-ID (platform allow_default_tenant=true everywhere — a dropped header silently lands in tenant default) |
Budget/rate governance: BYOK monthly_budget_usd + rate_limit_rpm with hard 402/429; Traefik 100 r/s per IP | GRAG (ADR 0029) | Configure in plan 1.6 |
Rate-limited vault reveal: /anonymization/api/v1/vault/{id}/reveal + /deanonymize limited to 10/min (burst 5) per source IP at Traefik; every reveal/delete audit-logged | GRAG | Active (relevant once §5 option (a) lands) |
Audit streams: tenant_settings_audit (Postgres, append-only), t:{tenant}:document_purge_audit, keyvault audit (key id + action + last4 only), safety/groundedness verdict streams — all content-free by design | GRAG | Active; counts/verdicts only, never user content |
| Access control on people: users, roles Partner/Analyst/Backoffice (+ Admin), Partner-only gates (Gutachten-Freigabe, Honorare) enforced server-side | Knoll (plan 2.3/7.7) | To build — GRAG authorizes keys, not people; it has NO user model |
Monitoring/alerting: purge outcome metric, kg_service_age_mirror_failures_total, ledger spend alert at 80 %, uptime probes | GRAG metrics + Knoll alerting (plan 7.3/7.6) | To wire |
| Backups + restore tests | Operator + Knoll | §6 — currently NOT ensured |
8. Knoll-side audit trail (audit_log, plan 2.1)
Write append-only events (actor user id, role, timestamp, ip/user-agent, object type + id, action, outcome, optional diff-summary — never document content). Events worth writing:
| Domain | Events |
|---|---|
| AuthN/AuthZ | login success/failure, logout, password/SSO changes, role change, permission-denied on Partner-only gates |
| Client/analysis lifecycle | client created/changed/blocked/deleted; analysis created/phase-changed/completed; GRAG provisioning + deprovisioning calls with returned ids (mirrors grag_refs) |
| Documents | upload (files id + grag document_id), download (who pulled which original — Art.-32 need-to-know evidence), delete, supersede |
| Questionnaire | submitted, re-submitted (versions), rendered-to-KB (document_id) |
| Expert report | pipeline step start/finish (ai_runs id, model, tokens, cost, groundedness band), Partner approval (who, when, version) — the QMS human-in-the-loop record |
| AI/GRAG calls | per ai_runs row: endpoint class, X-Pipeline-Id, outcome, error taxonomy class (401/403/402/429/503) — no prompt content |
| GDPR operations | data_export (scope, delivery), deletion_started / each §3 step / deletion_completed (probe results, residuals), consent/AVV references |
| Settings/Email | knoll_ai_settings changes, fee changes (Partner-only), notification sends (template id, recipient) |
Retention of audit_log itself: keep beyond client deletion as deletion proof (pseudonymize actor references only if required) — confirm period with counsel.
9. Open items feeding plan 0.11 (GDPR go/no-go)
- plan 1.8 BFF auth fixed + 401 probe green (0.10a).
- plan 1.4 purge flags (
WORKSPACES_DOCUMENT_PURGE_ENABLED,LINEAGE_CLEANUP_ENABLED) verified on live; §3 step 11 probe passes on a fixture document. - plan 1.13 backups running + first restore test done (§6), incl. TODO-VERIFY on the live PITR state.
- §4 AVV chain signed, incl. TODO-VERIFY host location.
- §5 German-PII decision recorded and implemented (1.10).
- Conversation hard-delete gap and KG edge-deletion gap accepted as documented residual risks or fixed platform-side (13-platform-gaps-issues.md).