Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875
- Part 1: How fzf Just Became Way More Memory-Efficient: The Bitmap Cache Upgrade
- Part 2: Unlocking Faster Fuzzy Finding: How a Smart Work Queue Made fzf Even Quicker
- Part 3: Speeding Up fzf: How a Tiny Change Made the World's Fastest Fuzzy Finder Even Faster
- Part 4: Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer
- Part 5: Fixing Syncthing’s REST API: Why “application/json; charset=utf-8” Was Wrong and How a Simple Commit Made It Right
- Part 6: Fixing UI Lag | Lazygit just got a major speed boost for one of its most frustrating pain points
- Part 7: Traefik PR #12880: A Tiny Fix That Squashes Unnecessary Allocations and Kills Routing Lag
- Part 8: Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875
Traefik is one of the most popular open-source reverse proxies and load balancers, loved by developers for its simplicity, dynamic configuration, and seamless integration with Docker, Kubernetes, and other orchestrators. It sits between your clients and backend services, handling routing, TLS termination, and much more.
source: Deploying Your Container with HTTPS Using Traefik as a Reverse Proxy
One powerful feature is mutual TLS (mTLS) authentication, where both the client and the server prove their identity using certificates. After Traefik verifies a client certificate, you often want to forward details about that certificate to your backend application (for logging, authorization, or auditing). That’s exactly what the PassTLSClientCert HTTP middleware does.
But there was a subtle security bug in how this middleware handled headers — and Pull Request #12875 fixes it cleanly. Let’s walk through what the problem was, why it mattered, how the PR solves it, and what the code changes look like. By the end, you’ll see exactly how the goals of the PR were achieved.
What Does PassTLSClientCert Actually Do? 🔗
When you enable mTLS on a Traefik router, the client presents a certificate during the TLS handshake. Traefik extracts information from req.TLS.PeerCertificates and can add two special headers to the request before it reaches your backend:
X-Forwarded-Tls-Client-Cert: Contains the full PEM-encoded client certificate (if you setpem: true).X-Forwarded-Tls-Client-Cert-Info: Contains structured details (subject, issuer, validity dates, SANs, etc.) in an escaped string (if you configure theinfosection).
Here’s a typical configuration example:
http:
middlewares:
pass-client-cert:
passTLSClientCert:
pem: true
info:
subject: true
issuer: true
notBefore: true
notAfter: true
Your backend service can then read these headers and trust that they came from a verified TLS connection.
source: what is mtls tls mutual authentication
The Problem: Unsanitized Headers Could Leak Through 🔗
Before PR #12875 , the middleware had a classic header injection / data leakage vulnerability.
Imagine this scenario:
- A malicious client (or an upstream proxy) sends a request that already includes
X-Forwarded-Tls-Client-CertorX-Forwarded-Tls-Client-Cert-Infoheaders with fake or harmful values. - Traefik performs mTLS, extracts the real certificate data, and calls
req.Header.Set(...)to add the correct values. - But it never removed the original headers.
In Go’s http.Header, calling Set replaces the value for that specific key, but if the middleware logic was structured in a way that the original value persisted (or was appended in certain edge cases), the backend could receive unsanitized data. Worse, an attacker could craft headers that looked legitimate or contained malicious payloads.
This broke the sanitation guarantee that the middleware was supposed to provide: “Only data extracted from the actual TLS connection should ever reach the backend”.
source: what is mtls tls mutual authentication
The Fix: Explicit Header Deletion Before Setting Sanitized Values 🔗
PR #12875 , authored by Mathis (@LBF38) , makes one focused, high-impact change in the middleware code.
Key files changed:
pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go(the core logic)pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go(new tests)
Here’s the before vs. after in the critical part of ServeHTTP:
// BEFORE (simplified)
if p.pem {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
req.Header.Set(xForwardedTLSClientCert, getCertificates(...))
}
}
req.Header.Set(xForwardedTLSClientCertInfo, ...)
// AFTER (the fix)
ctx := middlewares.GetLoggerCtx(...)
logger := log.FromContext(ctx)
// 🔥 Explicitly delete any incoming (potentially malicious) headers
req.Header.Del(xForwardedTLSClientCert)
if p.pem {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
req.Header.Set(xForwardedTLSClientCert, getCertificates(ctx, req.TLS.PeerCertificates))
}
}
req.Header.Del(xForwardedTLSClientCertInfo)
if p.info != nil {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
headerContent := p.getCertInfo(ctx, req.TLS.PeerCertificates)
req.Header.Set(xForwardedTLSClientCertInfo, headerContent)
}
}
Why this works:
req.Header.Del(key)completely removes the header (and all values for that key) from the request.- Only after deletion does the middleware set the new, trusted value derived from
req.TLS.PeerCertificates. - If no valid peer certificates exist, the headers are simply not set — no stale data remains.
This is a textbook example of proper header sanitization in proxy middleware.
New Tests Prove the Fix 🔗
The PR also added clear test cases that simulate an attacker sending fake headers:
req.Header.Set(xForwardedTLSClientCert, "Unsanitized HEADER") // malicious input
// ... set real TLS certs ...
// After middleware runs:
assert.Equal(t, sanitizedCertValue, req.Header.Get(xForwardedTLSClientCert))
// Original "Unsanitized HEADER" is gone!
These tests ensure the behavior is rock-solid and will catch regressions in future releases.
Why This PR Matters (The “Why” Behind the Commits) 🔗
- Security: Prevents header injection attacks and ensures downstream services can always trust the certificate data.
- Consistency: Matches how other Traefik middlewares (and industry best practices like those in Nginx or Envoy) handle forwarded headers.
- Minimal & Safe: No new features, no breaking changes — just a surgical fix that improves reliability without touching configuration, TLS handshake, or routing logic.
The commits were deliberately small and focused:
- Update the middleware logic with explicit
Del()calls. - Add comprehensive tests.
- (Implicitly) Update any internal documentation or changelog references.
Goals of the Pull Request — Clearly Achieved ✅ 🔗
By the end of the PR:
- Goal 1: Stop unsanitized headers from reaching backends → Achieved with
req.Header.Del(). - Goal 2: Ensure only real TLS peer certificate data is forwarded → Achieved (still uses
req.TLS.PeerCertificates). - Goal 3: Maintain backward compatibility and zero impact on existing users → Achieved (no config changes required).
- Goal 4: Prove the fix works → Achieved with new unit tests.
This tiny but critical improvement makes Traefik even more trustworthy when you’re using mTLS in production.
Wrapping Up 🔗
Header sanitization might sound like a small detail, but in a reverse proxy that handles millions of requests, it’s the difference between “secure by default” and “vulnerable to clever attackers”. PR #12875 is a perfect example of proactive security engineering in open-source software.
If you use PassTLSClientCert today, update to the latest Traefik version that includes this fix. Your backends will thank you — and your security team will too.
Thanks to contributor Mathis (@LBF38) for the clean fix — this is exactly why open-source communities rock.
I hope you enjoyed reading this post as much as I enjoyed writing it. If you know a person who can benefit from this information, send them a link of this post. If you want to get notified about new posts, follow me on YouTube , Twitter (x) , LinkedIn , and GitHub .
- Part 1: How fzf Just Became Way More Memory-Efficient: The Bitmap Cache Upgrade
- Part 2: Unlocking Faster Fuzzy Finding: How a Smart Work Queue Made fzf Even Quicker
- Part 3: Speeding Up fzf: How a Tiny Change Made the World's Fastest Fuzzy Finder Even Faster
- Part 4: Fixing a Sneaky XSS Vulnerability in Hugo: Inside the Commit That Makes Markdown Rendering Safer
- Part 5: Fixing Syncthing’s REST API: Why “application/json; charset=utf-8” Was Wrong and How a Simple Commit Made It Right
- Part 6: Fixing UI Lag | Lazygit just got a major speed boost for one of its most frustrating pain points
- Part 7: Traefik PR #12880: A Tiny Fix That Squashes Unnecessary Allocations and Kills Routing Lag
- Part 8: Fixing Header Sanitization in Traefik’s PassTLSClientCert Middleware: A Deep Dive into PR #12875