feat(api): emit security response headers on every response (PMS-388) #367

Merged
nrupard merged 4 commits from feat/PMS-388-app-level-security-headers into main 2026-06-29 21:31:53 +02:00
Member

Three of the four browser-facing surfaces shipped with no security response headers; only bunyip-api set a full set. The 2026-06-17 standup rejected the shared Traefik headers-security@docker middleware (broad blast radius: an attempt broke Mokosh and knocked out Bunyip until manual recovery) and moved the fix to the application layer.

This is the mokosh-server half: a global security_headers middleware (parity with bunyip-api's .wrap(SecurityHeaders)) layered on the outermost router so it covers the API, the portal API, and the not_a_frontend fallback uniformly. It sets HSTS (max-age=31536000; includeSubDomains; preload), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a deny-by-default Permissions-Policy, and a CSP tuned for this surface (default-src 'none' with style-src 'unsafe-inline' so the inline-styled fallback page renders, plus frame-ancestors 'none', base-uri 'none', form-action 'none').

The module is gated behind the server feature since it depends on axum. Unit tests assert the full header set and the HSTS/frame-deny/nosniff values are present on a response.

#PMS-388

Three of the four browser-facing surfaces shipped with no security response headers; only bunyip-api set a full set. The 2026-06-17 standup rejected the shared Traefik `headers-security@docker` middleware (broad blast radius: an attempt broke Mokosh and knocked out Bunyip until manual recovery) and moved the fix to the application layer. This is the mokosh-server half: a global `security_headers` middleware (parity with bunyip-api's `.wrap(SecurityHeaders)`) layered on the outermost router so it covers the API, the portal API, and the `not_a_frontend` fallback uniformly. It sets HSTS (max-age=31536000; includeSubDomains; preload), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a deny-by-default Permissions-Policy, and a CSP tuned for this surface (default-src 'none' with style-src 'unsafe-inline' so the inline-styled fallback page renders, plus frame-ancestors 'none', base-uri 'none', form-action 'none'). The module is gated behind the `server` feature since it depends on axum. Unit tests assert the full header set and the HSTS/frame-deny/nosniff values are present on a response. #PMS-388
feat(api): emit security response headers on every response (PMS-388)
Some checks failed
E2E / Playwright against staging (pull_request) Successful in 35s
Integration / integration tests (pull_request) Failing after 1m18s
Check / fmt + clippy + build + tests (pull_request) Successful in 1m26s
dc6a8baf1d
Three of the four browser-facing surfaces shipped with no security response headers; only bunyip-api set a full set. The 2026-06-17 standup rejected the shared Traefik `headers-security@docker` middleware (broad blast radius: an attempt broke Mokosh and knocked out Bunyip until manual recovery) and moved the fix to the application layer.

This is the mokosh-server half: a global `security_headers` middleware (parity with bunyip-api's `.wrap(SecurityHeaders)`) layered on the outermost router so it covers the API, the portal API, and the `not_a_frontend` fallback uniformly. It sets HSTS (max-age=31536000; includeSubDomains; preload), X-Content-Type-Options: nosniff, X-Frame-Options: DENY, Referrer-Policy: no-referrer, a deny-by-default Permissions-Policy, and a CSP tuned for this surface (default-src 'none' with style-src 'unsafe-inline' so the inline-styled fallback page renders, plus frame-ancestors 'none', base-uri 'none', form-action 'none').

The module is gated behind the `server` feature since it depends on axum. Unit tests assert the full header set and the HSTS/frame-deny/nosniff values are present on a response.

#PMS-388
Merge remote-tracking branch 'origin/main' into feat/PMS-388-app-level-security-headers
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Successful in 1m46s
Integration / integration tests (pull_request) Successful in 18m37s
E2E / Playwright against staging (pull_request) Failing after 2m21s
b99cb2073f
# Conflicts:
#	src/utils/mod.rs
nrupard scheduled this pull request to auto merge when all checks succeed 2026-06-29 21:19:03 +02:00
Merge branch 'main' into feat/PMS-388-app-level-security-headers
Some checks failed
Check / fmt + clippy + build + tests (pull_request) Has been cancelled
Integration / integration tests (pull_request) Has been cancelled
E2E / Playwright against staging (pull_request) Failing after 2m35s
7fe6aeacc2
Merge branch 'main' into feat/PMS-388-app-level-security-headers
All checks were successful
Check / fmt + clippy + build + tests (pull_request) Successful in 4m40s
E2E / Playwright against staging (pull_request) Successful in 1m1s
Create release / Gate (release-branch merges only) (pull_request) Successful in 4s
Create release / Create release from merged PR (pull_request) Has been skipped
Integration / integration tests (pull_request) Successful in 15m41s
39395ed3a5
nrupard deleted branch feat/PMS-388-app-level-security-headers 2026-06-29 21:31:53 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
3 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/mokosh-server!367
No description provided.