Feb 21, 2026·6 min read·1 visit
Traefik versions < 2.6.1 fail to normalize Fully Qualified Domain Names (FQDNs) during TLS configuration lookups. Sending a Host header like 'example.com.' causes Traefik to miss the custom security policy (like mTLS) and revert to the default policy, potentially allowing unauthenticated access.
A logic error in Traefik's TCP router allows attackers to bypass specific TLS configurations—including Mutual TLS (mTLS) requirements—simply by appending a trailing dot to the Host header. This forces the proxy to fall back to the default, often less secure, TLS configuration.
In the world of DNS, we often forget that google.com is actually google.com.. That trailing dot represents the root of the DNS hierarchy. Browsers hide it, marketing teams hate it, but to a resolver, it is the absolute truth. It is the Fully Qualified Domain Name (FQDN). Most web servers normalize this automatically, stripping the dot before processing the request to ensure example.com and example.com. are treated as the same entity.
But what happens when a modern, "cloud-native" reverse proxy like Traefik decides to take things literally? You get CVE-2022-23632. This isn't a buffer overflow or a complex heap grooming exercise. It is a fundamental disagreement between how a user types a domain and how Go's map lookups handle strings.
Imagine you have a fortress. The front door is guarded by a bouncer who checks IDs (mTLS). The window, however, just has a sign that says "Default Entry." If you walk up to the bouncer and say "I am here for the Fortress," he checks your ID. If you say "I am here for the Fortress..." (with a dramatic pause/dot), the bouncer gets confused, checks his list, doesn't see an exact string match for the name with the pause, and shrugs: "I guess you go to the Default Entry then." You just walked past the security because of a punctuation mark.
The vulnerability resides in pkg/server/router/tcp/router.go. Traefik uses a map to store TLS configurations, keyed by the hostname. When a request comes in, Traefik extracts the Host header (or SNI) and looks it up in this map to determine which security settings to apply—cipher suites, min/max TLS versions, and crucially, ClientAuth (mTLS) settings.
The logic was painfully simple: exact string matching. In Go, map["example.com"] is a completely different memory address than map["example.com."]. There was no normalization step. If your configuration defined rules for secure.internal, the map key was secure.internal.
When an attacker sends a request with Host: secure.internal., the router successfully routes the traffic (because the routing logic often handles FQDNs or wildcards differently), but the TLS configuration lookup fails. Instead of rejecting the connection, Traefik's fail-safe mechanism kicks in: it applies the DefaultTLSConfigName. If your default configuration is permissive (which it usually is to support public traffic), the attacker just downgraded the security of that specific connection to the baseline.
Let's look at the vulnerable function findTLSOptionName. It takes a map of options and the host string. Notice the lack of preprocessing on the host variable.
// Vulnerable Code (simplified)
func findTLSOptionName(tlsOptionsForHost map[string]string, host string) string {
// Direct map lookup. If host is "example.com.", this returns nil
tlsOptions, ok := tlsOptionsForHost[host]
if ok {
return tlsOptions
}
// ... (logic to check wildcards omitted) ...
// Fallback to default if no match found
return traefiktls.DefaultTLSConfigName
}The fix introduced in version 2.6.1 is a classic "try it both ways" approach. It acknowledges that the input might be an FQDN and tries to normalize it by stripping or adding the dot to find a match.
// Patched Code in v2.6.1
func findTLSOptName(tlsOptionsForHost map[string]string, host string, fqdn bool) string {
// 1. Try exact match
tlsOptions, ok := tlsOptionsForHost[host]
if ok { return tlsOptions }
if !fqdn { return "" }
// 2. Try STRIPPING the trailing dot
if last := len(host) - 1; last >= 0 && host[last] == '.' {
tlsOptions, ok = tlsOptionsForHost[host[:last]]
if ok { return tlsOptions }
return ""
}
// 3. Try ADDING a trailing dot
tlsOptions, ok = tlsOptionsForHost[host+"."]
if ok { return tlsOptions }
return ""
}This simple change closes the gap. Now, example.com. will successfully resolve to the config for example.com.
The most dangerous scenario for this bug is an mTLS bypass. Mutual TLS is often used for internal services, where the server requires the client to present a trusted certificate. This is common in Zero Trust architectures.
The Setup:
api.secret.corp with strict mTLS required.The Attack: A standard request gets blocked:
$ curl https://api.secret.corp/admin
> 400 Bad Request (No Client Certificate)The exploit request adds the dot:
$ curl -k --header "Host: api.secret.corp." https://api.secret.corp/adminWhat happens internally:
api.secret.corp in the ClientHello (standard TLS).api.secret.corp. (from the header/SNI logic depending on setup).DefaultTLSConfig (no mTLS).api.secret.corp. to the correct backend service because the routing rules often treat FQDNs leniently or use regex.This vulnerability turns robust security configurations into security theater. The impact is technically rated as High (7.4), but functionally, it can be Critical depending on your architecture.
If you rely on Traefik as an Ingress Controller for Kubernetes, and you use it to terminate mTLS for sensitive pods, this bug renders that protection null and void against a trivial manipulation. It also allows attackers to bypass specific cipher suite restrictions. For example, if secure.bank.com requires TLS 1.3, but the default config allows TLS 1.0, an attacker could force a protocol downgrade by using the FQDN trick, potentially opening the door to other cryptographic attacks.
The real kicker? This leaves almost no trace in standard logs other than a slightly weird Host header, which most analysts would ignore as a typo or browser quirk.
The remediation is straightforward: Upgrade to Traefik v2.6.1. The patch handles the FQDN normalization logic internally, ensuring that host and host. share the same security context.
Emergency Workaround:
If you cannot upgrade immediately (perhaps you are stuck on an old vendor fork), you must explicitly define the FQDN in your router rules. If your rule is Host(\example.com`)`, you need to change it to:
rule = "Host(`example.com`) || Host(`example.com.`)"However, this is tedious and error-prone. The upgrade is the only scalable fix. This serves as a reminder to developers: inputs are messy, humans are creative, and DNS is a ancient beast that will bite you if you don't respect the dot.
CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N| Product | Affected Versions | Fixed Version |
|---|---|---|
Traefik Traefik Labs | < 2.6.1 | 2.6.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-295 |
| Attack Vector | Network |
| CVSS v3.1 | 7.4 (High) |
| EPSS Score | 0.00557 (Low) |
| Impact | Security Bypass (mTLS/TLS Options) |
| Exploit Status | PoC Available |
Improper Certificate Validation