Skip to main content

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)

ThreatAssetControl
Spoofing (cross-tenant request)Any assignment/windowJWT tenant claim + X-Tenant-Id must match + RLS current_setting('ghasi.tenant_id')
Tampering (window state bypass)compliance_windowWrites only via service code; RLS+ORM enforce invariants; CHECK constraints on state
Repudiation (who activated?)Assignment auditImmutable activated_at, created_by; escalation_log retained 7y
Information disclosure (list all tenants' compliance)compliance-reportRLS, role check, SQL function denies cross-tenant input
Denial of serviceMaterializerRate-limited, horizon-capped, advisory locks prevent runaway
Elevation of privilegeRole=learner writing assignmentsFine-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+):

RoleScopes
learnerassignments:read:self
managerassignments:read:ou, windows:read:ou
compliance_adminassignments:write, windows:read, report:read, ai.suggest:call
tenant_adminsuperset of compliance_admin
auditorassignments:read, windows:read, report:read (read-only)
supporttime-bounded impersonation with support_ticket_id; logs tagged support-mode=true

4. Tenancy Isolation

Enforced in three layers:

  1. Gateway: rejects requests whose JWT tenant_idX-Tenant-Id.
  2. Service code: every repository call includes tenantId explicitly; handlers type-check against the JWT claim.
  3. Database RLS: every table has RLS policy using current_setting('ghasi.tenant_id'). The service sets this on each connection via SET LOCAL at the start of every request's tx.

No bypass. Even super-admin ops go through explicit tenant-selection UX.

5. Data Classification

DataClassNotes
assignment.* metadataInternalNot personally sensitive unless titles contain PII
compliance_window.user_idPII-linkedSubject to GDPR/HIPAA right to erasure
compliance_window.state + timestampsRegulatory evidenceRetained 7y for audit
escalation_logInternalRetained 7y
ai_provenanceInternal / observabilityMay 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:

KindAction
exportEmit assignment.gdpr.export.produced.v1 with tar.gz index of all windows for subject
erasureFor 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.
restrictionSet 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.js in strict mode; reject non-RFC-5545, reject UNTIL beyond 10 years, reject COUNT > 200, reject INTERVAL > 50.
  • ISODuration: parse + cap (dueOffset ≤ P365D, gracePeriod ≤ P30D).
  • Targets: user IDs 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; preload
  • X-Content-Type-Options: nosniff
  • X-Frame-Options: DENY
  • Referrer-Policy: no-referrer

12. Dependency Security

  • SBOM generated on every build (CycloneDX).
  • pnpm audit + Snyk gating.
  • Weekly npm outdated report.
  • 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:

  1. Page on-call.
  2. Freeze materializer via feature flag.
  3. Preserve forensic snapshot of outbox and escalation_log.
  4. Rotate affected credentials if compromise suspected.
  5. Issue postmortem within 5 business days.