Security
:::info Source
Sourced from services/assignment-service/SECURITY_MODEL.md in the documentation repo.
:::
Companion: 13 Security Compliance Tenancy · 14 Risks & Tradeoffs
1. Threat Model (STRIDE)
| Threat | Asset | Control |
|---|---|---|
| Spoofing (cross-tenant request) | Any assignment/window | JWT tenant claim + X-Tenant-Id must match + RLS current_setting('ghasi.tenant_id') |
| Tampering (window state bypass) | compliance_window | Writes only via service code; RLS+ORM enforce invariants; CHECK constraints on state |
| Repudiation (who activated?) | Assignment audit | Immutable activated_at, created_by; escalation_log retained 7y |
| Information disclosure (list all tenants' compliance) | compliance-report | RLS, role check, SQL function denies cross-tenant input |
| Denial of service | Materializer | Rate-limited, horizon-capped, advisory locks prevent runaway |
| Elevation of privilege | Role=learner writing assignments | Fine-grained RBAC at handler boundary + RLS safety net |
2. Authentication
- Gateway validates JWT issued by identity-service (RS256 via JWKS).
- Service requires every inbound HTTP to carry
Authorization: Bearer <jwt>. - Token minimum claims:
sub(userId),tenant_id,roles[],scope[],exp,iss='https://auth.ghasi.com'. - Service-to-service (NATS subscribers, outbox dispatcher): mTLS on internal mesh + signed service JWT (
iss=internal) for cross-service HTTP.
3. Authorization
Policy engine: OPA Rego policies compiled into a local evaluator (@ghasi/policy).
# assignment.rego (excerpt)
allow {
input.action = "assignment.create"
role_has_scope(input.roles, "assignments:write")
input.tenant_id == input.jwt.tenant_id
}
allow {
input.action = "window.read.self"
input.path.userId == input.jwt.sub
}
Role → scope matrix (S4+):
| Role | Scopes |
|---|---|
learner | assignments:read:self |
manager | assignments:read:ou, windows:read:ou |
compliance_admin | assignments:write, windows:read, report:read, ai.suggest:call |
tenant_admin | superset of compliance_admin |
auditor | assignments:read, windows:read, report:read (read-only) |
support | time-bounded impersonation with support_ticket_id; logs tagged support-mode=true |
4. Tenancy Isolation
Enforced in three layers:
- Gateway: rejects requests whose JWT
tenant_id≠X-Tenant-Id. - Service code: every repository call includes
tenantIdexplicitly; handlers type-check against the JWT claim. - Database RLS: every table has RLS policy using
current_setting('ghasi.tenant_id'). The service sets this on each connection viaSET LOCALat the start of every request's tx.
No bypass. Even super-admin ops go through explicit tenant-selection UX.
5. Data Classification
| Data | Class | Notes |
|---|---|---|
assignment.* metadata | Internal | Not personally sensitive unless titles contain PII |
compliance_window.user_id | PII-linked | Subject to GDPR/HIPAA right to erasure |
compliance_window.state + timestamps | Regulatory evidence | Retained 7y for audit |
escalation_log | Internal | Retained 7y |
ai_provenance | Internal / observability | May be masked per tenant policy |
6. Encryption
- In transit: TLS 1.3 only. Internal mesh uses mTLS via SPIFFE SVIDs.
- At rest: AWS RDS/Aurora AES-256. EBS-level encryption on all volumes.
- Application-layer: not required for current schema (no free-text PII). Reserved.
- Secrets: AWS Secrets Manager; rotated quarterly. No secrets in env vars committed.
7. GDPR / HIPAA Subject Requests
Consumer of gdpr.subject_request.received.v1:
| Kind | Action |
|---|---|
export | Emit assignment.gdpr.export.produced.v1 with tar.gz index of all windows for subject |
erasure | For each compliance_window where user_id=subject: if state∈{completed, closed_missed} preserve as pseudonymised (replace user_id with erased:<hash>); if state∈{open, in_progress, overdue} close with reason gdpr_erasure. Publish assignment.gdpr.erasure.applied.v1. |
restriction | Set state='paused' on all open windows for subject (reversible). |
Pseudonymisation preserves aggregate-count integrity for compliance reports while removing identifiability.
Retention exemption: if tenant regulatory regime requires preservation beyond erasure request (e.g., FDA 21 CFR Part 11), we mark records regulatory_hold=true and surface this in the subject-access response.
8. Input Validation
- Zod on every HTTP input.
- RRULE: parse via
rrule.jsin strict mode; reject non-RFC-5545, rejectUNTILbeyond 10 years, rejectCOUNT > 200, rejectINTERVAL > 50. - ISODuration: parse + cap (
dueOffset≤ P365D,gracePeriod≤ P30D). - Targets:
userIDs validated against format; group/OU IDs validated against tenant projection at activation. - JSON bodies: max 256 KB; reject deeper than 8 levels of nesting.
9. Rate Limiting
Per-tenant token bucket (Redis) in addition to API Gateway global limits (see API_CONTRACTS §6).
10. Audit Logging
Every state transition emits a structured audit log line:
{
"actor": "usr_…", "action": "assignment.activate",
"entity": "assignment", "id": "asn_…",
"tenant_id": "tnt_…", "trace_id": "01HXYZ…",
"ip": "203.0.113.4", "userAgent": "…",
"result": "ok", "at": "2026-04-15T10:24:55.000Z"
}
Shipped to SigNoz + archived to S3 with object-lock (WORM) for 7 years.
11. CSP / HTTP Headers
HTTP API is JSON-only; no HTML surface, but we still set:
Strict-Transport-Security: max-age=31536000; includeSubDomains; preloadX-Content-Type-Options: nosniffX-Frame-Options: DENYReferrer-Policy: no-referrer
12. Dependency Security
- SBOM generated on every build (CycloneDX).
pnpm audit+ Snyk gating.- Weekly
npm outdatedreport. - Critical/High CVE SLAs: patch within 7 days (High) / 24 h (Critical).
13. Secrets & Keys
- DB creds: rotated quarterly via Secrets Manager.
- JWT JWKS: fetched from identity-service, cached 5 min.
- NATS creds: per-service nkey, rotated quarterly.
- AI Gateway credentials: internal service JWT only; Gateway owns provider secrets.
14. Abuse Prevention
- Materializer explosion guard: reject assignment with estimated windows > 10 million at activation; require explicit override from
tenant_admin. - Escalation storm guard: EscalationRunner rate-limited to 500 actions / minute / tenant.
- Reminder storm guard: ReminderDispatcher batches and de-duplicates.
15. Incident Response
See /runbooks/assignment-service/incident.md (to be authored by ops). High-level:
- Page on-call.
- Freeze materializer via feature flag.
- Preserve forensic snapshot of
outboxandescalation_log. - Rotate affected credentials if compromise suspected.
- Issue postmortem within 5 business days.