Feb 24, 2026·6 min read·4 visits
If Caddy can't find your private CA file, it doesn't crash—it just lets anyone with a valid public certificate (like Let's Encrypt) log in. This high-severity bug turns a missing file into a full authentication bypass.
A critical logic error in Caddy Server's TLS module causes mutual TLS (mTLS) authentication to fail open if the configured Certificate Authority (CA) file is missing or unreadable. Instead of halting the server, Caddy swallows the error and initializes the TLS configuration with a nil CA pool, defaulting to the system's public trust store.
Mutual TLS (mTLS) is the gold standard for service-to-service authentication. It’s the digital equivalent of a bouncer at a speakeasy who demands a very specific membership card signed by the owner before letting you in. In a properly secured environment, if the bouncer loses the guest list, the club stays closed. Nobody gets in. That is 'failing secure'.
Caddy, the modern, automatic-HTTPS web server we all know and love, decided to take a different approach in versions prior to 2.11.1. In a twist of irony suitable for a dark comedy, if Caddy cannot find the 'guest list' (your private CA certificate), it doesn't shut down. It doesn't panic. It doesn't even throw a loud error.
Instead, it shrugs, opens the door, and decides to trust anyone with a valid ID card from anywhere. Did you lose the file containing your internal Microservices Root CA? No problem. Caddy will now happily accept a client certificate signed by DigiCert, Let's Encrypt, or any other public CA in your server's system trust store. This is the definition of 'failing open,' and for a security gateway, it is catastrophic.
The root cause of CVE-2026-27586 isn't a buffer overflow or a complex cryptographic oracle attack. It is the bane of every senior engineer's existence: Error Swallowing.
The vulnerability resides in the ClientAuthentication.provision() method within modules/caddytls/connpolicy.go. When Caddy starts up or reloads its configuration, it attempts to load the PEM files specified in your trusted_ca_cert_file directive. These files contain the cryptographic roots of trust that define who is allowed to connect.
In the vulnerable code, the logic iterates through the file paths, attempts to convert them to DER format, and appends them to the pool. However, look at the error handling logic. If convertPEMFilesToDER returns an error (e.g., file not found, permission denied, or bad format), the function returns nil.
> [!NOTE]
> In Go, returning nil as an error usually means "Success".
By returning nil when an error actually occurred, the function lies to the caller. It reports that provisioning was successful, even though the specific CA file was never loaded. The variable clientauth.ca remains uninitialized (nil), but the server startup sequence continues as if everything is fine.
So, we have a nil CA pool. Why does that lead to an auth bypass instead of a handshake failure? This brings us to the behavior of Go's crypto/tls standard library, which Caddy relies on.
When Caddy configures the TLS listener, it sets ClientAuth to RequireAndVerifyClientCert. This tells the Go TLS stack: "Do not establish a connection unless the client presents a valid certificate."
However, it also sets ClientCAs to the pool we just failed to load—which is now nil. You might expect nil to mean "trust nobody." But in Go's crypto/tls implementation, if ClientCAs is nil, the library defaults to using the host system's root CA set to verify client certificates.
This means the trust boundary shifts from "Only certs signed by MySecretOrg" to "Any cert signed by a Public CA trusted by Debian/Ubuntu/Alpine." If an attacker presents a certificate for evil-hacker.com signed by Let's Encrypt, Caddy verifies the chain against the system roots, sees it's valid, and allows the connection.
The fix is embarrassingly simple, which highlights just how dangerous silent failures are. The developers simply needed to propagate the error up the stack so the server would refuse to start.
Here is the diff from commit d42d39b4bc237c628f9a95363b28044cb7a7fe72:
// modules/caddytls/connpolicy.go
for _, fpath := range clientauth.TrustedCACertPEMFiles {
ders, err := convertPEMFilesToDER(fpath)
if err != nil {
- return nil // The Logic Bomb
+ return err // The Fix
}
clientauth.TrustedCACerts = append(clientauth.TrustedCACerts, ders...)
}They also fixed a second instance of the same bug just a few lines down where caPool.Provision(ctx) was called. If the pool provisioning failed, it previously returned nil; now it correctly returns err.
This change ensures that if your security configuration is broken (missing files), the server refuses to operate. This is the correct behavior: Fail Closed.
Exploiting this requires no memory corruption wizardry. It purely relies on configuration drift or operator error. Here is the scenario:
/etc/caddy/private_ca.pem in the config./etc/caddy/private_ca.pem is deleted, renamed, or has its permissions changed so the Caddy user can't read it. Maybe an automated Ansible script failed halfway through a rotation.To bypass authentication, the attacker simply needs a client certificate that validates against the server's system roots. They can generate one using any public CA (if they have a domain) or potentially use a self-signed cert if the server's system trust store is incredibly lax (unlikely, but possible in containerized dev environments).
# Attacker generates a legit cert for their own domain
# (e.g. using certbot/LetsEncrypt)
certbot certonly --standalone -d attacker-box.com
# Attacker connects to the victim Caddy server
curl -v --cert /etc/letsencrypt/live/attacker-box.com/cert.pem \n --key /etc/letsencrypt/live/attacker-box.com/privkey.pem \n https://victim-caddy-server:8443Result: HTTP/2 200 OK. Access granted.
The remediation is straightforward. If you are running Caddy, check your version immediately.
1. Patch: Update to Caddy v2.11.1 or later. This version correctly crashes/halts if your CA files are missing.
2. Audit Your Config: Even after patching, verify that your CA files actually exist. If you update and Caddy suddenly refuses to start, congratulations—you were likely vulnerable and running in a fail-open state without realizing it.
3. Log Monitoring: In the fixed version, look for fatal errors during startup. In the vulnerable version, you won't see the error, so you must rely on auditing the filesystem to ensure the trusted_ca_cert_file paths are valid.
> [!WARNING] > Do not rely on "it works" as a sign of security. In this specific case, "it works" was the symptom of the vulnerability.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N/E:P| Product | Affected Versions | Fixed Version |
|---|---|---|
Caddy caddyserver | < 2.11.1 | 2.11.1 |
| Attribute | Detail |
|---|---|
| CWE ID | CWE-755 |
| Attack Vector | Network |
| CVSS v4.0 | 8.8 (High) |
| Impact | Authentication Bypass |
| Root Cause | Improper Error Handling (Swallowed Error) |
| Exploit Status | PoC Available |
Improper Handling of Exceptional Conditions