Kai Ole Hartwig
22 min read
By

Traefik CVE-2026-114640: The plaintext router passes forged client certificates to the backend

If you require client certificates in front of an Ingress, the backend usually relies on a few headers: Ssl-Client-Verify reports whether the check succeeded, and Ssl-Client-Subject-Dn says who presented the certificate. Such headers are worth exactly as much as the component that sets them. In the Ingress-NGINX provider of Traefik 3.7, only the TLS router of an Ingress set them, while the plaintext router of the same Ingress took them unchanged from the client. If the HTTPS redirect is switched off there, a self-written header is enough to pose as any mTLS identity. CVE-2026-114640 is the most tangible of several flaws in the same translation layer that Traefik 3.7.14 closes.

TL;DR — 90 seconds

One Ingress, two routers and only one doorkeeper

With mutual TLS, the client identifies itself with a certificate. The server requests it during the handshake, and the client proves possession of the matching private key with a signature over the handshake so far. RFC 8446 calls this message CertificateVerify. Only the component that terminates TLS ever sees this proof. The backend behind the ingress controller only receives HTTP, so the result of the check has to travel some other way. According to its documentation, Ingress-NGINX uses four headers for this: ssl-client-verify with the verification result, ssl-client-subject-dn and ssl-client-issuer-dn with the names from the certificate, and ssl-client-cert with the full certificate in PEM format once the annotation nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream is set to "true".

Nothing about these headers is cryptographically bound. Their value rests entirely on the assumption that exactly one component between client and backend sets them and that the client cannot send them itself. The proxy has to honour that assumption on every path to the backend, not just on the one the operator has in mind.

According to the Kubernetes project's announcement, maintenance of Ingress-NGINX ended in March 2026, with no further releases or security fixes planned after that. Traefik offers a way out with its Ingress-NGINX provider. The provider reads existing Ingress resources including their NGINX annotations and translates them into Traefik routers, middlewares and TLS options. According to the advisory, it has understood the client certificate annotations since version 3.7.0. An Ingress with TLS comes with two routers: one on the HTTPS entry point, which carries the client check, and one on the HTTP entry point. According to the advisory, the second one normally only redirects to HTTPS, just as Ingress-NGINX redirects with a 308 by default once TLS is enabled for the Ingress.

This is exactly where the flaw lay. According to the advisory, the middleware that builds the Ssl-Client-* headers from the verified certificate was attached only to the TLS router. The HTTP router did not get it and therefore passed on whatever headers the client brought along. As long as it only redirects, that is harmless. But once someone sets nginx.ingress.kubernetes.io/ssl-redirect: "false", it forwards the request to the backend. A client with no certificate at all can then send Ssl-Client-Verify: SUCCESS and a subject DN of its choosing. The client check from auth-tls-verify-client does not apply on port 80 at all, because it is bound to the TLS handshake, and there is no handshake there.

A second weakness lies in how header names are spelled. HTTP also allows underscores in field names, and with its default setting aliasHeadersStrategy: keep Traefik forwards such headers unchanged. For applications behind CGI or FastCGI this is tricky, because RFC 3875 converts header names to upper case and replaces hyphens with underscores. Ssl-Client-Verify and Ssl_Client_Verify therefore map to the same name, HTTP_SSL_CLIENT_VERIFY. nginx ignores headers containing underscores in its default configuration, because underscores_in_headers is off and ignore_invalid_headers is on. Traefik, by contrast, has to remove such aliases explicitly, and that is exactly what the fix adds. The same pattern was behind the underscore header spoofing in FrankenPHP in June.

What the fix commit changes

The fix is pull request #13912, which landed in the v3.7 branch on September 16. At its core it changes the middleware in pkg/middlewares/ingressnginx/authtlspasscertificatetoupstream/auth_tls_pass_certificate_to_upstream.go. According to its title and description, the translation in translator.go now also attaches the middleware to the HTTP router of every Ingress with the annotation. New is a fixed set of managed headers: Ssl-Client-Cert, Ssl-Client-Verify, Ssl-Client-Subject-Dn and Ssl-Client-Issuer-Dn. The new function deleteManagedHeaders walks all header names on every request, normalises them and deletes every match. Normalisation upper-cases letters and turns every other character except digits into a hyphen. The code deliberately avoids Header.Del because, according to its comment, that function would miss exactly the alias spellings the fix is meant to guard against.

Only after deleting does the middleware decide what to set. Without a TLS connection it sets nothing. A code comment explains this with the fact that nginx builds these headers from the $ssl_client_* variables, which are empty outside of TLS. On TLS without a client certificate, Ssl-Client-Verify is set to NONE, and only with a verified certificate does the middleware fill all four headers from the certificate. The new tests cover both directions: a plaintext request carrying all four headers ends up with none of them, while an unrelated header such as X_Request_Id survives.

The pull request description sums up the cause in one sentence: these headers are Traefik's to set. Up to 3.7.13, ownership of the identity headers was a property of a single router. With the fix it becomes a property of the edge, which always deletes first and only sets values from verified state. So the flaw was less a missing check than the assumption that requests only arrive via the path the check was built for.

From port 80 to someone else's identity

Out of the box, nobody is affected. The Ingress-NGINX provider is not active in Traefik until you switch it on, in the Helm chart for example with providers.kubernetesIngressNginx.enabled: true. For an attack to succeed, several more things have to come together: an Ingress with auth-tls-secret and auth-tls-pass-certificate-to-upstream: "true", ssl-redirect disabled on exactly that Ingress, a reachable HTTP entry point for the host and a backend that bases its authorisation on the Ssl-Client-* headers.

The combination is mainly risky during a migration. According to the code comment in the fix, a plaintext request behind nginx carries none of these headers, because nginx only fills them from TLS variables. If you take over Ingress resources together with their annotations from an Ingress-NGINX installation, you rely on the translation carrying this property along. In Traefik 3.7.0 to 3.7.13 it did not.

The attack itself is a single HTTP request. The attacker sends it in plaintext to the host, sets Ssl-Client-Verify to SUCCESS and enters the identity they want to assume as the subject DN. Traefik forwards the request via the HTTP router to the backend, where it looks like a successful mTLS login. How much damage follows depends on what that identity is allowed to do in the backend. The CVSS rating reflects this: it sees no impact on Traefik itself but high impact on the confidentiality and integrity of the downstream system.

According to the advisory, the CVE affects only the Ingress-NGINX provider, and only in the 3.7 line, because older versions do not translate the annotation at all. If you run Traefik without this provider, you are not affected. The update is still worthwhile, because CVE-2026-114640 did not come alone.

The same release closes further flaws where a request gets past a check the operator believed was in place. With CVE-2026-114638, two Secrets whose names differ only by a dot versus a hyphen produced the same TLS option name, so one host accepted another host's client CA. With GHSA-qvj7-gq9c-hp7q, an Ingress with ssl-passthrough got an additional HTTP router from 3.7.13 on, where basic auth, auth-url and whitelist-source-range were never applied. GHSA-fh26-gfpp-7xxx affects all providers: Traefik binaries built with Go 1.27 skipped the SNI check for HTTP/2 requests with :scheme http over a TLS connection, making a router that requires client certificates reachable without one. According to the advisory, Traefik builds its official images with Go 1.26, but third-party rebuilds may be affected. Version 3.7.11 had already fixed GHSA-j994-9gqj-9hwq, a case where a conflict between TLS options fell back to the default options without a client check. And GHSA-p8wx-ph99-xgcr followed in 3.7.15 on October 15: if several Ingresses share a host and only some of them carry auth-tls-secret, the host likewise fell back to default options without a client certificate, according to the advisory, unless core.strictTLSOptions was enabled. All 3.7 versions up to and including 3.7.14 were affected.

CVE / advisoryAffectedFixPreconditionCVSS
CVE-2026-114640 (GHSA-mwrr-6hp4-pxr6)3.7.0 to 3.7.133.7.14Ingress-NGINX provider, auth-tls-pass-certificate-to-upstream, ssl-redirect: "false", backend trusts the headers8.7 v3.1 / 7.0 v4 (high)
CVE-2026-114638 (GHSA-rv2h-qh7j-xrjw)3.7.0 to 3.7.133.7.14Two auth-tls-secret names differing only by dot versus hyphen7.0 v4 (high)
GHSA-qvj7-gq9c-hp7q (no CVE)3.7.133.7.14ssl-passthrough plus access annotations, HTTP entry point reachable7.0 v4 (high)
GHSA-fh26-gfpp-7xxx (no CVE)up to 2.11.57, 3.0.0 to 3.7.13, Go 1.27 builds only2.11.58 / 3.7.14TLS option with client check, HTTP/2 with :scheme http7.0 v4 (high)
GHSA-p8wx-ph99-xgcr (no CVE)3.7.0 to 3.7.143.7.15Several Ingresses on one host, only some with auth-tls-secret7.0 v4 (high)
GHSA-j994-9gqj-9hwq, predecessor3.7.0 to 3.7.103.7.11One host on several Ingresses with client check8.2 v4 (high)

The advisories were published on October 7; VulnCheck only assigned the CVE number on October 22. Both CVSS scores are in the CVE record and describe the same flaw under two versions of the standard: 8.7 under v3.1, 7.0 under v4. As of October 27, none of the sources I checked mentions a public exploit, no attacks are known, and the flaw is not in the CISA KEV catalog. EPSS puts the probability of exploitation in the next 30 days at 0.20 percent. The advisory does describe the request precisely enough that rebuilding it takes no special skills.

Traefik 3.7.14, 3.7.16 and the new router names

The fix for CVE-2026-114640 ships in Traefik 3.7.14, released on October 6. You should not stop there. On October 15, 3.7.15 followed with three more advisories, including the mTLS flaw GHSA-p8wx-ph99-xgcr in the same provider described above. Since October 21, 3.7.16 has been available; with GHSA-hrpp-p4fc-wwf8 it closes a flaw in ForwardAuth that also affects the auth-url annotation in the Ingress-NGINX provider. I would therefore go straight to 3.7.16. According to the advisories, the 3.0 to 3.6 branches no longer receive patches; if you are on one of them, you have to move to 3.7. For Traefik 2.11, CVE-2026-114640 does not matter; the fixes for the release's other flaws are in 2.11.58 and later there.

Before updating, you should know the changes it brings. According to the migration guide, the Ingress-NGINX provider no longer replaces dots in Ingress and Secret names with hyphens. This changes the names of generated routers; an Ingress named my.ingress, for example, becomes the router default-my.ingress-rule-0-path-0. Routers and middlewares for Ingresses with ssl-passthrough now follow the pattern <namespace>-<ingress>-rule-<rule index>-path-<path index>. Dashboards, alerts and queries against the Traefik API that still use the old names will come up empty afterwards. TLSRoute router names change as well, and with the Gateway API only one of two identical routes on the same listener will serve traffic.

Two pitfalls concern the applications behind Traefik. A backend that used to see Ssl-Client-* headers on plaintext requests will no longer get any there, and underscore variants of these headers no longer arrive over TLS either. If you are coming from older versions, you also pick up the change from 3.7.13, after which Traefik no longer forwards the Upgrade: h2c and HTTP2-Settings headers to backends. Backends that need unencrypted HTTP/2 are then only reachable via the h2c:// scheme in the server URL.

The advisory does not list an official workaround. Since, according to the advisory, the request only reaches the backend when ssl-redirect is disabled, you close the path of CVE-2026-114640 until the update by re-enabling the redirect on all Ingresses with a client check. That does not help against the mixed-up client CAs or the passthrough router, though.

Hardening beyond the patch

First, some context. In this attack Traefik runs no foreign code and does not end up in an undefined state. It forwards a request as it is supposed to, just along a path where the identity check is missing. What helps, therefore, is anything that prevents plaintext paths to protected backends, keeps identity headers under control at the edge, fails closed instead of open when the TLS configuration conflicts, and makes the backend less dependent on headers. Anything that only hardens the Traefik container misses this mechanism.

Forbid the plaintext path with an admission rule

According to the advisory, the forged request only reaches the backend when ssl-redirect is disabled on the Ingress. That combination can be checked before the resource even lands in the cluster. A ValidatingAdmissionPolicy, generally available since Kubernetes 1.30, can reject Ingresses that set auth-tls-secret or auth-tls-pass-certificate-to-upstream and at the same time set ssl-redirect to "false". The condition can be written in CEL directly against metadata.annotations. A second rule can pin down which namespace may serve which host. It targets the predecessor flaw GHSA-j994-9gqj-9hwq, where, according to the advisory, a tenant could trigger the conflict with their own Ingress for an existing host. Kyverno or Gatekeeper do the same if you already run one of them. More patterns for admission policies and tenant separation are in the series TYPO3 on Kubernetes under IT-Grundschutz.

The operational cost is low as long as nobody actually needs this combination, and that is exactly what you check in your existing resources before enforcing the rule. Its limit is that it only closes the path of this one CVE. It does not help against mixed-up client CAs or the passthrough router.

Delete identity headers on the entry points

The fix makes Traefik the owner of the Ssl-Client-* headers. You can apply this principle yourself as well. The Headers middleware removes a request header if you set it to an empty string in customRequestHeaders. Attach such a middleware for Ssl-Client-Verify, Ssl-Client-Subject-Dn, Ssl-Client-Issuer-Dn and Ssl-Client-Cert to the entry points via entryPoints.<name>.http.middlewares. According to the docs, Traefik prepends these middlewares to the list of every router on the entry point and applies them in the order declared. On the HTTP entry point, the forged values disappear before any router can pass them on. On the HTTPS entry point, the deletion runs before the provider's middleware, which then sets the verified values.

Deleting by name only catches the canonical spelling, though. Against underscore variants, Traefik 3.7 offers the entry point option http.aliasHeadersStrategy. With delete, Traefik removes every request header whose name contains characters other than letters, digits and hyphens; with reject, it answers such requests with a 400. The default is keep. The docs rightly point out that both modes also hit legitimate headers such as X_Request_Id. So check your access logs before switching to see whether your clients send such names. For PHP applications behind Traefik I consider delete the right default, because RFC 3875 collapses hyphen and underscore anyway.

Backends that do not rely on headers alone

A backend that reads identity from headers should at least know that the request comes from Traefik. For that, you set up mTLS between Traefik and the backend yourself: Traefik presents a client certificate via its ServersTransport, and the backend only accepts connections with that certificate. I described how Traefik gets its own identity for this in Traefik carries its own identity. In addition, a NetworkPolicy limits access to the backend to the Traefik pods. Both protect against forged headers that reach the backend by bypassing Traefik. They do not help against CVE-2026-114640 itself, because here the forgery came through Traefik.

Let TLS options fail closed

Three flaws in this series, GHSA-j994-9gqj-9hwq, GHSA-p8wx-ph99-xgcr and CVE-2026-114638, revolve around TLS options, and the first two end the same way: two routers map the same host to different TLS options, Traefik discards both mappings and falls back to the default option, which does not require client certificates. The Traefik docs themselves state that a router with clientAuth then no longer enforces its check. The install option core.strictTLSOptions: true, as the flag --core.strictTLSOptions=true, disables this fallback. According to the docs, the routers involved are then marked as erroneous and not built at all. The host stays unreachable until the conflict is resolved, and that is exactly what you want for an mTLS-protected service. The advisory for GHSA-p8wx-ph99-xgcr names the option explicitly as a condition: with core.strictTLSOptions enabled, the host did not fall back to the default options.

The price is a new kind of outage. If a team creates a second Ingress for an existing host with a different TLS option, the host goes down instead of silently becoming insecure. That is why the option needs monitoring on router errors and clear ownership of hosts. In addition, the default TLSOption should not be weaker than necessary, because according to the docs it is the fallback for every conflict.

The translation layer as a transitional solution

The Ingress-NGINX provider is a compatibility layer. It has to reproduce the behaviour of another proxy, and every place where that does not succeed exactly can turn into a security flaw. Between 3.7.11 and 3.7.16, several advisories concern exactly this layer. I therefore see the provider as a migration tool rather than a permanent solution. I would move mTLS-protected services to native Traefik resources or the Gateway API in the medium term, where you define TLS options and routers yourself instead of having them derived from someone else's annotations.

What does not help here

seccomp, dropped capabilities, runAsNonRoot, allowPrivilegeEscalation: false and readOnlyRootFilesystem remain right for Traefik, but they target code that does damage inside the container. There is no such code here. The same applies to the Pod Security Standard “restricted”, to user namespaces and to a minimal base image. An egress lock for Traefik does nothing either, because the request takes the permitted path to the backend.

Nor does the obvious idea of checking the certificate from Ssl-Client-Cert against your own CA in the backend help. A certificate is public. Anyone who knows one can write it into a header without holding the private key. Only the signature in the TLS handshake proves possession, and only whoever terminates TLS sees it. Finally, a CVE gate in the pipeline would have missed the flaw for two weeks, since 15 days passed between the advisories on October 7 and the CVE number on October 22. An automated image update, on the other hand, would have proposed 3.7.14 as a merge request shortly after the release, whether or not a CVE number existed yet.

Detection and verification

You can find out whether you are affected in three steps. First the version: every Traefik image from 3.7.0 to 3.7.13 contains the flaw. Then the configuration: whether the Ingress-NGINX provider is running shows in --providers.kubernetesingressnginx among the arguments or in providers.kubernetesIngressNginx.enabled in the Helm values. Finally the resources: look for all Ingresses with nginx.ingress.kubernetes.io/auth-tls-pass-certificate-to-upstream: "true" and check whether any of them has ssl-redirect set to "false". While you are at it, look for auth-tls-secret values that differ only by dot versus hyphen, for hosts shared by several Ingresses and, on 3.7.13, for Ingresses with ssl-passthrough.

Whether someone exploited the flaw is harder to answer, since neither the vendor nor the advisory names indicators. I would search the Traefik access logs for requests to mTLS-protected hosts whose RequestScheme is http and that ended not with a redirect but with a 2xx status. The documented fields RequestHost, RequestScheme, RouterName and DownstreamStatus are enough for that. Traefik does not write request headers to the access log by default, because accesslog.fields.headers.defaultMode is set to drop. So you will only see the forged values themselves there if you explicitly enabled header logging. This search pattern is my own inference.

If you suspect something, first re-enable ssl-redirect on the affected Ingresses. Then check in the backend which actions ran under the affected identities, and invalidate sessions and tokens the backend issued based on those identities. The update to 3.7.16 follows in the same go.

Recommendation for operators

Act now if Traefik 3.7.0 to 3.7.13 serves services with client certificates through the Ingress-NGINX provider and ssl-redirect is disabled anywhere. Re-enable the redirect immediately, upgrade to 3.7.16 and check beforehand which dashboards and alerts depend on generated router names. The same applies if you run Traefik from a rebuild with Go 1.27 and use TLS options with a client check.

Monitoring is enough if you run Traefik without the Ingress-NGINX provider and with the official image. The update still belongs in the next maintenance window, because 3.7.14 to 3.7.16 close further flaws, for example in ForwardAuth and in the handling of NTLM and Kerberos connections, that have nothing to do with the provider.

As with every flaw, the same applies here: patches and updates belong in automation. If Renovate or Dependabot propose new versions of the Traefik Helm chart and image as merge requests on their own, and the pipeline tests them against a staging environment, a security release like 3.7.14 is a matter of hours rather than weeks. A cooldown of a few days for routine updates protects against compromised releases; security updates are exempt from it. The renamed routers in particular show what this process is worth, because a broken alert or a middleware reference that no longer resolves shows up in staging and not in production. The calendar shows how quickly a backlog builds up with Traefik: between August 26 and October 21, five releases, 3.7.12 to 3.7.16, each closed security advisories. If you update by hand and take two weeks of holiday in between, you end up rolling out several of them at once, with all the renames.

Frequently asked questions about CVE-2026-114640

Am I affected if I don't use the Ingress-NGINX provider?+

Not by this CVE. According to the advisory, it lives solely in the Ingress-NGINX provider from version 3.7.0 onwards. The update is still worthwhile, because the same release closes further flaws that are independent of the provider.

Is it enough to switch ssl-redirect back on?+

Against CVE-2026-114640, yes, because the HTTP router then only redirects. It does not fix the client CA mix-up from CVE-2026-114638 or the unprotected passthrough router, though. The update to 3.7.16 therefore remains necessary.

Why doesn't the backend simply verify the certificate from Ssl-Client-Cert?+

Because a certificate is public. Anyone who knows one can write it into a header without holding the private key. Only the signature in the TLS handshake proves possession, and only the component that terminates TLS ever sees it.

What does the aliasHeadersStrategy option do?+

It controls what happens to headers whose names contain underscores or other special characters. With delete Traefik removes them, with reject it refuses the request. That closes aliases such as Ssl_Client_Verify, which under RFC 3875 map to the same CGI name as the original. Check beforehand whether your clients send legitimate headers with underscores.

Conclusion

CVE-2026-114640 is not an exotic memory bug but a mistake about who owns a header. The proxy checked the identity correctly, but only on one of two paths to the same backend, and on the other it passed the client's claim straight through. The lesson reaches far beyond Traefik. Every header a backend reads as identity needs an owner at the edge that deletes it first on every path and only sets it from verified state. And every configuration that falls back to a weaker default on conflict should fail instead. If you are migrating away from Ingress-NGINX right now, take a deliberate look at your mTLS-protected services from port 80.

Sources

I check your Traefik edge for unprotected plaintext paths and foreign identity headers, switch mTLS to fail closed and automate your Traefik updates.

Traefik and ingress audits after migrating from Ingress-NGINX, header hygiene and mTLS all the way to the backend, admission rules for Ingress annotations, update automation with Renovate and tested pipelines.

Platform operations instead of consulting on paper: I continuously check, patch and harden your infrastructure.

About the author