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.ts → getSecurityHeaders() in apps/cms/src/lib/security/headers.ts.
Proposal¶
- Port
getSecurityHeaders()into LMS atapps/lms/src/lib/security/headers.ts(kept in-app until T-006 extracts it into@open-learning-hub/server). - Add a
headers()block to apps/lms/next.config.ts that applies the shared set to all routes. - 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-Policyminimal.
- 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.tsexists 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 checkpasses.
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.tsheaders()+apps/lms/src/lib/security/headers.ts(verbatim CMS port); Vitest insrc/lib/security/__tests__/headers.test.ts; admin doc corrected.