Files
magnus919_agent-skills/traefik/references/csp-spa-debugging.md
T

133 lines
5.8 KiB
Markdown

# 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](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/headers/)
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](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/headers/)
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:
```yaml
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](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/headers/) — header overwrite behavior, CORS preflight interception
- [Traefik Middleware overview](https://doc.traefik.io/traefik/reference/routing-configuration/http/middlewares/overview/) — router vs. service middleware execution order
- [Traefik Entrypoints](https://doc.traefik.io/traefik/reference/install-configuration/entrypoints/) — entrypoint-level `http.middlewares` configuration
- [MDN Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) — `default-src`, `connect-src`, browser enforcement
- [MDN CORS](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) — preflight request mechanics
- [unrolled/secure](https://github.com/unrolled/secure#available-options) — the library Traefik uses for security headers