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:
- A new route class appears in
route-access-inventory.md.
- A new env var enters
packages/config/src/catalog.ts with classification required-core, required-per-app, or production-only.
- A milestone closes that introduces a new subsystem (e.g. a billing webhook surface, a new worker queue group, a new vector store backend).
- 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 (Legacy-Provider, 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
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).
Legacy-Provider webhook flow
Trust boundary: Legacy-Provider → API (HMAC signature, replay window from Legacy-Provider-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)
Trust boundary: user → web (auth + tenant). Boundary: web → provider (API key from @hivegrid/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)
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
Trust boundary: user/PortalUser → the Core-Privacy- beziehungsweise
Customer-Portal-Route (Session, Tenant-/PortalUser-Ownership und Permission).
Boundary: Runtime → Storage. worker-import darf in die beiden deterministischen
GDPR-Prefixe schreiben, Core und Portal dürfen jeweils nur ihr eigenes Prefix
lesen, worker-general darf die beiden Export-Prefixe und den bestehenden
Tenant-Hard-Delete-Prefix nur eng begrenzt listen und löschen. Jede Runtime hat ein
eigenes Access-Key-Paar; nur Writer und Reader teilen den serverseitigen
32-Byte-SSE-C-Customer-Key. Dieser Key wird weder an den Browser noch in eine URL
gegeben. Exporte tragen keine benutzerdefinierten S3-Metadaten, weil Hetzner bei
SSE-C nur den Objektinhalt, nicht solche Metadaten verschlüsselt.
Der export-first Keyspace trennt
gdpr-exports/privacy/tenants/{tenantId}/{exportJobId}/{attemptId}.zip von
gdpr-exports/portal/tenants/{tenantId}/{exportJobId}/{attemptId}.json. Die
zufällige beziehungsweise deterministisch versuchsspezifische attemptId
verhindert, dass ein verlorener Retry ein bereits veröffentlichtes Artefakt
überschreibt. Die Portal-Objektadresse enthält keine PortalUser-ID.
Downloads prüfen Header, erwartete Länge und Obergrenze vor der Response; die
Byte-Obergrenze und exakte Endlänge werden während des begrenzten Streamings erneut
erzwungen. Download-Zähler und Audit dokumentieren deshalb den autorisierten
Download-Start, nicht den vollständig konsumierten Browser-Body. Es gibt keine
Presigned URL und keinen direkten Browserzugriff auf den Bucket. Privacy ist
ab 72 Stunden, Portal ab 24 Stunden nicht mehr abrufbar. Der 15-minütige Scheduler
löscht im ersten erfolgreichen Sweep nach Ablauf; Provider-/Queue-Ausfälle können
die physische Löschung verlängern und müssen deshalb bis zum erfolgreichen Retry
alarmieren. Die Zugriffssperre der App bleibt davon unabhängig sofort wirksam.
Bei deaktiviertem Versioning und Object Lock löscht eine auf gdpr-exports/
begrenzte dreitägige Provider-Lifecycle-Regel verwaiste Attempts als letzte
Rückfallebene. Sie darf keine anderen Bucket-Prefixe erfassen und ersetzt weder
den 15-Minuten-Sweep noch dessen Alarmierung; Provider-Lifecycle-Ausführung ist
asynchron und deshalb kein minutengenauer Retention-Nachweis.
Repository-Tests und Deploy-Preflight belegen diese Code- und Env-Grenzen, aber
nicht die tatsächlich aktive Hetzner-Bucket-Policy oder Provider-Semantik. Der
verbindliche Positiv-/Negativ-Proof ist daher der redigierte Staging-Operator-
Checkpoint in
docs/deploy/customer-portal.md.
Der legacy Pfad apps/worker/src/jobs/dsgvo-export.ts und generische Presigned-URL-
Storage-Tests sind kein Nachweis für diesen GDPR-Vertrag. Boundary: Audit-Kette
(Forensik; siehe docs/security/incident-response.md).
Beim SSE-C-Cutover müssen die früheren Klartext-Keyspaces
tenants/*/gdpr/*.zip und tenants/*/portal-gdpr/*/*.json inventarisiert,
gelöscht und ihre DB-Referenzen abgelaufen beziehungsweise entfernt werden. Der
neue Reader bleibt für diese Legacy-Objekte fail-closed; es gibt keinen
Klartext-Fallback.
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; track via HiveGrid security ticket. |
| 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; track via HiveGrid security ticket. |
| 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/refetch with provider secret + replay controls; outbox prevents double-side-effect |
billing webhook tests |
Provider-specific replay window or refetch policy. |
| 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 |
Browser ingest capability plus per-IP and bounded anonymous-visitor rate limits; identity fields are rejected and writes run in an isolated tenantless RLS context |
apps/api/src/route-access-inventory.test.ts (public-analytics-token) |
The browser capability is intentionally public; infrastructure/WAF limits remain defense in depth. |
| 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 (@hivegrid/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 as hivegrid_worker: NOBYPASSRLS, no role memberships, no DDL authority; job context may set only the trusted tenant identity |
scripts/provision-rls-roles.mjs, apps/worker/src/rls-context.ts, packages/db/src/rls-runtime-readiness.server.ts, verify:rls-live |
The production role/Coolify state still has to be verified during rollout; a mis-grant is a release blocker. |
4. Database (Postgres + PgBouncer + replicas)
| Threat |
Mitigation |
Evidence / Gate |
Residual risk |
| S — Forged connection |
PgBouncer is non-public, uses SCRAM-SHA-256, and carries separate credentials for hivegrid_app, hivegrid_worker, callback-only hivegrid_authentik_jit, and its admin probe |
infra/pgbouncer/pgbouncer.ini, docs/ops/pgbouncer.md, scripts/provision-rls-roles.mjs |
Transport/network controls and credential rotation remain operator-owned and must be verified in the target environment. |
| 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/write |
Every Prisma model is explicitly classified; all 264 RLS definitions are generated from a reviewed policy contract, tenant tables use ENABLE + FORCE RLS, and runtime GUCs are transaction-local |
verify:tenant-policy-contract, verify:rls-release-readiness, verify:rls-live, prisma/migrations/20260713190000_appwide_rls_cutover/migration.sql |
Repository and ephemeral PostgreSQL proof do not prove the production cutover. Existing production data, roles, PgBouncer, replicas, and Coolify env must pass the rollout checks before traffic is enabled. |
| I — Replica leak |
The read client is probed against the same restricted runtime-role, forced-RLS, policy, definer-allowlist, and empty-GUC contract as the write client |
packages/db/src/rls-runtime-readiness.server.ts, docs/architecture/rw-split.md |
Actual replica role/grant and migration lag must pass readiness in the target environment before it receives traffic. |
| 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 |
hivegrid_app, hivegrid_worker, and hivegrid_authentik_jit are LOGIN NOBYPASSRLS without memberships; the JIT login has no table/sequence privileges and can execute exactly one callback capability. hivegrid_migrator is non-superuser and may only SET ROLE to the NOLOGIN BYPASSRLS owner of an exact allow-list of narrow SECURITY DEFINER functions |
scripts/provision-rls-roles.mjs, packages/db/src/rls-runtime-readiness.server.ts, verify:rls-live |
The definer allow-list and grants must be reviewed whenever a new pre-tenant/platform capability is introduced; production grant drift fails readiness. |
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; GDPR exports use no presigned URL |
packages/storage/src/s3-client.test.ts, packages/storage/src/sse-c.test.ts |
GDPR provider policy remains an operator checkpoint. |
| T — Upload of malicious file |
AV scanner layer 1 (magic-bytes) + layer 2 (ClamAV planned) |
packages/storage/src/av-scan.test.ts, docs/ops/asset-cdn.md |
Layer 2 (ClamAV) container provisioning is operator step. |
| I — Public bucket exposure |
Buckets default to private; runtime keys remain action/prefix scoped |
packages/storage/src/s3-client.test.ts, operator checkpoint in docs/deploy/customer-portal.md |
Operator must verify per environment. |
| D — Egress cost burn |
Distributed principal/artifact gate and bounded streaming on authenticated GDPR proxy; bounded signed-URL issuance elsewhere |
apps/web/app/utils/privacy/gdprExportDownloadGate.server.test.ts, packages/storage/src/sse-c.test.ts |
Operator-side WAF absorbs larger public bursts. |
| E — Cross-tenant object read |
Tenant-bound key guards plus action/prefix-scoped runtime credentials |
packages/storage/src/access/index.test.ts, packages/storage/src/paths/index.test.ts, packages/storage/src/sse-c.test.ts |
Provider policy remains an operator checkpoint. |
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 + native platform-lead intake
| Threat |
Mitigation |
Evidence / Gate |
Residual risk |
| S — Forged platform source |
Source-specific hashed Bearer credential; source identity comes only from that credential |
apps/api/src/routes/platform-lead-routes.test.ts |
Credential compromise requires immediate rotation/revoke. |
| T — Submission tampering/replay |
Strict schema/body limit plus mandatory source-partitioned idempotency hash |
apps/api/src/routes/platform-lead-routes.test.ts, packages/db/src/platform-leads.rls.integration.test.ts |
A producer must reuse its submission ID until acknowledgement. |
| 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 — Platform-lead PII exposure |
Operator RLS + read_own/read_all; PII-free audit/outbox/queue payloads |
packages/db/src/platform-leads.rls.integration.test.ts, apps/worker/src/jobs/platform-notification-outbox.test.ts |
Privileged operators still require normal access review. |
| D — Spam/credential probing |
Pre-auth IP limit plus source-specific rate limit; disabled ingress returns neutral 404 |
apps/api/src/public-ingress-rate-limit.test.ts, apps/api/src/routes/platform-lead-routes.test.ts |
Distributed probing remains observable via ingress metrics. |
| R — Stale browser writer |
Legacy /api/leads/capture is write-free and returns 410; marketing.js removed |
apps/web/app/routes/api.leads.capture.test.ts |
Keep drain monitoring until all stale clients disappear. |
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;
@hivegrid/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 keep
runtime configuration explicit; see docs/deployment.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; the planned HiveGrid hash-chain verifier is the canonical forensics source referenced by
incident-response.md once it lands.
- SBOM — generated per build; see
sbom.md.
Gaps tracked in the roadmap
The following items are knowingly incomplete and should be tracked through HiveGrid tickets:
- B4 — Mail templates currently hardcode English subjects/bodies.
- 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.