Hardening Your Traefik with Security Headers

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 with customFrameOptionsValue helps prevent clickjacking by disallowing your site to be loaded inside iframes from other origins.
  • stsSeconds, stsIncludeSubdomains, and stsPreload 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 the Content-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)
Scanning results from MDN Observatory for "app.symnix.com".

Sources