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.