Traefik security headers: CSP, HSTS, and the full middleware stack
Published: 2026-03-02
Security headers are the cheapest security improvement you can make to a web application: one Traefik Middleware resource, applied to every IngressRoute, and you're done. No app changes, no redeploy. Here's the full set we use and why each header is there.
The Middleware
yamlapiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
namespace: default
spec:
headers:
# Force HTTPS for 1 year, include subdomains, allow preloading
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
# Don't send referrer to cross-origin destinations
referrerPolicy: "strict-origin-when-cross-origin"
# Prevent MIME-type sniffing
contentTypeNosniff: true
# Block clickjacking
frameDeny: true
# Disable browser features we don't use
permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=()"
# Remove server fingerprinting header
customResponseHeaders:
X-Powered-By: ""
Server: ""
# Content Security Policy
contentSecurityPolicy: >-
default-src 'self';
script-src 'self' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self';
frame-ancestors 'none';
base-uri 'self';
form-action 'self'
Applying the middleware to IngressRoutes
Reference it in every IngressRoute:
yamlapiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: my-app-https
spec:
entryPoints:
- websecure
routes:
- match: Host(`app.example.com`)
kind: Rule
middlewares:
- name: redirect-https
- name: security-headers
services:
- name: my-app
port: 80
tls:
secretName: my-app-tls
Middleware order matters: redirect-https runs first to ensure the connection is HTTPS, then security headers are set on the response.
Header-by-header explanation
Strict-Transport-Security (HSTS)
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
Tells browsers: for the next year, connect to this domain and all subdomains only over HTTPS. Even if the user types http://, the browser upgrades it locally without a round trip. preload allows submission to the HSTS preload list — browsers ship with these domains baked in.
Don't add preload until you're sure every subdomain serves HTTPS. It's very hard to undo once submitted to the preload list.
X-Frame-Options / frame-ancestors
frameDeny: true sets X-Frame-Options: DENY. The CSP frame-ancestors 'none' is the modern equivalent — both are set for maximum compatibility.
These prevent your site from being embedded in an iframe on another domain (clickjacking attacks).
Content-Type-Options
X-Content-Type-Options: nosniff
Prevents the browser from guessing the MIME type of a response. Without it, a browser might execute JavaScript served with Content-Type: text/plain if it "looks like" JavaScript. Stops MIME-confusion attacks.
Referrer-Policy
Referrer-Policy: strict-origin-when-cross-origin
Sends the full URL as a referrer to same-origin requests (useful for analytics), but only the origin (no path or query string) to cross-origin requests. Prevents leaking paths like /reset-password?token=abc123 to third-party scripts.
Permissions-Policy
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Explicitly disables browser APIs that the app doesn't use. A compromised third-party script can't request camera access even with user interaction — the permission is blocked at the HTTP header level.
Remove Server headers
yamlcustomResponseHeaders:
X-Powered-By: ""
Server: ""
Setting to empty string removes the headers. Prevents fingerprinting — an attacker seeing Server: uvicorn/0.30.1 knows exactly which framework to look for CVEs in.
Content Security Policy
CSP is the most powerful but also the most dangerous header — set it wrong and you break your app. Start with a permissive policy and tighten:
Permissive (start here):
default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline';
Tighter (after eliminating inline scripts):
default-src 'self'; script-src 'self'; style-src 'self';
With nonces (ideal for SPAs):
script-src 'nonce-{random_per_request}';
For a mostly-static site with no third-party scripts, a simple policy is reasonable. If you use Google Analytics, fonts, or Stripe — add their origins explicitly:
script-src 'self' https://www.googletagmanager.com;
style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;
font-src 'self' https://fonts.gstatic.com;
connect-src 'self' https://analytics.google.com;
Use Content-Security-Policy-Report-Only with a report-uri to discover violations before switching to enforce mode.
HTTPS redirect middleware
The redirect-to-HTTPS middleware completes the stack:
yamlapiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: redirect-https
namespace: default
spec:
redirectScheme:
scheme: https
permanent: true
Testing
bash# Check headers manually
curl -sI https://example.com | grep -iE "strict|frame|content-type|x-powered|csp|referrer|permissions"
# Expected output:
# strict-transport-security: max-age=31536000; includeSubDomains; preload
# x-content-type-options: nosniff
# x-frame-options: DENY
# referrer-policy: strict-origin-when-cross-origin
Online validators:
- https://securityheaders.com — grades your headers A through F
- https://observatory.mozilla.org — broader HTTP security check
A perfect score on securityheaders.com takes about 30 minutes the first time. Worth it.