Files
HiveGrid-Wireframes/hivegrid-cloud/docs/security/threat-model.md
T

35 KiB

Threat Model

STRIDE-based threat model covering every architecture component shipped (or planned) by the boilerplate. Each row links the threat to the static verification gate, runtime control, or test surface that demonstrates the mitigation so the model stays anchored to evidence the codebase already produces. Live runtime proof (penetration test, production incident review) is tracked separately in pen-test-scope.md and the post-incident reviews under incident-response.md.

Review cadence

This document is refreshed when:

  1. A new route class appears in route-access-inventory.md.
  2. A new env var enters packages/config/src/catalog.ts with classification required-core, required-per-app, or production-only.
  3. A milestone closes that introduces a new subsystem (e.g. a billing webhook surface, a new worker queue group, a new vector store backend).
  4. A penetration test or coordinated disclosure finding lands a new control class.

A full review is mandatory before every major-release tag.

Components in scope

# Component Code location Coverage
1 Web apps/web In scope
2 API apps/api In scope
3 Worker apps/worker In scope
4 Database (Postgres + PgBouncer + replicas) packages/db, infra/pgbouncer In scope
5 Two-tier cache (Redis + in-memory L1) packages/shared-store In scope
6 Outbox + dispatcher packages/db, apps/worker/src/jobs/outbox-* In scope
7 Object storage packages/storage In scope
8 Mail packages/mail In scope
9 Realtime collaboration (Hocuspocus) apps/hocuspocus, packages/realtime In scope
10 Plugins (WASM sandbox foundation) packages/plugin-runtime, packages/plugin-sdk, apps/worker/src/jobs/plugin-* In scope; production enablement blocked pending external sign-off
11 AI pipeline + cost tracking + quota packages/ai, apps/worker/src/jobs/ai-* In scope
12 Search indexer (Meilisearch) packages/search, apps/worker/src/jobs/search-indexer* In scope
13 Vector store (pgvector / Qdrant) packages/vector-store In scope
14 RAG pipeline packages/ai/src/embeddings, apps/worker/src/jobs/rag-indexer* In scope
15 Webhooks (inbound + outbound delivery) apps/api/src/routes/webhooks, apps/worker/src/jobs/webhook-delivery* In scope
16 Form widget + lead capture packages/form-widget, apps/web/app/routes/embed/*, apps/web/app/routes/api.leads.capture.ts In scope
17 Feature flags + targeting packages/feature-flags In scope
18 Asset CDN (Bunny pull zone) apps/web/vite.config.ts, scripts/cdn/* In scope
19 Backup + tenant snapshot CLI packages/cli In scope
20 Sentry self-hosted infra/sentry, packages/error-reporter In scope (operator-owned trust)
21 Authentik (planned SSO IdP) infra/authentik In scope (forward-looking)
22 Marketing / Form lead funnel apps/web/app/routes/api.leads.capture.ts, packages/marketing-sdk In scope

Explicitly out of scope for this document (covered by vendor agreements / operator runbooks):

  • Third-party provider compromise (Stripe, Mailjet, OpenAI, Bunny CDN, S3 host)
  • Client-device theft or physical-access attacks
  • DNS hijack / registrar compromise
  • Operator-laptop compromise (covered by IT-security policy, not this repository)

Data-flow diagrams

The diagrams below describe the trust boundaries that the STRIDE matrix references. Solid arrows are synchronous, dashed arrows are asynchronous (queue / outbox).

Authentication flow

flowchart LR
  Browser["Browser (user)"]
  Web["Web SSR<br/>apps/web"]
  API["API<br/>apps/api"]
  DB[("Postgres")]
  Mail["Mail provider<br/>(Mailjet)"]
  Browser -- "POST /login (CSRF token)" --> Web
  Web -- "verifyPassword + issue session cookie" --> API
  API -- "User lookup<br/>(hashed password, TOTP)" --> DB
  Web -- "Set-Cookie: __session (HttpOnly,Secure,SameSite)" --> Browser
  Web -- "magic-link / password-reset" -.-> Mail
  Mail -.-> Browser

Trust boundary: browser → web (CSRF token, origin allow-list, TLS). Boundary: web → API (signed JWT or shared session secret). Boundary: web → mail (provider API key, no PII beyond email + token).

Stripe webhook flow

flowchart LR
  Stripe["Stripe (provider)"]
  API["API /v1/webhooks/stripe"]
  Outbox[("OutboxEvent")]
  Worker["Worker<br/>dispatcher"]
  DB[("Postgres")]
  Stripe -- "POST + Stripe-Signature header" --> API
  API -- "verify signature with STRIPE_WEBHOOK_SIGNING_SECRET" --> API
  API -- "publishOutboxEvent (FOR UPDATE SKIP LOCKED)" --> Outbox
  Outbox -.-> Worker
  Worker -- "apply subscription change" --> DB
  Worker -- "publish cache-invalidation" -.-> DB

Trust boundary: Stripe → API (HMAC signature, replay window from Stripe-Timestamp, idempotency key recorded). Boundary: API → outbox (transactional write — no side-effects before commit). Boundary: worker → DB (per-row tenant scope).

AI request flow (with cost tracking + quota)

flowchart LR
  User["User UI"]
  Web["Web action / loader"]
  AICost["@toolctrl/ai/cost<br/>executeLLMCall"]
  Quota["assertTenantAICreditAccess<br/>(spend-cache, 60s TTL)"]
  Provider["LLM provider<br/>(OpenAI / Scaleway / Anthropic)"]
  Outbox[("outbox.ai-cost")]
  Worker["Worker ai-cost.persist"]
  DB[("AICostEvent")]
  User --> Web
  Web --> AICost
  AICost --> Quota
  Quota -- "ok / degrade / deny" --> AICost
  AICost -- "if ok" --> Provider
  Provider -- "stream tokens + usage" --> AICost
  AICost -- "publish PII-safe payload (requestId-keyed)" -.-> Outbox
  Outbox -.-> Worker
  Worker --> DB

Trust boundary: user → web (auth + tenant). Boundary: web → provider (API key from @toolctrl/config, never read from UI code). Boundary: cost-publish → outbox (idempotency via requestId; payload sanitized — no prompt text persisted by default).

Plugin execution flow (foundation shipped; production sign-off pending)

flowchart LR
  Caller["Caller (API or Web)"]
  Loader["Plugin loader<br/>packages/plugin-runtime"]
  Sandbox["WASM sandbox<br/>(capabilities allow-list)"]
  Host["Host bridge<br/>(DB, mail, http)"]
  DB[("Postgres<br/>tenant-scoped")]
  Caller -- "invoke(pluginId, capability, input)" --> Loader
  Loader -- "verify signed manifest + declared capabilities" --> Sandbox
  Sandbox -- "host-call(capability, args)" --> Host
  Host -- "tenant + permission check" --> DB

Trust boundary: caller → loader (plugin manifest signature). Boundary: sandbox → host (capability allow-list; deny by default). Boundary: host → DB (tenant scope enforced regardless of caller).

GDPR export / erasure flow

flowchart LR
  Admin["Tenant admin / DPO"]
  Web["/admin/users/:id/export"]
  API["API export endpoint"]
  Worker["Worker exports.run"]
  Storage[("Encrypted S3 bucket")]
  Audit[("AuditEvent<br/>hash-chained")]
  Admin --> Web
  Web --> API
  API -- "enqueue exports.run (CRITICAL priority)" -.-> Worker
  Worker -- "stream user data (row-cap by plan)" --> Storage
  Worker -- "append audit entry" --> Audit
  Worker -- "signed URL (15 min)" --> Admin

Trust boundary: admin → web (admin.users.export permission). Boundary: worker → storage (per-tenant prefix, server-side encryption). Boundary: audit chain (forensics; see docs/security/incident-response.md).

STRIDE matrices per component

Each table follows the same shape: Threat (the STRIDE category and concrete vector) | Mitigation (what code/infra/operator control answers it) | Evidence / Gate (the test, verifier, runbook or env-catalog entry that demonstrates the mitigation today) | Residual risk (what remains, accepted by the operator).

1. Web (apps/web)

Threat Mitigation Evidence / Gate Residual risk
S — Forged session cookie Signed cookie via SESSION_SECRET, Secure + HttpOnly, SameSite resolved by runtime packages/config/src/session-cookie.test.ts, apps/web/app/utils/session.server.test.ts Stolen device → session theft. Mitigated operationally by short session TTL + IdP-side revoke (planned with Authentik).
S — Magic-link replay Single-use MagicLinkToken with consumedAt and TTL; rate-limited issuance packages/db/prisma/schema.prisma (MagicLinkToken), apps/web/app/utils/services/magic-link.server.test.ts TTL window allows one-time interception via mail-provider compromise. Accepted; mitigated by Mailjet SPF/DKIM.
T — XSS via user content React escaping by default, CSP planned (see B-005), no dangerouslySetInnerHTML in mutating routes apps/web/app/utils/csp.test.ts, ESLint react/no-danger rule CSP not yet enforced in production headers (tracked in docs/improvement-plan 10-compliance-polish).
T — CSRF on browser mutation Per-session CSRF token required on POST/PUT/PATCH/DELETE; verified header-first apps/web/app/utils/session-csrf-sweep.test.ts, docs/security/csrf-inventory.md None known; new mutating routes must register an inventory entry (verified by test).
R — Privileged action without audit Mutating admin routes emit AuditEvent with hash-chain apps/web/app/routes/admin/audit-trails.test.tsx, verify:permission-matrix Full hash-chain verifier is gated by compliance-audit-export task; chain validation runs nightly.
I — Tenant-isolation leak via crafted ID Loaders resolve tenant from session, not URL; static inventory snapshot verify:tenant-isolation-report, docs/security/tenant-isolation-report.md C3 runtime cross-tenant test still pending (tracked in docs/improvement-plan).
I — Secret in HTML / log process.env access banned in UI routes; pino redact rules packages/config/src/env-access-enforcement.test.ts Operator must verify Sentry PII scrubbers — see docs/security/incident-response.md.
D — Login brute force Sliding-window rate-limit on /login, structured 429 apps/web/app/utils/services/auth.server.test.ts Distributed credential stuffing still possible. Mitigated by password policy + TOTP.
E — Permission bypass Role/permission matrix from permissions-seed.server.ts; role-matrix tests enforce coverage packages/db/src/permissions-seed.server.test.ts, verify:permission-matrix None known.

2. API (apps/api)

Threat Mitigation Evidence / Gate Residual risk
S — Forged JWT HS256 verification with JWT_SECRET; refresh-path requires server-side state apps/api/src/auth/*.test.ts, verify:openapi-security Secret rotation runbook (see incident-response.md).
S — Stolen API key Hashed lookup; API_KEY_LEGACY_PLAINTEXT_FALLBACK=false in production; per-tenant scope at handler apps/api/src/route-access-inventory.test.ts, docs/security/route-access-inventory.md Key leaked in dependency repo → mitigated by hash-only storage.
T — Idempotency replay Idempotency keys recorded per tenant; idempotency table planned in compliance-audit-export apps/api/src/middleware/idempotency.test.ts Window between key issuance and recording. Accepted (millisecond order).
T — Inbound webhook replay Signature verification with provider secret + timestamp window; outbox prevents double-side-effect apps/api/src/routes/webhooks/stripe.test.ts Window of STRIPE_WEBHOOK_TOLERANCE_SECONDS (default 300s).
T — SSRF via outbound webhook URL Outbound delivery rejects RFC1918, link-local, loopback, metadata IPs apps/worker/src/jobs/webhook-delivery.test.ts (block-list) DNS rebind possible during request; mitigated by per-attempt IP resolution.
R — Repudiated admin API call Every /v1/admin/* request emits AuditEvent with request-id linkage apps/api/src/openapi.test.ts, verify:permission-matrix None known.
I — OpenAPI exposes admin route OpenAPI guard rejects routes without explicit auth class apps/api/src/openapi.test.ts, verify:openapi-security None known.
I — Error envelope leak Structured {error: {code, messageKey}} envelope; no raw err.message in mutating routes apps/api/src/error-envelope.test.ts B5 follow-up: a small number of legacy loaders still return string bodies.
D — Unauthenticated analytics flood ANALYTICS_TOKEN gate + per-token rate-limit apps/api/src/route-access-inventory.test.ts (public-analytics-token) None known.
D — Auth burst Login rate-limit returns structured 429 apps/api/src/app.ts rate-limit branch; apps/api/src/auth.test.ts Distributed bursts handled by infra-level WAF.
E — Permission bypass via parameter tampering Permission matrix loaded once at boot; every /v1/* handler asserts assertCan(actor, action, resource) verify:permission-matrix None known.

3. Worker (apps/worker)

Threat Mitigation Evidence / Gate Residual risk
S — Job spoofing Producers are server-side only; BullMQ queues authenticated by Redis ACL packages/redis/src/connection.ts, docs/ops/worker-queues.md Redis-tenant compromise. Mitigated by Redis password + network isolation.
T — Queue poisoning Each producer goes through typed enqueueJob with descriptor validation apps/worker/src/handlers.test.ts, apps/worker/src/enqueue.test.ts None known.
T — Outbox replay OutboxEvent.processedAt/dispatched flag + FOR UPDATE SKIP LOCKED claim apps/worker/src/jobs/outbox-claim.test.ts Two workers in race window: covered by integration test.
R — Side-effect replay without audit Outbox is the only path to external side-effects in critical flows; receivers wrap with withOutboxIdempotency apps/worker/src/jobs/outbox-dispatcher.test.ts Legacy direct producers still exist; migration tracked in #42 follow-ups.
I — Job payload leak PII-redaction at enqueue (@toolctrl/logger redact rules); SBOM excludes payload packages/logger/src/redact.test.ts Operator must verify Sentry breadcrumb scrubbers.
D — Queue starvation Per-group queue separation (general, mail, import, webhook) + concurrency caps + job-priority apps/worker/src/queues.test.ts, docs/architecture/job-priority.md Operator must size per group; see runbook.
E — Worker → DB privilege escalation Worker connects with role limited to required tables; no admin DDL grants infra/postgres/init.sql + Coolify role provisioning Mis-grant by operator → audited via docs/operations/database-rollback.md.

4. Database (Postgres + PgBouncer + replicas)

Threat Mitigation Evidence / Gate Residual risk
S — Forged connection mTLS to PgBouncer in production; per-app credentials in userlist.txt infra/pgbouncer/pgbouncer.ini, docs/ops/pgbouncer.md Operator must rotate per release.
T — SQL injection Prisma parameterized queries only; $queryRaw requires tagged template + lint rule ESLint custom rule + grep guard in CI $executeRawUnsafe banned in app code (allowed in CLI for migrations).
T — Backup tampering Backups encrypted with age; SHA256 manifest signed at write docs/ops/backup-cli.md, packages/cli/src/backup/*.test.ts Encryption key custody: operator-owned.
R — Direct DB write without audit Per-tenant write happens through Prisma + AuditEvent emission; direct psql writes flagged by pg_stat_statements review docs/operations/database-rollback.md Operator runbook.
I — Cross-tenant read Every model query carries tenantId; static inventory snapshot enforced by test verify:tenant-isolation-report RLS not yet enforced; runtime integration test still pending (C3).
I — Replica leak Read replicas configured with hot_standby=on, default_transaction_read_only=on; DATABASE_READ_URL user has SELECT only docs/architecture/rw-split.md, infra/postgres/replica.sql Operator must verify replica grants per release.
D — Connection exhaustion PgBouncer transaction pooling + per-app max_client_conn docs/ops/pgbouncer.md Long transactions block pool; mitigated by statement_timeout.
E — Privilege escalation in DB Per-app role with least privilege; pgcrypto and vector extensions granted to migrator only infra/postgres/init.sql Operator must audit role grants per release.

5. Two-tier cache (Redis L2 + in-memory L1)

Threat Mitigation Evidence / Gate Residual risk
T — Cross-tenant cache poisoning Cache keys always prefixed with tenant:{tenantId}: packages/shared-store/src/two-tier-cache.test.ts Operator-side key-collision risk if Redis is shared with another product. Documented in docs/architecture/two-tier-cache.md.
R — Invalidate without audit Publisher logs pubsub.publish with channel + tenantId; subscribers log invalidation packages/shared-store/src/two-tier-cache.test.ts (publish path) Logs retention by operator.
I — Reading another tenant's data on race L1 cache scoped per-process; L2 keyed per tenant Same as above None known.
D — Pub/sub flood Channel-level rate-limit in publisher; CACHE_INVALIDATION_ENABLED killswitch packages/config/src/catalog.ts (CACHE_INVALIDATION_ENABLED) Operator killswitch covers worst case.
E — Redis ACL bypass Per-app password; Redis users restricted to required key patterns infra/redis/users.acl, docs/ops/worker-queues.md Operator-owned.

6. Outbox + dispatcher

Threat Mitigation Evidence / Gate Residual risk
T — Lost side-effect Outbox row inside same transaction as state change apps/worker/src/jobs/outbox-claim.test.ts None — by design.
T — Duplicate side-effect withOutboxIdempotency keyed by (eventId, receiver) apps/worker/src/jobs/outbox-dispatcher.test.ts Receiver must be idempotent — convention documented.
R — Dispatch without record Each attempt updates attemptCount, lastError, terminal dead-letter state apps/worker/src/jobs/outbox-dispatcher.test.ts None known.
D — Backoff starvation Exponential backoff with cap; dead-letter after N attempts Same Dead-letter must be triaged by operator (runbook in docs/architecture/outbox-pattern.md).

7. Object storage

Threat Mitigation Evidence / Gate Residual risk
S — Spoofed presigned URL Server-side signing with provider IAM credentials; client never holds long-lived keys packages/storage/src/s3.test.ts None known.
T — Upload of malicious file AV scanner layer 1 (magic-bytes) + layer 2 (ClamAV planned) packages/storage/src/av-scan.test.ts, docs/improvement-plan/01-tech-debt-foundation/av-scanner-layer-*.md Layer 2 (ClamAV) container provisioning is operator step.
I — Public bucket exposure Buckets default to private; ACL test enforces packages/storage/src/bucket-policy.test.ts (operator-run) Operator must verify per environment.
D — Egress cost burn Per-tenant rate-limit on signed-URL issuance packages/storage/src/rate-limit.test.ts Operator-side WAF + Bunny CDN absorb large bursts.
E — Cross-tenant object read Object key prefixed with tenant:{tenantId}/...; loader verifies prefix matches actor's tenant packages/storage/src/key-scope.test.ts None known.

8. Mail (packages/mail)

Threat Mitigation Evidence / Gate Residual risk
S — Spoofed sender SPF/DKIM/DMARC enforced at provider; per-domain key isolation Operator: provider DNS records Operator must verify per release.
T — Template injection Templates compiled at build; runtime arguments escaped packages/mail/src/templates/*.test.ts None known.
I — PII in mail subject/body Templates take a minimal context object; pino redact for log lines packages/mail/src/templates/*.test.ts Operator must verify Mailjet retention.
D — Mass-mail abuse Per-tenant + per-recipient rate-limit; bounce + complaint webhooks shut down sender on threshold apps/worker/src/jobs/mail.test.ts Operator must monitor reputation.

9. Realtime collaboration (Hocuspocus)

Threat Mitigation Evidence / Gate Residual risk
S — Anonymous connect JWT auth hook verifies HOCUSPOCUS_AUTH_SECRET token; 15-min TTL apps/hocuspocus/src/auth.test.ts Token leak via XSS → mitigated by short TTL + CSP.
T — Document tampering Yjs CRDT signed updates; persistence layer rejects malformed updates apps/hocuspocus/src/persistence.test.ts None known.
I — Cross-tenant doc read CollabDoc.tenantId enforced at every load; auth hook checks tenant claim apps/hocuspocus/src/auth.test.ts None known.
D — Connection storm Per-document concurrent-connection cap; per-tenant total cap apps/hocuspocus/src/limits.test.ts (planned) Operator must size Hocuspocus container.
E — Editor → DB write without permission Hocuspocus persists only into CollabDoc; downstream mutation requires explicit API call with permission check apps/hocuspocus/src/persistence.test.ts None known.

10. Plugins (WASM sandbox foundation; production sign-off pending)

Threat Mitigation Evidence / Gate Residual risk
S — Unsigned plugin Manifest signature verification, capability consent, and production kill switch docs/features/plugins.md, docs/operations/plugin-lifecycle.md, docs/security/plugin-runtime-signoff.md Production remains blocked until external WASM/security sign-off is complete.
T — Sandbox escape WASM runtime boundary, sanitized outputs, iframe sandbox/CSP, and Phase-C boundary enforcer verify:phase-c-boundaries, packages/plugin-runtime/src/*.test.ts New WASM-runtime CVE -> mitigated by sandbox updates + operator runbook.
R — Plugin action without audit Lifecycle, invoke, hook, and capability-use paths emit audit/outbox evidence with pluginId and tenant scope apps/worker/src/jobs/plugin-*.test.ts, docs/operations/plugin-lifecycle.md External review must validate audit sufficiency before production enablement.
I — Capability exfiltration Default-deny capability list; explicit manifest allow-list and tenant capability grants apps/web/app/routes/app.$tenant/settings/plugins*.test.tsx, apps/api/src/route-access-inventory.test.ts Capability allow-list must be reviewed per plugin install.
D — Resource exhaustion Per-invocation limits, crash counters, queue isolation, and auto-disable behavior apps/worker/src/jobs/plugin-invoke-bootstrap.test.ts, docs/features/plugins.md Operator-tuned per plugin class.
E — Capability abuse / privilege escalation app.plugins.manage / app.plugins.use gates plus tenant-scoped lifecycle/runtime services docs/roles-matrix.md, docs/features/plugins.md, docs/security/plugin-runtime-signoff.md External review remains required before production enablement.

11. AI pipeline (cost tracking + quota)

Threat Mitigation Evidence / Gate Residual risk
S — Forged provider response Provider API keys server-side only; response not trusted for billing — usage echoed back from provider AND reconciled with token-count packages/ai/src/cost/*.test.ts Provider compromise → reconciled at month-end via provider invoice.
T — Prompt injection System prompts isolated from user-content; tool-call schemas validated packages/ai/src/prompts/*.test.ts Cannot fully prevent — operator must review high-trust tool surfaces.
R — AI call without audit executeLLMCall writes AICostEvent via outbox keyed by requestId packages/ai/src/cost/execute.test.ts, apps/worker/src/jobs/ai-cost.test.ts None known.
I — Prompt data leakage Default policy: no prompt text persisted; only token counts + model packages/ai/src/cost/sanitize.test.ts If operator opts into prompt-log retention, GDPR-DSR responsibility is operator.
D — Cost exhaustion Soft + hard tenant quotas (AI_QUOTA_ENFORCEMENT_ENABLED); concurrency counter; spend-cache 60s TTL packages/ai/src/cost/quota.test.ts, docs/features/ai-cost-quotas.md Burst between two cache refreshes accepted (≤60s window).
E — Use of disabled provider/model Provider/model allow-list per tenant; degraded mode swaps to cheaper model packages/ai/src/cost/degrade.test.ts None known.

12. Search indexer (Meilisearch)

Threat Mitigation Evidence / Gate Residual risk
S — Spoofed index write Indexer connects with master-key; tenant-scoped keys used by query path packages/search/src/tenant-scope.test.ts Operator must rotate master key per release.
T — Cross-tenant index pollution Each document carries tenantId; query path always adds tenantId filter packages/search/src/tenant-scope.test.ts None known.
I — PII in search index Indexer applies projection allow-list; sensitive fields stripped before index apps/worker/src/jobs/search-indexer.test.ts Operator must verify projection allow-list per model.
D — Reindex storm Reindex via outbox with backoff; CLI reindex is batched packages/cli/src/search/reindex.test.ts Operator-tuned batch size.

13. Vector store (pgvector / Qdrant)

Threat Mitigation Evidence / Gate Residual risk
T — Cross-tenant vector pollution Tenant-scoped wrapper enforces tenantId on every upsert and query packages/vector-store/src/tenant-scope.test.ts None known.
I — Embedding inversion Operator decides per-tenant whether high-sensitivity data is embeddable; documented in docs/features/rag.md docs/features/rag.md Embedding inversion research is active; high-sensitivity tenants should disable RAG.
D — Index build cost Embedding pipeline rate-limited per tenant; outbox-driven backoff apps/worker/src/jobs/rag-indexer.test.ts Operator-tuned per plan.

14. RAG pipeline

Threat Mitigation Evidence / Gate Residual risk
S — Source spoofing Sources tagged with tenant + permission scope at ingest packages/ai/src/embeddings/source.test.ts None known.
T — Retrieved-context poisoning Retrieval restricted to tenant; per-source access checks packages/ai/src/embeddings/retrieve.test.ts None known.
I — Leak via retrieval Retrieval re-checks actor permission per source Same If permissions change mid-conversation, prior turn may already contain redacted source. Mitigated by short conversation window.

15. Webhooks (inbound + outbound)

Threat Mitigation Evidence / Gate Residual risk
S — Inbound spoofed signature HMAC verification with provider secret; constant-time compare apps/api/src/routes/webhooks/*.test.ts None known.
T — Inbound replay Timestamp window + idempotency key recorded in outbox Same Window default 300s.
T — Outbound SSRF URL block-list (RFC1918, link-local, metadata, loopback); per-attempt DNS resolution apps/worker/src/jobs/webhook-delivery.test.ts DNS rebind: mitigated by per-attempt resolution.
R — Outbound delivery without record Each attempt logs status, response code, latency; signed receipt available to tenant apps/worker/src/jobs/webhook-delivery.test.ts None known.
I — Payload exfiltration Outbound payload built from typed event schema; PII redacted before send packages/webhooks/src/payload.test.ts Operator must vet customer-configured destinations.
D — Slow-loris destination Per-destination concurrency cap + timeout; circuit-breaker after consecutive failures apps/worker/src/jobs/webhook-delivery.test.ts None known.

16. Form widget + lead capture

Threat Mitigation Evidence / Gate Residual risk
S — Forged embed origin Origin allow-list per form (tenant-configured); MARKETING_ALLOWED_ORIGINS for global apps/web/app/routes/api.leads.capture.test.ts Operator must keep allow-list current.
T — Submission tampering Server validates schema; honeypot field; Turnstile gate when enabled apps/web/app/routes/api.embed.forms.submit.test.ts Sophisticated bots may still pass Turnstile; rate-limit caps damage.
T — Embed-snippet tampering SRI hash on widget.js (planned per form-widget detail-md follow-up) docs/features/form-widget.md Until SRI ships, customer must serve widget over HTTPS.
I — Lead PII exposure Submissions stored per-tenant; admin inbox requires marketing.leads.read apps/web/app/routes/admin.marketing-leads.test.tsx None known.
D — Spam flood Sliding-window rate-limit (5/min IP, 1/min email); Turnstile when enabled apps/web/app/routes/api.leads.capture.test.ts Distributed bots may evade IP limit.

17. Feature flags + targeting

Threat Mitigation Evidence / Gate Residual risk
T — Targeting rule poisoning Rules edited only by users with feature-flags.write; AuditEvent on every change packages/feature-flags/src/audit.test.ts None known.
R — Silent kill-switch AuditEvent emitted on enable/disable; admin UI shows last actor + timestamp Same None known.
I — Rule data leak Rule fields never echoed in user-facing logs packages/feature-flags/src/serialize.test.ts None known.
D — Eval-cache thrash Two-tier cache with pub/sub invalidate; killswitch docs/architecture/two-tier-cache.md Operator killswitch.

18. Asset CDN (Bunny pull zone)

Threat Mitigation Evidence / Gate Residual risk
T — Cache poisoning at edge Origin returns immutable Cache-Control for fingerprinted assets; non-fingerprinted assets short TTL scripts/cdn/cache-policy.test.mjs Operator must verify pull-zone config.
I — Source-map exposure Source-maps not uploaded to public CDN in production builds; Sentry-side only apps/web/vite.config.ts (sentry-vite-plugin config) Operator must verify per release.
D — Origin shielding Bunny edge absorbs bursts; origin protected by Coolify rate-limit docs/ops/asset-cdn.md Operator-tuned.

19. Backup + tenant snapshot CLI

Threat Mitigation Evidence / Gate Residual risk
S — Restore in production NODE_ENV=production guardrail; super-admin required for --profile=none packages/cli/src/tenant/snapshot.test.ts Operator-side training.
T — Snapshot tampering age encryption + SHA256 manifest; manifest signed packages/cli/src/backup/sign.test.ts Key custody by operator.
R — Restore without audit CLI emits structured audit log; required --reason flag packages/cli/src/tenant/restore.test.ts None known.
I — PII in snapshot 3 PII profiles (full, minimal, none); deterministic salt-seeded anonymization docs/ops/tenant-snapshot.md Operator must select correct profile per use-case.

20. Sentry self-hosted

Threat Mitigation Evidence / Gate Residual risk
I — PII in error / breadcrumb PIISpanProcessor strips PII before send; client + server scrubbers packages/observability/src/pii.test.ts Operator must verify per release; PII-scrubber test required on every Sentry SDK upgrade.
I — Cross-tenant event leak Per-project DSN; tenant attached as tag (not PII), not user-id packages/error-reporter/src/sentry.test.ts Operator must keep project isolation.
R — Event tampering at Sentry Sentry self-hosted = single tenant; operator-owned trust boundary infra/sentry/README.md Operator-owned.
D — Event storm Sentry rate-limits per DSN; client-side sampling packages/error-reporter/src/sample.test.ts None known.

21. Authentik (planned SSO IdP)

Threat Mitigation Evidence / Gate Residual risk
S — Forged SSO assertion OIDC/SAML signature verification; pinned issuer per environment infra/authentik/README.md (planned) None expected.
R — Login without audit on IdP Authentik emits its own AuditEvent stream; replicated to platform audit Same Operator-owned.
I — User-attribute leak Attribute mapping reviewed per release; minimum-necessary claim set Same Operator-owned.
D — IdP outage Fallback to local credentials for break-glass admin Same Documented in docs/security/incident-response.md.

22. PgBouncer

Threat Mitigation Evidence / Gate Residual risk
T — Pool contamination Transaction-mode pooling; no session-level GUCs from app docs/ops/pgbouncer.md Operator must verify server_reset_query per release.
I — Credential leak via SHOW admin_users restricted; SHOW permissions audited infra/pgbouncer/pgbouncer.ini Operator-owned.
D — Pool exhaustion Per-app max_client_conn + statement timeout on origin Same Documented in runbook.

Cross-cutting controls

  • Secret handling — secrets never read from UI code; @toolctrl/config exposes typed loaders. .env.test allow-listed; .env git-ignored. See docs/env-access-inventory.md.
  • Branding / infra parametrization — BRAND_* env vars and scripts/infra-config.mjs give forks override knobs without removing the verify-gate contract — see docs/deployment-porting.md.
  • Migrations — structural Prisma diff verified via verify:db-migrate-status; rollback procedure documented in docs/operations/database-rollback.md.
  • Audit chain — every privileged mutation emits an AuditEvent; hash-chain verifier planned in compliance-audit-export is the canonical forensics source referenced by incident-response.md.
  • SBOM — generated per build; see sbom.md.

Gaps tracked in the roadmap

The following items are knowingly incomplete and tracked under the improvement plan:

  • B4 — Mail templates currently hardcode English subjects/bodies (docs/improvement-plan 04-i18n).
  • B5 — Response.json("Unauthorized", …) style stringly errors still exist in some loaders.
  • C1 / C2 — Handler coverage below target (API < 70 %, worker < 50 %).
  • C3 — Cross-tenant runtime integration test pending.
  • D3 — packages/logger / packages/error-reporter Pino+Sentry adapters complete; PIISpanProcessor verification is per-release.
  • CSP enforcement in production headers — tracked under 10-compliance-polish.
  • Plugin sandbox (component 10) — full implementation pending phase-06.
  • Authentik IdP (component 21) — staging-only today; production cutover tracked in phase-08.

Each gap has an owning task in the improvement plan and a matching commit category.