# Platform Service Threat Model

## Purpose & Audience

This document provides a structured threat analysis of the platform service using the STRIDE framework. It identifies threats, existing mitigations, residual risks, and recommended actions. It is intended for security engineers, architects, and auditors. For the security architecture, see [security.md](security.md). For OWASP-specific analysis, see [compliance-matrix.md](compliance-matrix.md).

**Last reviewed:** 2026-04-10
**Scope:** Platform service (`apps/platform`), its integration points with `apps/server` and `apps/search`, and supporting infrastructure (Aurora MySQL, Redis, AWS KMS).

---

## System Boundaries

```
                         ┌─── Trust Boundary: Internet ───┐
                         │                                │
  User Browser ──HTTPS──→│ ALB/HAProxy                    │
                         │      │                         │
                         └──────┼─────────────────────────┘
                                │
                         ┌──────┼─── Trust Boundary: VPC ─────────────────┐
                         │      v                                         │
                         │  [Express Server]                              │
                         │      │                                         │
                         │      ├── ConnectRPC ──→ [Platform Service]     │
                         │      │                      │                  │
                         │      ├── ConnectRPC ──→ [Search Service]       │
                         │      │                                         │
                         │      ├── TCP ──→ [Aurora MySQL]                │
                         │      │                                         │
                         │      ├── TLS ──→ [Redis/ElastiCache]           │
                         │      │                                         │
                         │      └── HTTPS ──→ [AWS KMS]                   │
                         │                                                │
                         │  [Snowflake] ←── TLS (read-only) ── [Server]  │
                         └────────────────────────────────────────────────┘
```

**Trust boundaries:**

1. Internet → VPC (ALB terminates TLS, validates certificates)
2. VPC → Platform Service (internal network, no mutual TLS in Phase 1)
3. Platform Service → Aurora (TCP within VPC, security group restricted)
4. Platform Service → Redis (TLS for remote, security group restricted)
5. Platform Service → KMS (HTTPS, IAM role authentication)

---

## Assets

| Asset                                       | Sensitivity | Impact if Compromised                                          |
| ------------------------------------------- | ----------- | -------------------------------------------------------------- |
| User identity data (encrypted)              | High        | Account takeover, impersonation                                |
| OAuth tokens (encrypted)                    | High        | Unauthorized access to third-party data (Notion, Google Drive) |
| Audit logs (PII encrypted)                  | High        | Compliance failure, privacy breach                             |
| Encryption keys (AES, HMAC, KMS)            | Critical    | Decrypt all identity data, OAuth tokens, audit PII             |
| Permission configuration (roles, overrides) | High        | Privilege escalation, unauthorized access                      |
| Session tokens (hashed)                     | High        | Session hijacking                                              |
| Conversation content                        | Medium      | Intellectual property exposure, privacy breach                 |
| Tenant configuration                        | Medium      | Service disruption, data residency violation                   |
| Rate limit state                            | Low         | DoS enablement                                                 |

---

## STRIDE Analysis

### S — Spoofing

| #   | Threat                                                          | Target                  | Existing Mitigation                                                                     | Residual Risk                                                                                                                                                                      | Severity | Action                                                                                                              |
| --- | --------------------------------------------------------------- | ----------------------- | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| S1  | Attacker forges JWT to impersonate user                         | Express auth middleware | Auth0 JWKS validation with RS256 signature verification. Issuer and audience validated. | Low — requires Auth0 private key compromise.                                                                                                                                       | Low      | None needed.                                                                                                        |
| S2  | Attacker spoofs `X-Forwarded-For` to bypass rate limiting       | Rate limit middleware   | `express-rate-limit` uses `req.ip` which respects Express `trust proxy` setting.        | **Medium** — if `trust proxy` misconfigured, attacker can rotate IPs.                                                                                                              | Medium   | **Verify** `trust proxy` is set to the ALB's IP range only, not `true` (which trusts any proxy). Add test for this. |
| S3  | Internal service spoofs ConnectRPC requests to platform service | Platform service        | No mutual TLS or service identity verification. Relies on VPC network isolation.        | **Medium** — any process in the VPC can call the platform service.                                                                                                                 | Medium   | **Phase 2:** Add service-to-service authentication (mTLS or signed service tokens).                                 |
| S4  | Attacker uses stolen session token                              | Session management      | Tokens stored as SHA-256 hashes. Absolute expiry (`expiresAt`). Revocation support.     | Low — token theft requires MitM (prevented by TLS) or client compromise.                                                                                                           | Low      | Phase 2: Add session binding (tie token to client fingerprint).                                                     |
| S5  | Search service admin auth bypass                                | Search admin middleware | JWT decoded without signature verification. Email compared to allowlist.                | **Medium** — internal service, but decode-without-verify is fragile. If search service exposed externally (misconfiguration), anyone with a valid Auth0 JWT could claim any email. | Medium   | **Verify** search service is not accessible outside VPC. Add signature verification even for internal services.     |

### T — Tampering

| #   | Threat                                                          | Target                                        | Existing Mitigation                                                                                                                           | Residual Risk                                                                                                             | Severity | Action                                                                    |
| --- | --------------------------------------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------- | ------------------------------------------------------------------------- |
| T1  | Attacker modifies permission cache to grant access              | Redis permission cache                        | Cache is a performance optimization; authoritative data is in Aurora. Cache miss falls through to DB. TTL-based expiry (30s for permissions). | Low — cache poisoning requires Redis access (network restricted). Cache grants are never trusted over DB denials on miss. | Low      | None needed. Current design is correct.                                   |
| T2  | Attacker modifies audit log entries                             | AuditLog table                                | Append-only (no `updatedAt`). `onDelete: Restrict` prevents cascade deletion. DB-level access control via IAM.                                | Low — requires DB admin access.                                                                                           | Low      | Consider DB-level triggers to prevent UPDATE on `audit_logs` table.       |
| T3  | Attacker modifies tenant configuration via proto3 empty strings | updateTenant handler                          | **Fixed.** Handler now uses truthy check (`if (req.name)`) instead of `!== undefined`. Comment documents the rationale.                       | Negligible — empty strings filtered before DB write.                                                                      | Low      | Fixed in `apps/platform/src/domains/platform/handlers/tenant.ts:125-130`. |
| T4  | Attacker tampers with encrypted data in DB                      | Encrypted fields (identity, OAuth, audit PII) | AES-256-GCM provides authenticated encryption. 128-bit auth tag detects any modification.                                                     | Negligible — GCM auth tag will reject tampered ciphertext.                                                                | Low      | None needed.                                                              |

### R — Repudiation

| #   | Threat                                             | Target        | Existing Mitigation                                                                                                     | Residual Risk                                                              | Severity | Action                                                                           |
| --- | -------------------------------------------------- | ------------- | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------------- |
| R1  | Super admin denies performing an action            | Audit log     | `isSuperAdmin` flag, `actingAsUserId` field. All super admin actions audit-logged. `revokedBy` persisted on revocation. | Low — comprehensive audit trail including revocation attribution.          | Low      | Fixed in `apps/platform/src/domains/platform/handlers/identity.ts:334-337`.      |
| R2  | User denies performing a sensitive action          | Audit log     | Every permission check logged with `userId`, `action`, `resource`, `outcome`. `requestId` enables distributed tracing.  | Low — comprehensive audit trail.                                           | Low      | None needed.                                                                     |
| R3  | Audit log entries lost during Redis buffer failure | Audit drainer | Fallback to structured pino JSON log. Entries reconstructable from log files.                                           | **Medium** — if both Redis and pino fail simultaneously, entries are lost. | Medium   | Add monitoring alert on audit drainer error rate. Document log replay procedure. |

### I — Information Disclosure

| #   | Threat                                            | Target                   | Existing Mitigation                                                                                                                                   | Residual Risk                                                                        | Severity | Action                                                                                                                                         |
| --- | ------------------------------------------------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| I1  | Attacker extracts user identities from DB         | User table               | HMAC hash (one-way) for lookup. AES-256-GCM encrypted copy requires privileged key. Raw ID never stored.                                              | Low — requires both DB access and `CODA_DB_IDENTITY_AES_KEY`.                        | Low      | None needed.                                                                                                                                   |
| I2  | Attacker extracts OAuth tokens from DB            | OAuthConnection table    | AES-256-GCM encrypted. Requires `CODA_DB_IDENTITY_AES_KEY`.                                                                                           | Low — same as I1.                                                                    | Low      | None needed.                                                                                                                                   |
| I3  | Audit master key compromise exposes all audit PII | AuditEncryptionKey table | Per-user DEK wrapped by KMS. Master key used for HMAC derivation.                                                                                     | **High** — master key compromise derives ALL per-user DEKs. Single point of failure. | High     | Use AWS KMS `GenerateDataKey` for per-user DEKs instead of HMAC derivation from a single master. This eliminates the single-secret dependency. |
| I4  | Cross-tenant data leakage via cache               | Redis permission cache   | Cache keys include `tenantId` prefix. Tenant suspension triggers prefix-scan purge.                                                                   | Low — cache keys are namespaced. No cache query crosses tenant boundaries.           | Low      | Verified by `inv-tenant-scoped.test.ts` (7 tests).                                                                                             |
| I5  | Error messages leak internal state                | API error responses      | `requireAuth` returns generic "Authentication required" (no JWT details). Error handler returns generic 500 messages. Sentry `sendDefaultPii: false`. | Low — error responses are generic.                                                   | Low      | None needed.                                                                                                                                   |
| I6  | Snowflake query results expose data across users  | Snowflake connection     | Read-only SQL wrapper. Session variables for row-level security. Keyword allowlist.                                                                   | Low — RLS enforced at Snowflake level.                                               | Low      | None needed.                                                                                                                                   |
| I7  | Master key derived from KMS ARN has low entropy   | Audit encryption         | **Fixed.** `AUDIT_MASTER_KEY` env var with Zod validation (`/^[0-9a-f]{64}$/i`). Random fallback in dev with warning log.                             | Negligible — proper 256-bit entropy when configured.                                 | Low      | Fixed in `apps/platform/src/server.ts:128-136` and `load-config.ts:48-54`.                                                                     |

### D — Denial of Service

| #   | Threat                                                          | Target                    | Existing Mitigation                                                                                                                           | Residual Risk                                                            | Severity | Action                                                                                                                       |
| --- | --------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- |
| D1  | Attacker floods API to exhaust rate limits for legitimate users | Rate limit middleware     | IP-based rate limiting: 100 req/min (API), 10 req/min (streaming). Redis-backed for distributed enforcement.                                  | **Medium** — distributed attack from many IPs bypasses per-IP limits.    | Medium   | Phase 2: Add user-based rate limiting (in addition to IP-based). Platform service already has `RateLimit` model designed.    |
| D2  | Attacker triggers expensive permission resolution queries       | Permission resolver       | Cache-first strategy (30s TTL). Parallel DB fallback. Max pipeline step count (16) bounds computation.                                        | Low — cached responses serve within 5ms p99. Cold path is bounded.       | Low      | None needed.                                                                                                                 |
| D3  | Redis failure cascades to service unavailability                | All Redis-dependent paths | Graceful degradation per dependency (security.md failure mode table). Rate limiting fails closed (429). Permission cache falls through to DB. | Low — each dependency has documented fallback behavior.                  | Low      | None needed. Design is correct.                                                                                              |
| D4  | Audit buffer fills Redis memory                                 | Audit drainer             | Drainer processes up to 1,000 entries per 1s interval.                                                                                        | **Medium** — under burst load, buffer could grow faster than drain rate. | Medium   | Add Redis memory monitoring. Consider back-pressure mechanism (drop non-critical audit entries if buffer exceeds threshold). |

### E — Elevation of Privilege

| #   | Threat                                            | Target                        | Existing Mitigation                                                                                                                                                 | Residual Risk                                                                                              | Severity | Action                                                                     |
| --- | ------------------------------------------------- | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | -------- | -------------------------------------------------------------------------- |
| E1  | User escalates to super admin                     | Permission pipeline step 9    | Super admin status stored in DB with `grantedBy` attribution. Cached with 5-min TTL. `NOT_SUPER_ADMIN` sentinel prevents repeated DB queries.                       | Low — requires DB write access to `super_admins` table.                                                    | Low      | None needed.                                                               |
| E2  | User bypasses deny override via super admin grant | Permission pipeline steps 8-9 | Deny-wins invariant: step 8 (deny check) runs BEFORE step 9 (super admin). Deny overrides cannot be bypassed.                                                       | Negligible — verified by `inv-deny-wins.test.ts`.                                                          | Low      | None needed. Architectural guarantee.                                      |
| E3  | User accesses resources in another tenant         | Tenant isolation              | Composite keys `[tenantId, userId, roleId]`. All queries include `tenantId`. Cache keys namespaced by tenant. Role assignment verifies `Role.tenantId == tenantId`. | Low — tenant isolation enforced at DB schema, query, cache, and application levels.                        | Low      | Verified by `inv-tenant-scoped.test.ts` (7 tests).                         |
| E4  | Role hierarchy cycle grants infinite permissions  | Role hierarchy resolver       | BFS traversal with visited-node tracking. Cycle detection prevents infinite loops.                                                                                  | Negligible — verified by `inv-role-cycle.test.ts`.                                                         | Low      | None needed.                                                               |
| E5  | SoD bypass via transitive role inheritance        | SoD validator                 | Direct exclusion pairs checked at assignment time.                                                                                                                  | **Medium** — transitive conflicts (role A excludes B, user has parent of A and is assigned B) NOT checked. | Medium   | Phase 2: Add transitive SoD checking via role hierarchy traversal.         |
| E6  | Expired permission override still grants access   | Permission resolver step 10   | `UserPermission.expiresAt` checked. Index on `expiresAt` enables cleanup queries.                                                                                   | Low — expiry check is part of the resolver pipeline.                                                       | Low      | Add periodic cleanup job to delete expired overrides (reduces query load). |

---

## Risk Summary

### Critical (fix before enforcement)

None remaining. I7 (master key entropy) is fixed.

### High (fix before Phase 1 GA)

| ID  | Threat                                                            | Action                                                                            |
| --- | ----------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| I3  | Single master key derives all per-user DEKs                       | Migrate to KMS `GenerateDataKey` per user (or accept risk with strong master key) |
| NEW | Conversation cache keys contain raw `identityId`                  | Hash keys with HMAC in `conversation-cache.ts`                                    |
| NEW | `OAuthConnection` and `Chat` lack `onDelete: Cascade` from `User` | Add cascade rules or document manual deletion requirement                         |

### Medium (fix before Phase 1 GA or in Phase 2)

| ID  | Threat                                                                                                                                     | Action                                                                                 |
| --- | ------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------- |
| NEW | **Notion OAuth callback open redirect** — `postMessage` origin falls back to `"*"` if `redirectUri` not configured; error HTML not escaped | Require `redirectUri`; escape HTML in error messages (`integration-routes.ts:284-403`) |
| NEW | **Snowflake timestamp interpolation** — `lastPollStart.toISOString()` interpolated into SQL instead of parameterized                       | Use parameterized query placeholders (`search/snowflake/sql-builders.ts:60,72`)        |
| S3  | No service-to-service auth (VPC-only isolation)                                                                                            | Add mTLS or service tokens                                                             |
| S5  | Search admin auth decodes JWT without verification                                                                                         | Add signature verification                                                             |
| R3  | Audit entries lost if Redis + pino both fail                                                                                               | Add drainer monitoring alert                                                           |
| D1  | Distributed DoS bypasses per-IP rate limits                                                                                                | Add user-based rate limiting                                                           |
| D4  | Audit buffer could outgrow drain rate                                                                                                      | Add memory monitoring + back-pressure                                                  |
| E5  | Transitive SoD not checked                                                                                                                 | Add transitive checking in Phase 2                                                     |

### Low / Accepted

All other threats have adequate mitigations. Residual risks are accepted given the current trust model (VPC isolation, TLS, authenticated encryption).

---

## Attack Scenarios

### Scenario 1: Compromised Internal Service

**Threat:** An attacker gains code execution on a service within the VPC (e.g., via dependency vulnerability in the search service).

**Current impact:** The attacker can call the platform service's ConnectRPC endpoints without authentication (S3). They can resolve permissions for any tenant/user, read effective permissions, and potentially trigger audit log entries.

**Mitigation chain:**

1. VPC security groups limit which services can reach the platform service port
2. ConnectRPC handlers validate request fields (tenant ID, user ID) but don't authenticate the caller
3. Write operations (role assignment, permission overrides) would require knowledge of valid UUIDs

**Recommended improvement:** Service-to-service mTLS or signed request tokens (Phase 2).

### Scenario 2: Master Key Exfiltration

**Threat:** An attacker obtains the `AUDIT_MASTER_KEY` environment variable.

**Current state:** The KMS ARN substring bug is **fixed**. `AUDIT_MASTER_KEY` is now a proper 256-bit hex-encoded secret validated by Zod regex (`/^[0-9a-f]{64}$/i`). In dev, a random key is generated with a warning log.

**Current impact:** With the proper 256-bit master key, the attacker can still derive ALL per-user encryption keys via `HMAC-SHA256(masterKey, userId)` and decrypt all audit log PII. The blast radius of a single master key compromise remains total, but brute force is no longer feasible.

**Recommended improvement:**

- **Phase 2:** Migrate to AWS KMS `GenerateDataKey` per user. Each per-user DEK is independently wrapped by KMS. Compromise of one DEK does not expose others. The master key concept is eliminated entirely.

### Scenario 3: Cross-Tenant Escalation Attempt

**Threat:** A user in Tenant A attempts to access resources in Tenant B.

**Mitigation chain (defense in depth):**

1. **JWT scope:** Auth0 JWT contains identity, but no tenant claim. Tenant resolved from `TenantUser` table.
2. **Pipeline step 2:** `ResolveTenant` verifies user has an active `TenantUser` membership. No membership → DENIED.
3. **Pipeline step 3:** Tenant status check — suspended/deactivated tenants reject all requests.
4. **DB queries:** All permission queries include `WHERE tenantId = ?`.
5. **Cache keys:** Namespaced `perm:{tenantId}:{userId}:*` — no cross-tenant cache pollution.
6. **Composite PKs:** `UserRole` PK is `[tenantId, userId, roleId]` — DB-level enforcement.

**Residual risk:** Negligible. Verified by 7 invariant tests in `inv-tenant-scoped.test.ts`.

---

## Maintaining This Document

- **Review cadence:** When attack surface changes (new endpoints, new integrations, new data flows).
- **Trigger for update:** New trust boundary, new asset category, or security incident.
- **Owner:** Platform team lead.
