Security
:::info Source
Sourced from services/catalog-service/SECURITY_MODEL.md in the documentation repo.
:::
Companion: ../../docs/13-security-compliance-tenancy.md · DATA_MODEL
1. Threat Model
| Asset | Threat | Mitigation |
|---|---|---|
| Course metadata | Cross-tenant leakage | RLS + visibility filter + X-Tenant-Id enforcement |
| Withdrawal reason | Disclosure of legal/privacy details | Reason stored but returned only to tenant admins |
| Taxonomy tree | Tamper | Optimistic concurrency + audit |
| Admin endpoints | Privilege escalation | Platform-admin role + separate JWT audience |
| Outbox | Replay leakage | Signed envelopes, per-tenant ACL on NATS subjects |
| Change-log (sync) | Cross-tenant drift | Service JWT aud=sync-service, tenant scoping in SQL |
2. Authentication
- Tenant JWT (RS256, issued by identity-service). Required claims:
sub,tid,roles,exp,iat. - Service-to-service: JWT with
aud=catalog-serviceor mTLS (in mesh). - Public API: no auth; tight rate limits; CDN layer.
3. Authorization (RBAC + ABAC)
Roles (platform-wide)
| Role | Scope |
|---|---|
catalog.course.view | Read courses own tenant |
catalog.course.edit | Metadata edits |
catalog.course.visibility | Change visibility |
catalog.course.archive | Archive |
catalog.version.manage | Deprecate / withdraw |
catalog.taxonomy.edit | Edit tenant taxonomies |
catalog.taxonomy.admin | Edit global taxonomies (platform-admin only) |
catalog.admin.replay | DLQ replay |
ABAC rules
- Visibility check:
deny if course.tenantId != caller.tid AND course.visibility NOT IN ('marketplace','public')
- Write operations require
caller.tid == course.tenantId. - Platform-admin role bypasses tenant filter for diagnostic endpoints only.
4. Input Validation
- All inputs schema-validated (Zod) at API boundary.
- String max lengths:
slug100,title.*200,description.*10,000,tag50 (max 20 tags). - JSON depth cap 5; total body size ≤ 256 KB.
- Taxonomy payloads capped at 1000 nodes / 100 KB.
5. Injection Defenses
- SQL: parameterised queries via
pg+ query builder; no string concat. - NoSQL / JSONB: payloads stored verbatim; no expression evaluation.
- Log injection: structured logging;
\n/control chars stripped. - SSRF: no outbound HTTP to arbitrary URLs; media resolution goes through media-service client with allow-list.
6. Secrets Management
- Secrets in AWS Secrets Manager or GCP Secret Manager.
- Pulled at boot via Service Account.
- No secrets in env var logs;
DOTENV_DENYenabled. - Rotation: DB creds quarterly, NATS creds quarterly, signing keys annually.
7. Transport Security
- TLS 1.3 required on all ingress.
- Internal mesh: mTLS (Linkerd / Istio).
- NATS: TLS + NKEY auth per service.
- CDN: HSTS preload, CSP
default-src 'self'.
8. RLS & Session Context
Every DB connection, immediately after checkout:
SET LOCAL app.current_tenant = $1;
SET LOCAL app.actor_id = $2;
SET LOCAL app.trace_id = $3;
Missing app.current_tenant → RLS denies all rows except where visibility is public/marketplace (which explicitly allows cross-tenant reads without tenant context).
Platform-admin context uses SET LOCAL app.is_platform_admin = 'true' and a policy clause OR current_setting('app.is_platform_admin', true) = 'true'.
9. Event Security
- Envelope signed with Ed25519 per-service key; sig stored in
envelope.sig. - Consumers verify signature for regulated events.
- Subject-level NATS ACLs: catalog-service can publish only
catalog.*; consume onlycontent.play_package.built.v1,authoring.course_draft.published.v1,gdpr.*.
10. PII & Data Classification
| Field | Class |
|---|---|
authors[].displayName | Personal (already public) |
authors[].userId | Pseudonymous ID |
withdrawn_reason | Internal |
| all others | Public (once visibility=public) |
No special category data (health, biometric) in catalog.
11. GDPR & Data Subject Requests
- Access: export JSON of all records referencing
userId. - Rectification: update
authors[].displayNameper request. - Erasure: replace
displayNamewith[redacted].userIdretained for audit unless legal basis forces deletion — in which case replaced withusr_00…tombstone. - Portability: not applicable (no user-owned content in catalog).
Handler: HandleGdprSubjectRequest use case; SLA 30 days.
12. Audit Logging
- All write ops append to
course_audit(immutable). - All read ops log traceId + tenantId + actor to central log (not to DB).
catalog.course.visibility_changed.v1+ audit row flaggedaudit=truefor privileged-access review.
13. Rate Limiting & Abuse
- Per-tenant and per-IP limits at API gateway.
- Slowloris and body-size protections at edge.
- Brute-force on
/public/v1/coursesmitigated by CDN and bot detection (Cloudflare / Fastly).
14. Supply Chain
- Dependencies pinned via
package-lock.json, checksummed. - Daily
npm audit+ Snyk; fail build on CVSS ≥ 7.0. - Container base image:
node:20-alpine+ distroless variant in prod. - SBOM generated on every build (CycloneDX).
15. Encryption at Rest
- Postgres: AES-256 at storage layer, KMS CMK per region.
- Redis: encrypted volume (EBS / GCP PD).
- Backups: encrypted with separate KMS key; cross-region replicated.
16. Security SLOs
| Metric | Target |
|---|---|
| Critical CVE patched | ≤ 48 h |
| Secret rotation window | ≤ quarterly |
| Audit log delivery | ≥ 99.99% within 1 h |
| Unauth attempts throttled | 100% (all fail-closed) |
17. Compliance Touchpoints
- SOC 2: change audit, access control, encryption.
- GDPR: DSR handlers.
- FERPA: catalog holds no student PII → minimal exposure.
- ISO 27001: Annex A controls mapped via the global
13-security-compliance-tenancydoc.
18. Security Test Suite
| Test | Frequency |
|---|---|
| RLS bypass attempt (cross-tenant SELECT) | every PR (integration) |
| AuthZ matrix (role × endpoint) | nightly |
| SAST (Semgrep) | every PR |
| DAST (ZAP) | weekly on staging |
| Dependency scan | daily |
| Penetration test | annually |