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.
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:
TrustedCACerts remains empty (no DER data appended from the file)len(clientauth.TrustedCACerts) > 0 guard on line 794 is false — skippedclientauth.CARaw is nil — line 806 returns nilclientauth.ca remains nil — no CA pool was createdprovision() returns nil — caller thinks provisioning succeededThen in ConfigureTLSConfig():
Active() returns true because TrustedCACertPEMFiles is non-emptyRequireAndVerifyClientCert (line 860)clientauth.ca is nil, so cfg.ClientCAs is never set (line 867 skipped)crypto/tls with RequireAndVerifyClientCert + nil ClientCAs verifies client certs against the system root pool instead of the intended CAThe fix is changing return nil to return err on lines 787 and 800.
{
"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)
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.
{
"nvd_published_at": "2026-02-24T17:29:03Z",
"cwe_ids": [
"CWE-755"
],
"github_reviewed_at": "2026-02-24T20:22:53Z",
"github_reviewed": true,
"severity": "HIGH"
}