Skip to main content

Security

:::info Source Sourced from services/delivery-service/SECURITY_MODEL.md in the documentation repo. :::

Companion: 13 Security, Compliance & Tenancy · DOMAIN_MODEL · AI_INTEGRATION

1. Threat Model (Service-Specific)

ThreatSurfaceMitigation
Unauthorized session accessREST APIJWT verification, ownership check on every session operation
Cross-tenant session leakREST API, event consumersRLS on all tables, tenant check in use cases, two-tenant simulator in CI
Offline bundle tamperDevice-side storageJWS license envelope verification, AES-GCM content encryption, checksum validation on mount
License replayLicense envelopeEnvelope includes device-bound nonce + expiry; one-time-use licenses for individual grants
Tutor prompt injectionTutor turn endpointai-gateway prompt-injection classifier; scoped system prompt
Tutor PII exfiltrationTutor turn endpointPII redaction before cloud inference; no-train flag; local inference preferred for sensitive tenants
AI curriculum driftTutor responsesOutput classifier flags off-topic; rate limit on per-session turns
Session hijack via stolen tokenREST APIShort JWT TTL (15 min); device binding; IP anomaly detection
DoS via navigation spamPATCH /navigate120 requests/minute/session rate limit; optimistic concurrency prevents queue buildup
SSE connection exhaustionTutor streamConnection limits per user (5 concurrent); auto-close on inactivity
Bundle download replaymount-offline endpointIdempotency-Key required; mount is unique per (device, bundle)

2. Authentication

All endpoints require a valid JWT issued by identity-service:

  • Header: Authorization: Bearer <jwt>
  • Algorithm: EdDSA Ed25519
  • Expiry: 15 minutes (refresh handled by client)
  • Required claims: sub (userId), tid (tenantId), did (deviceId), scope

Public endpoints: none. All delivery endpoints require authentication.

3. Authorization

3.1 Required Permissions

EndpointPermission
POST /play-sessionsdelivery.play_session:create
PATCH /{id}/navigatedelivery.play_session:navigate (owner only via ABAC)
POST /{id}/pausedelivery.play_session:manage (owner only)
POST /{id}/completedelivery.play_session:manage (owner only)
POST /{id}/abandondelivery.play_session:manage (owner only)
GET /{id}/statedelivery.play_session:read (owner or instructor)
POST /{id}/tutor/turndelivery.tutor:use (owner only)
POST /{id}/mount-offlinedelivery.offline:mount (owner only)
POST /{id}/unmount-offlinedelivery.offline:unmount (owner only)

3.2 ABAC Predicates

resource.tenant_id == ctx.tenant_id
resource.user_id == ctx.user.id // owner-only ops
resource.enrollment.user_id == ctx.user.id // for navigate/complete

Instructors with instructor.sessions:read scope may view sessions for learners in their assignment groups — enforced by additional predicate:

ctx.user.role in [instructor, org_manager]
AND resource.enrollment.assignment.owner_id == ctx.user.id

4. Multi-Tenant Isolation

Every table has tenant_id and RLS policy. The application layer also:

  1. Sets app.tenant_id from JWT on every request (via PgBouncer hook).
  2. Rejects any request where X-Tenant-Id header does not match JWT tid.
  3. Validates aggregate tenant membership at construction (domain invariant).
  4. Runs a "two-tenant simulator" integration test in CI that verifies all endpoints refuse cross-tenant access.

5. Offline Bundle Security

5.1 License Envelope

License Envelope = JWS(signed by tenant key, content = {
tenantId, userId, deviceId, bundleId, courseVersionId,
issuedAt, expiresAt, nonce
})

On mount, delivery-service:

  1. Verifies JWS signature against tenant public key.
  2. Confirms deviceId in envelope matches JWT did.
  3. Confirms expiresAt > now().
  4. Confirms tenantId, userId match JWT.
  5. Confirms bundleId corresponds to a known bundle.
  6. Validates bundle checksum against content-service's published checksum.

5.2 Bundle Encryption

Bundle content is encrypted with AES-GCM using a key derived from (tenantKey, devicePubKey, bundleId). Delivery does not handle decryption; the client runtime does. Delivery only verifies integrity.

5.3 Tamper Detection

If a client reports tampered content (via content.bundle.tamper_detected.v1), delivery-service:

  1. Force-unmounts all affected OfflineMounts.
  2. Pauses any active sessions using those mounts.
  3. Forwards tamper report to content-service for broader investigation.
  4. Alerts security team (PagerDuty P2).

6. Input Validation

All request bodies validated via class-validator (NestJS DTOs):

FieldValidation
enrollmentIdULID format, exists, user-owned
courseVersionIdULID format, known version
deviceIdULID format, matches JWT did
cursor.moduleIdExists in manifest
cursor.lessonIdExists in manifest, part of moduleId
prompt (tutor)Max 2000 chars, non-empty, UTF-8 valid
licenseEnvelopeValid JWS format, signature verifies
bundleChecksumSHA-256 format

7. Rate Limiting

Per API_CONTRACTS §4. Implementation uses Redis counters with token-bucket algorithm:

Key: delivery:ratelimit:{endpoint}:{scope}
Value: { tokens: int, lastRefill: timestamp }
TTL: 2x window

Rate-limit exceeded returns 429 Too Many Requests with Retry-After header.

8. Secrets Management

SecretStorageRotation
JWT verification key (JWKS)KMS via identity-service JWKS endpointAuto-rotated by identity
Database passwordAWS Secrets Manager / Vault90 days
Redis passwordAWS Secrets Manager / Vault90 days
NATS credentialsAWS Secrets Manager / Vault90 days
AI Gateway tokenAWS Secrets Manager / Vault30 days
Tenant signing keys (for envelope)KMS, per-tenant180 days

All secrets loaded at startup; never logged, never returned in API responses.

9. Audit Logging

The following operations produce audit events sent to the audit sink:

OperationEvent
Session startaudit.delivery.session_started
Offline mountaudit.delivery.offline_mounted (includes license hash)
Offline unmount with tamperaudit.delivery.tamper_response
Cross-tenant attemptaudit.security.cross_tenant_rejected
Unauthorized accessaudit.security.unauthorized_access

Audit events include actor.id, tenant_id, trace_id, source IP, user agent, and action-specific metadata.

10. Compliance

RegulationDelivery-Specific Requirements
GDPRParticipate in subject deletion via gdpr.subject_request.received.v1; delete sessions and tutor turns for subject
COPPAVerify tenant COPPA flag before tutor turns for minors; redact names in prompts
FERPAEducational institution tenants; strict audit logging for session access by non-owners
SOC 2Audit trail for all session lifecycle operations
HIPAANot applicable (no PHI in delivery context)

11. Security Testing

TestFrequency
SAST (Semgrep)Every PR
DAST (OWASP ZAP)Nightly against staging
Dependency scan (Snyk / npm audit)Every PR + daily
Pen testAnnual + after major releases
Tenant isolation testEvery PR (CI)
Offline bundle tamper testWeekly
JWT fuzzingMonthly
License envelope fuzzingMonthly

12. Incident Response

SeverityConditionResponse
P1Cross-tenant data leak confirmedImmediate service halt, legal/compliance notification, incident bridge
P2Tamper detection spikeInvestigate bundle, rotate tenant keys, alert content team
P3AI tutor prompt injection succeedingUpdate classifier, review affected turns, notify ai-gateway team
P3Rate-limit bypassPatch, deploy, monitor

All P1/P2 incidents require post-mortem with action items tracked in platform backlog.