5.8 KiB
CSP / SPA Debugging — Entrypoint Header Overwrite
Debugging guide for the failure mode where an entrypoint-level headers middleware
silently breaks cross-origin SPAs by overwriting router-level
Content-Security-Policy headers.
The Mechanism
Traefik middleware execution order on the response path is the reverse of the request path:
Request: entrypoint middlewares → router middlewares → service middlewares → backend
Response: backend → service middlewares → router middlewares → entrypoint middlewares
Entrypoint-level middlewares run last on the response path. The headers
middleware documentation states:
"Custom headers will overwrite existing headers if they have identical names." — Traefik Headers middleware docs
Therefore, when an entrypoint applies a chain containing contentSecurityPolicy
(e.g., a default@file security-headers chain on websecure), it overwrites any
router-level CSP on the response. Router-level CSP overrides are impossible
when the entrypoint also sets CSP.
The Failure
If the entrypoint CSP is generic — typically default-src 'self' with no
connect-src directive — the browser enforces it and blocks every cross-origin
fetch/XHR the SPA makes. The SPA's own page and static assets load normally
(they are same-origin), but all API calls to a different origin silently fail.
The backend logs show zero requests from the SPA. This is the key tell that distinguishes this failure from backend issues, network problems, or CORS misconfiguration on the backend.
Diagnostic Flow
SPA page loads (200) but API calls fail silently
│
├─ 1. Check CSP on the SPA page:
│ curl -D- -o /dev/null https://your-spa.example.com/ | grep -i content-security-policy
│
│ If you see `default-src 'self'` with no `connect-src` → this is the problem.
│ The browser blocks all cross-origin requests.
│
├─ 2. Confirm zero requests reach the backend:
│ Check backend logs for the complete absence of requests from the SPA.
│ (Not 403s, not CORS errors — nothing at all.)
│
├─ 3. Identify the source of the CSP:
│ Check whether the CSP comes from the backend or from Traefik.
│ curl -D- -o /dev/null https://your-backend-api.example.com/ | grep -i content-security-policy
│
│ If the backend emits its own (different) CSP but the SPA page shows a
│ generic one, the entrypoint middleware is overwriting it.
│
└─ 4. Check the entrypoint middleware chain:
Look at the entrypoint's `http.middlewares` list in static config,
then trace the chain to find `contentSecurityPolicy` in a headers middleware.
The Fix
Remove contentSecurityPolicy from the entrypoint-level default middleware chain.
Let each service emit its own tailored CSP. Many applications (GoToSocial,
Mastodon, Nextcloud, etc.) ship their own CSP headers that are specific to their
needs.
A proxy-wide default-src 'self' is actively harmful for any SPA that
communicates with a different origin. The proxy should not impose a CSP that
overrides what the application itself intends.
Other security headers (HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy) are safe to keep in the entrypoint chain — they do not interfere with cross-origin API calls.
CORS Preflight Interception
The headers middleware also intercepts CORS preflight requests when CORS headers
are configured:
"If CORS headers are set, then the middleware does not pass preflight requests to any service, instead the response will be generated and sent back to the client directly." — Traefik Headers middleware docs
This is useful when a backend does not handle OPTIONS preflight requests (returns
405). Adding a headers middleware with accessControlAllowMethods and
accessControlAllowOriginList to the router handles preflights at the proxy level.
Example:
http:
middlewares:
cors-preflight:
headers:
accessControlAllowMethods:
- GET
- POST
- PUT
- DELETE
- PATCH
- OPTIONS
accessControlAllowOriginList:
- "*"
accessControlAllowHeaders:
- "*"
accessControlMaxAge: 120
addVaryHeader: true
Related but Distinct Failures
| Failure | Symptom | Cause |
|---|---|---|
| CSP overwrite (this guide) | SPA loads, zero API requests reach backend | Entrypoint headers middleware overwrites router CSP |
| Rate limiting | SPA loads, 429 errors on asset/API requests | Entrypoint rate limit too low for SPA burst |
| Double middleware execution | Doubled rate-limit counts, duplicate compression | Same middleware declared at both entrypoint and router level |
Sources
- Traefik Headers middleware — header overwrite behavior, CORS preflight interception
- Traefik Middleware overview — router vs. service middleware execution order
- Traefik Entrypoints — entrypoint-level
http.middlewaresconfiguration - MDN Content-Security-Policy —
default-src,connect-src, browser enforcement - MDN CORS — preflight request mechanics
- unrolled/secure — the library Traefik uses for security headers