Enforce Security Headers and Rate Limits with Traefik File Provider
Use Traefik's file provider to define shared Headers and RateLimit middlewares once and apply them to routers for security headers and request limits.
10 Aug 2025, 18:38 UTC

Problem: repeat the same security and rate rules on every router
You run several services behind Traefik and need the same response headers—such as X-Content-Type-Options: nosniff—and the same per-client request ceiling on each one. Writing those rules into container labels for every service is repetitive, and changing them often means redeploying containers or restarting Traefik.
Useful takeaway: define Headers and RateLimit middlewares once in a file-provider dynamic configuration, reference them by name in any router, and let Traefik reload the file without a process restart. This guide assumes Traefik v2 or v3.
How Traefik splits static and dynamic configuration
Traefik separates static configuration—entry points, providers, and the API—from dynamic configuration, which includes routers, middlewares, and services. The file provider watches a YAML or TOML file for dynamic changes. A syntax error can block a reload, but the previous configuration stays active.
A router matches a request using a rule such as Host or PathPrefix, then forwards it to a service. Middlewares listed under the router run in the order they appear, before the request reaches the backend. The Headers middleware can set customResponseHeaders on responses. The RateLimit middleware uses average and burst values and returns 429 Too Many Requests when a client exceeds the limit.
Worked configuration
This example runs Traefik with a static configuration file and a separate dynamic file watched by the file provider.
Static configuration: traefik.yml
entryPoints:
web:
address: ':80'
websecure:
address: ':443'
api:
dashboard: true
providers:
file:
filename: '/etc/traefik/dynamic.yml'
watch: true
Dynamic configuration: /etc/traefik/dynamic.yml
http:
routers:
app-router:
rule: 'Host(`app.example.com`)'
service: app-service
entryPoints:
- websecure
middlewares:
- secure-headers
- ip-rate-limit
middlewares:
secure-headers:
headers:
customResponseHeaders:
X-Content-Type-Options: 'nosniff'
Referrer-Policy: 'no-referrer'
X-Frame-Options: 'DENY'
ip-rate-limit:
rateLimit:
average: 100
burst: 50
sourceCriterion: ClientIP
services:
app-service:
loadBalancer:
servers:
- url: 'http://127.0.0.1:8080'
The middleware chain is secure-headers then ip-rate-limit. Because Traefik unwinds responses through the chain in reverse order, placing Headers first means security headers can still appear on a 429 response. Putting RateLimit first rejects traffic earlier but may omit those headers from the rejection response.
Run and verify
Run Traefik from a user account that can read both configuration files. Binding to ports 80 and 443 requires root or the CAP_NET_BIND_SERVICE capability; otherwise use higher ports such as 8080 and 8443.
# Run from the directory containing traefik.yml traefik --configFile=/etc/traefik/traefik.yml
Expected checks:
- Request a matching path and confirm headers are present:
curl -i 'https://app.example.com/'
Look for X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, and X-Frame-Options: DENY in the response.
- Send repeated requests from one client and expect
429after the burst allowance is exhausted:
for i in $(seq 1 160); do curl -s -o /dev/null -w '%{http_code}\n' 'https://app.example.com/'; done | sort | uniq -c
Expect mostly successful backend responses until the burst is consumed, then 429 responses. Exact counts depend on backend latency and the average refill rate.
- Confirm the file provider detected a change after editing
dynamic.ymlby watching Traefik logs. Look for a successful reload message or a syntax-error message; the previous configuration remains active if the file is invalid.
Limits and common mistakes
- In-memory rate limiting.
RateLimituses an in-memory store by default, so multiple Traefik instances do not share a single counter. Configure a Redis store if you need a shared limit across instances. - Router scope. Header and rate rules apply only to responses passing through the matched router. Direct backend calls and unmatched paths are unaffected.
- Middleware order. Changing the order changes which headers appear on rejected responses and how early traffic is dropped.
- Client IP behind another proxy.
ClientIPsees the immediate connection IP. If Traefik sits behind a load balancer, that may be the load balancer's address rather than the end user's address. - Do not mix Traefik v1 labels. Labels such as
traefik.frontendare incompatible with v2/v3 labels such astraefik.http.routers. - Duplicated rules. Define shared middlewares once in the file provider and reference them from multiple routers instead of repeating header and rate settings per service.
Rollback
Editing dynamic.yml changes Traefik's routing state. Keep a backup copy before editing:
cp /etc/traefik/dynamic.yml /etc/traefik/dynamic.yml.bak
If a reload fails or behavior is wrong, restore the previous file and let the file provider pick it up:
cp /etc/traefik/dynamic.yml.bak /etc/traefik/dynamic.yml
Traefik should apply the restored configuration without a restart.
Practical result check
After any change, confirm through the Traefik API or dashboard that the router lists the intended middleware chain in the expected order, then repeat the curl header check and the repeated-request 429 check.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.