Caddy: mTLS client authentication silently fails open when CA certificate file is missing or malformed
GHSA-hffm-g8v7-wrv7 · CVE-2026-27586 · GO-2026-4539
Published · Modified
AI SAST
Find this class of vulnerability in your own code
Corgea's AI-native static analysis detects vulnerabilities like this one across your repositories, ranks them by exploitability, and returns review-ready fixes.
Description
Summary
Two swallowed errors in ClientAuthentication.provision() cause mTLS client certificate authentication to silently fail open when a CA certificate file is missing, unreadable, or malformed. The server starts without error but accepts any client certificate signed by any system-trusted CA, completely bypassing the intended private CA trust boundary.
Details
In modules/caddytls/connpolicy.go, the provision() method has two return nil statements that should be return err:
Bug #1 — line 787:
ders, err := convertPEMFilesToDER(fpath)
if err != nil {
return nil // BUG: should be "return err"
}
Bug #2 — line 800:
err := caPool.Provision(ctx)
if err != nil {
return nil // BUG: should be "return err"
}
Compare with line 811 which correctly returns the error:
caRaw, err := ctx.LoadModule(clientauth, "CARaw")
if err != nil {
return err // CORRECT
}
When the error is swallowed on line 787, the chain is:
TrustedCACertsremains empty (no DER data appended from the file)- The
len(clientauth.TrustedCACerts) > 0guard on line 794 is false — skipped clientauth.CARawis nil — line 806 returns nilclientauth.caremains nil — no CA pool was createdprovision()returns nil — caller thinks provisioning succeeded
Then in ConfigureTLSConfig():
Active()returns true becauseTrustedCACertPEMFilesis non-empty- Default mode is set to
RequireAndVerifyClientCert(line 860) - But
clientauth.cais nil, socfg.ClientCAsis never set (line 867 skipped) - Go's
crypto/tlswithRequireAndVerifyClientCert+ nilClientCAsverifies client certs against the system root pool instead of the intended CA
The fix is changing return nil to return err on lines 787 and 800.
PoC
- Configure Caddy with mTLS pointing to a nonexistent CA file:
{
"apps": {
"http": {
"servers": {
"srv0": {
"listen": [":443"],
"tls_connection_policies": [{
"client_authentication": {
"trusted_ca_certs_pem_files": ["/nonexistent/ca.pem"]
}
}]
}
}
}
}
}
Start Caddy — it starts without any error or warning.
Connect with any client certificate (even self-signed):
openssl s_client -connect localhost:443 -cert client.pem -key client-key.pem
- The TLS handshake succeeds despite the certificate not being signed by the intended CA.
A full Go test that proves the bug end-to-end (including a successful TLS handshake with a random self-signed client cert) is here: https://gist.github.com/moscowchill/9566c79c76c0b64c57f8bd0716f97c48
Test output:
=== RUN TestSwallowedErrorMTLSFailOpen
BUG CONFIRMED: provision() swallowed the error from a nonexistent CA file.
tls.Config has RequireAndVerifyClientCert but ClientCAs is nil.
CRITICAL: TLS handshake succeeded with a self-signed client cert!
The server accepted a client certificate NOT signed by the intended CA.
--- PASS: TestSwallowedErrorMTLSFailOpen (0.03s)
Impact
Any deployment using trusted_ca_cert_file or trusted_ca_certs_pem_files for mTLS will silently degrade to accepting any system-trusted client certificate if the CA file becomes unavailable. This can happen due to a typo in the path, file rotation, corruption, or permission changes. The server gives no indication that mTLS is misconfigured.
References
- WEB https://github.com/caddyserver/caddy/security/advisories/GHSA-hffm-g8v7-wrv7
- ADVISORY https://nvd.nist.gov/vuln/detail/CVE-2026-27586
- WEB https://github.com/caddyserver/caddy/commit/d42d39b4bc237c628f9a95363b28044cb7a7fe72
- WEB https://gist.github.com/moscowchill/9566c79c76c0b64c57f8bd0716f97c48
- PACKAGE https://github.com/caddyserver/caddy
- WEB https://github.com/caddyserver/caddy/releases/tag/v2.11.1
- WEB https://pkg.go.dev/vuln/GO-2026-4539
Ready to move
Start Securing
Free, no credit card | First findings in minutes