For quite some time now, I’ve been using Traefik as the reverse proxy and ingress controller in several of my personal and client projects. Its clean configuration model, native support for Docker and Kubernetes, and automatic TLS management make it a no-brainer for me. But as my services grew more public, I wanted to make sure to follow basic security good practices — especially when it comes to HTTP security headers.
Headers can protect against XSS attacks, clickjacking, content sniffing, and more. While Traefik already provides good support for these headers through its middleware system, I noticed that a lot of people either don’t enable them or leave them misconfigured. So I decided to dive in, tweak my setup, and see how far I could push the security grade.
Spoiler: After a few changes, I was able to bump several of my domains up to an A+ rating, using a quick scanning tool like https://developer.mozilla.org/en-US/observatory/.
Middleware for Security Headers
Traefik allows you to define reusable middleware in a static or dynamic configuration file. I defined the secHeaders
middleware in Traefik’s dynamic configuration, using a separate YAML file (e.g., middlewares.yml
) placed in a directory that Traefik watches for dynamic configuration. To enable this, make sure your static configuration file contains something like:
providers:
file:
directory: /etc/traefik/dynamic
watch: true
Adding support for our dynamic config files.
This tells Traefik to load any valid config files from that path and apply them live. Here’s how the middleware looks inside such a dynamic file:
http:
middlewares:
secHeaders:
headers:
browserXssFilter: true
contentTypeNosniff: true
frameDeny: true
customFrameOptionsValue: "SAMEORIGIN"
stsIncludeSubdomains: true
stsPreload: true
stsSeconds: 31536000
contentSecurityPolicy: >-
default-src 'self';
script-src 'self' https://stats.example.com
connect-src 'self' https://stats.example.com
img-src 'self' data: https://stats.example.com
style-src 'self' 'unsafe-inline';
object-src 'none';
base-uri 'none';
frame-ancestors 'none';
customResponseHeaders:
server: ""
x-powered-by: ""
Defining a middleware with our added security headers.
Let’s break down what this does:
browserXssFilter
enables basic protection against reflected XSS.contentTypeNosniff
tells browsers not to try to guess the content type.frameDeny
along withcustomFrameOptionsValue
helps prevent clickjacking by disallowing your site to be loaded inside iframes from other origins.stsSeconds
,stsIncludeSubdomains
, andstsPreload
enforce strict HTTPS for an entire year across your domain and its subdomains, which is also necessary if you want to be included in browser preload lists.contentSecurityPolicy
allows us to set theContent-Security-Policy
header value to a custom value.customResponseHeaders
strips out unnecessary headers that may leak information (Server
,X-Powered-By
)
Note: Depending on what you’re building, you might need to fine-tune your Content Security Policy, especially if you’re embedding third-party scripts or using CDNs. For example, the above CSP allows scripts and network connections from stats.example.com
and images from api.examplecdn.com
. You’ll want to update these to reflect the actual domains your application needs.
Connecting Middleware to Your Services
Once the middleware is defined, you can hook it into any of your applications using Traefik labels. For example, if you’re deploying a Docker service, you might add something like this to your container labels:
- "traefik.http.routers.backend.middlewares=secHeaders@file"
Using our defined secHeaders from our dynamic configuration.
This attaches the secHeaders
middleware to the router named backend
, and the @file
part tells Traefik to look for the middleware definition in your dynamic configuration files.
That’s it. Every request hitting that service now gets the full suite of security headers injected into the response.
Testing and Results
After applying the changes and deploying, I used https://developer.mozilla.org/en-US/observatory/ to test one of my domains.
Before:
- Missing or misconfigured headers
- “D” or “F” grade on security scans
After:
- All major security headers present
- Clean CSP with a minimized attack surface
- A+ grade (on the domains where I tried this so far)

Sources
- Traefik HTTP Headers - https://doc.traefik.io/traefik/middlewares/http/headers/
- MDN HTTP Observatory - https://developer.mozilla.org/en-US/observatory
- Content Security Policy - https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CSP
- HSTS Preload Requirements - https://hstspreload.org/