# Caddy: mTLS client authentication silently fails open when CA certificate file is missing or malformed

Caddy's mTLS provision() swallowed CA-load errors, leaving ClientCAs nil so any system-trusted client cert passed RequireAndVerifyClientCert.

- Payout: Research
- Severity: high
- Program: Caddy
- Found by: moscowchill
- Disclosed: 2026-02-24
- CVE: CVE-2026-27586
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-hffm-g8v7-wrv7
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-hffm-g8v7-wrv7
- Tags: area:web domain:web impact:internal-access kind:research platform:ghsa sev:high tech:go technique:source-review vuln:auth-bypass vuln:misconfiguration

**Target:** Caddy TLS client_authentication module (connpolicy.go provision)
**Root cause:** Two `return nil` statements in ClientAuthentication.provision() swallow errors from convertPEMFilesToDER and caPool.Provision, leaving clientauth.ca nil while Active() still reports true and mode defaults to RequireAndVerifyClientCert.
**Steps:**
1. Configure Caddy with `trusted_ca_certs_pem_files: ["/nonexistent/ca.pem"]` in a tls_connection_policy client_authentication block.
2. Start Caddy; it starts with no error or warning because provision() returns nil.
3. Connect with `openssl s_client -connect localhost:443 -cert client.pem -key client-key.pem` using a self-signed cert.
4. Handshake succeeds: ClientCAs is nil so Go crypto/tls verifies against the system root pool instead of the intended CA.
**How found:** Code review of provision() in modules/caddytls/connpolicy.go spotted `return nil` where sibling code (line 811) correctly does `return err`; traced the resulting nil ca through Active() and ConfigureTLSConfig() to confirm ClientCAs is never set.
**Exploitation:** Attacker presents any client certificate chaining to a public/system-trusted CA (or otherwise accepted by the system pool) to a Caddy instance whose configured private CA file is missing or malformed; the handshake succeeds and mTLS is bypassed.
**Insight:** In Go TLS servers, RequireAndVerifyClientCert with a nil ClientCAs silently falls back to system roots — audit any config path where ClientCAs assignment is conditional on a pool that may be nil, and grep for `return nil` inside error branches of provisioning code.

## Proof of concept (copied exactly from the original report)

**Caddy config pointing mTLS at nonexistent CA file**

```
{
    "apps": {
        "http": {
            "servers": {
                "srv0": {
                    "listen": [":443"],
                    "tls_connection_policies": [{
                        "client_authentication": {
                            "trusted_ca_certs_pem_files": ["/nonexistent/ca.pem"]
                        }
                    }]
                }
            }
        }
    }
}
```

**openssl s_client handshake with client cert**

```
openssl s_client -connect localhost:443 -cert client.pem -key client-key.pem
```

