Security
:::info Source
Sourced from services/authoring-service/09-SECURITY_MODEL.md in the documentation repo.
:::
Companion: 13 Security & Compliance · 04 API Contracts
1. Threat Model
| Threat | Surface | Mitigation |
|---|---|---|
| Cross-tenant draft leak | REST API, events, sync | Multi-layer tenant isolation (§3) |
| Prompt injection via block content | AI generate flows | Pre-moderation + input wrapping at ai-gateway |
| AI PII exfiltration | AI flows | PII redaction before gateway + provider no-train flag |
| SCORM zip RCE | SCORM import | Sandboxed extraction + manifest validator + signed origin allowlist |
| Unauthorized publish | Publish endpoint | Explicit publish permission + approver != author invariant |
| Malicious embed block | Embed block runtime | Whitelisted providers + iframe sandbox attributes |
| Session hijack | All endpoints | Short JWT TTL + rotating refresh + device binding |
| Outbox poisoning | Event pipeline | Schema validation + signed envelopes + replay idempotency |
| Yjs update corruption | Collab WS | Signed update vectors + server-side validation |
| Insider draft exfiltration | All reads | Per-request audit log + compliance officer read trail |
| Draft enumeration | GET /drafts | ULIDs (unpredictable) + tenant filter + authz on each row |
| IDOR on blocks | PATCH/DELETE block | ABAC policy: block.tenantId == ctx.tenantId AND user in draft collaborators |
| Mass data scraping | List endpoints | Rate limits + pagination caps + anomaly detection |
2. Authentication
- Inbound clients: JWT Bearer (Ed25519 signed, KMS-backed
kid) - Service-to-service: mTLS + internal service tokens
- WebSocket collab: JWT in subprotocol header (not query string to avoid log leak)
2.1 JWT Validation
Every request validates:
- Signature via JWKS from identity-service
expnot in the pastiss == https://auth.ghasi.ioaudincludesauthoring-servicekidmatches a non-revoked keytid(tenant) matchesX-Tenant-Idheaderdpopproof if present (M6+)
Failure → 401 with WWW-Authenticate header.
3. Authorization
3.1 Roles (coarse)
| Role | Permissions |
|---|---|
platform_admin | Read all drafts (audit); no writes |
compliance_officer | Read all drafts for audit; redaction controls |
org_owner, org_admin | Manage provider settings (in provider tenants) |
provider_admin | Create/delete drafts, add collaborators, configure prompts |
author | Create/edit/submit drafts; use AI co-author |
reviewer | Read drafts in in_review; approve/reject |
publisher | Publish approved drafts (M3+; before that, author+reviewer combined) |
learner, individual | No access |
3.2 ABAC Predicates
| Resource:Action | Predicate |
|---|---|
draft:create | user.role IN {author, provider_admin} AND user.tenantId == resource.tenantId |
draft:read | resource.tenantId == user.tenantId AND (user IN resource.collaborators OR user.role IN {provider_admin, compliance_officer}) |
draft:update | resource.tenantId == user.tenantId AND user IN resource.collaborators AND resource.state == 'editing' |
draft:submit_review | resource.createdBy == user.id OR user IN resource.collaborators with role=editor |
draft:approve | user.role IN {reviewer, provider_admin} AND user != resource.lastSubmittedBy |
draft:publish | user.role IN {publisher, provider_admin} AND resource.state == 'approved' |
draft:delete | user == resource.createdBy OR user.role == provider_admin AND resource.state == 'editing' |
block:create | Same as draft:update |
ai:generate | Same as draft:update + tenant AI budget check |
collab:join | Same as draft:read |
scorm:import | Same as draft:update |
3.3 Decision Flow
Request arrives
↓
[AuthN Guard] validates JWT → RequestContext populated
↓
[Controller] declares required permission (decorator: @RequireAuth('draft:update'))
↓
[PolicyEngine] evaluates:
1. Does role grant permission? (coarse)
2. Do ABAC predicates pass? (fine)
3. Is tenant scoped? (rls check)
↓
[Log] decisionId recorded; linked to audit chain
↓
Controller handler executes
3.4 UI Hint Endpoint
POST /api/v1/authz/check
{ "checks": [{"resource":"draft:publish","resourceId":"drf_01H..."}] }
=> { "results": [{"resource":"draft:publish","allowed":false,"reason":"state_not_approved"}] }
4. Multi-Tenant Isolation
| Layer | Mechanism |
|---|---|
| Edge (CDN) | Per-tenant path prefixes or domain |
| API Gateway | X-Tenant-Id header must equal JWT tid; otherwise reject |
| Controller | Use cases take RequestContext with tenantId; never accept from body |
| Domain | Aggregate construction validates tenant consistency (INV-1) |
| Postgres RLS | Every authoring table has rls_*_tenant policy |
| Postgres session | PgBouncer proxy-init sets app.tenant_id per request |
| Events (NATS) | tenantId in envelope; consumer filters on it |
| Cache (Redis) | Key prefix tenants/{tid}/authoring/... |
| S3 | SCORM uploads under tenants/{tid}/scorm/...; signed URLs per-caller |
| Search | Tenant filter on every OpenSearch query |
| Vector index | Tenant filter on every pgvector k-NN |
| Logs | tenant_id tag on every structured log line |
| Metrics | tenant_id label on every metric |
4.1 Tenant Leak Prevention Tests
Every CI build runs the two-tenant simulator suite:
- Provision two tenants (A, B) with distinct users
- For each endpoint and event topic, attempt cross-access
- Assert 100% refusal with correct error codes
- Assert no cross-tenant data in logs, metrics, caches
5. Input Validation
5.1 Zod Schemas on Every Endpoint
All request bodies validated via Zod. Schema violations return 422 with field-level errors.
5.2 Block Content Validation
- Markdown: sanitized via rehype-sanitize with strict whitelist
- Image alt: length 1-500 chars, no HTML
- Embed URLs: whitelist of providers (YouTube, Vimeo, Loom, CodePen, specific tenant-approved)
- Embed sandbox attributes: non-empty whitelist (
allow-scripts allow-same-originby default; loosened only byprovider_admin) - Interaction config: JSON Schema validation per interaction type
5.3 SCORM Import Validation
- Max zip size: 500 MB
- Extract to ephemeral sandbox (separate container, no network)
- Validate
imsmanifest.xmlagainst SCORM 1.2/2004 XSD - Reject manifests with external entity references (XXE prevention)
- Scan for zip-slip attacks (path traversal in entry names)
- Scan for zip bombs (entry size ratio check)
- Remove any
<script>tags from embedded HTML (scripts are not supported in SCORM import v1)
6. Output Encoding
- JSON responses: explicit
charset=utf-8andX-Content-Type-Options: nosniff - HTML in previews: served from sandboxed subdomain (
preview-{draftId}.ghasi.io) - Runtime player: CSP with nonce,
frame-ancestors 'self' - Markdown → HTML rendering: DOMPurify in the player
7. Secret Management
- No secrets in code; all via env vars loaded from Vault/KMS at boot
- AI gateway tokens: rotated weekly, stored in Secret Manager
- Database passwords: rotated monthly via automated Vault policy
- Signing keys: KMS-backed; never present on pod filesystem
Required env vars:
DATABASE_URL postgres://authoring:***@pg:5432/authoring
NATS_URL nats://nats:4222
NATS_CREDS_PATH /vault/secrets/nats.creds
AI_GATEWAY_URL https://ai-gateway.internal
AI_GATEWAY_TOKEN (injected from Vault)
IDENTITY_JWKS_URL https://auth.ghasi.io/.well-known/jwks.json
OTEL_EXPORTER_OTLP_ENDPOINT http://otel-collector:4318
S3_SCORM_BUCKET ghasi-authoring-scorm-{region}
REDIS_URL redis://redis:6379
8. CSRF & CORS
- Cookie-based session not used; JWT Bearer in
Authorizationheader - CORS: allowlist of tenant domains (
https://{tenant-slug}.ghasi.io, plus trusted first-party) Access-Control-Allow-Credentials: false(no cookies)- Preflight cache 600s
9. Rate Limiting
(See 04-API_CONTRACTS.md §4.) Enforced at API Gateway using Redis token bucket. Service-side secondary enforcement for AI endpoints.
10. WebSocket Security
- JWT validated on upgrade; rejected if expired mid-session (re-auth required)
- Per-connection rate limits: 100 messages/sec
- Server-side Yjs update validation: every incoming update parsed and re-serialized
- Large update vectors (> 1 MB) rejected
- Idle timeout: 5 minutes
11. SSE Security
- Each SSE connection tied to the JWT; revoked refresh → connection closed
- Heartbeat every 15s (server → client)
- Client disconnection detected → server-side job continues; results attached on reconnect
12. Audit Logging
Every mutation writes an audit entry via AuditLogger (port → audit-service, async via event). Fields:
tenantId,userId,decisionIdaction,resourceType,resourceIdtimestamp,ip,userAgentdelta: before/after hash
Retention: 7 years (regulated class).
13. Headers (Response)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Content-Security-Policy: default-src 'none'; script-src 'self' 'nonce-{R}'; connect-src 'self' https://*.ghasi.io; img-src 'self' data: https:; style-src 'self' 'unsafe-inline'; frame-ancestors 'none';
Permissions-Policy: camera=(), microphone=(), geolocation=()
14. Dependency Security
- Lockfiles committed (
pnpm-lock.yaml) - Snyk /
npm auditin CI; blocks on HIGH/CRITICAL - SBOM generated on every release (SPDX format)
- Provenance attestations (SLSA level 3)
- Distroless base image for runtime
- No
curl | bashanywhere; all tool installs pinned
15. Compliance Hooks
| Regulation | Hook |
|---|---|
| GDPR Art. 17 (Erasure) | gdpr.subject_request.received.v1 handler anonymizes/deletes user references in drafts |
| GDPR Art. 15 (Access) | /internal/compliance/user-data?userId={id} returns all draft-related data for a user |
| GDPR Art. 20 (Portability) | Same endpoint with ?format=json exports user's authored drafts |
| SOC 2 CC6.1 | Access controls audited via decisionId chain |
| FERPA (if applicable) | Student data never stored in authoring — pure authoring context |
| COPPA (if applicable) | Tenant config flag prevents AI flows for tenants serving under-13 audiences |
16. Incident Response
| Event | Automated Response | Human Response |
|---|---|---|
| DLQ non-empty > 5 min | PagerDuty alert | On-call runbook |
| Cross-tenant attempt detected | Block request, alert SecOps | SecOps investigation within 15 min |
| Publish saga timeout rate > 5% | Alert platform team | Investigate downstream services |
| AI moderation block rate > 20% | Alert content-safety team | Review prompts and inputs |
| Unauthorized admin access attempt | Block + alert | SecOps runbook |
17. Data Classification
| Data | Classification | Handling |
|---|---|---|
| Draft titles, content | Business sensitive | Encrypted at rest (Postgres TDE); RLS enforced |
| AI provenance | Audit-relevant | Retained 7 years; regulated class |
| Collaborator lists | Internal | Standard |
| Uploaded media refs | Business sensitive | Signed URLs only |
| SCORM source zips | Business sensitive | Per-tenant S3 prefix; encrypted |
| Block versions | Audit-relevant | Retained 2 years, then archived |