Skip to content

Wire LMS security headers

Problem

The LMS does not emit standard security headers. apps/lms/next.config.ts has no headers() block, and apps/lms/src/proxy.ts only sets tenant-resolution headers. The admin doc at apps/lms/docs/admin/security-and-rate-limiting.md describes headers as already in place — a documentation-vs-implementation drift.

CMS, by contrast, wires headers globally via apps/cms/next.config.tsgetSecurityHeaders() in apps/cms/src/lib/security/headers.ts.

Proposal

  1. Port getSecurityHeaders() into LMS at apps/lms/src/lib/security/headers.ts (kept in-app until T-006 extracts it into @open-learning-hub/server).
  2. Add a headers() block to apps/lms/next.config.ts that applies the shared set to all routes.
  3. Mirror CMS environment-aware behavior:
    • HSTS only in production.
    • CSP report-only baseline (no enforcement yet).
    • X-Frame-Options: SAMEORIGIN, X-Content-Type-Options: nosniff, Referrer-Policy: strict-origin-when-cross-origin, Permissions-Policy minimal.
  4. Update apps/lms/docs/admin/security-and-rate-limiting.md to point at the new module and accurately describe what is enforced.

Acceptance criteria

  • apps/lms/src/lib/security/headers.ts exists and exports the same header set CMS uses.
  • LMS responses include the headers above (verified by a Vitest snapshot or e2e header assertion).
  • HSTS is gated on process.env.NODE_ENV === 'production'.
  • LMS admin doc reflects the implemented behavior with file links.
  • npm run check passes.

Out of scope

  • CSP enforcement (stays report-only here; tighten in a follow-up).
  • Per-route header overrides.
  • Extracting the helper into @open-learning-hub/server (handled by T-006).

Notes / decisions log

  • 2026-05-24: Ticket created from monorepo audit.
  • 2026-05-24: Implemented via next.config.ts headers() + apps/lms/src/lib/security/headers.ts (verbatim CMS port); Vitest in src/lib/security/__tests__/headers.test.ts; admin doc corrected.