Feb 24, 2026·6 min read·79 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
netfoil, an allowlist-based DNS proxy, failed to sanitize ALPN fields parsed from untrusted DNS-over-HTTPS (DoH) HTTPS Resource Records. This allowed attackers to inject ANSI escape sequences into log files or trigger Denial of Service (DoS) via uncontrolled memory allocations.
An issue was discovered in the tokio-postgres library for Rust prior to version 0.7.18. A trust assumption mismatch between the PostgreSQL protocol messages sent by a server and how they are parsed and indexed by the client-side library allows a rogue or compromised database server to trigger a Denial of Service (DoS) crash via an unhandled out-of-bounds slice indexing panic.
CVE-2026-14669 is a critical heap-based buffer overflow vulnerability in PostgreSQL's date/time formatting function to_char(timestamptz). The flaw arises from unsafe copying of user-controlled timezone abbreviations into a fixed-size internal buffer. An authenticated database user can trigger this issue by setting a long POSIX timezone abbreviation containing custom formatting, allowing them to overwrite adjacent heap structures and hijack execution control to achieve remote code execution (RCE) with the privileges of the 'postgres' operating system user.
An unauthenticated remote denial of service vulnerability exists in the Unleash feature management platform. By submitting a crafted JSON payload containing deeply nested structures to an OpenAPI-validated endpoint, an attacker can trigger uncontrolled recursion within the error formatting module. This leads to a call-stack exhaustion (RangeError: Maximum call stack size exceeded) inside the Node.js runtime, causing the service to crash immediately without recovery.
CVE-2026-63004 is a server-side request forgery (SSRF) vulnerability in the Unleash feature management platform. Authenticated administrators with CREATE_ADDON or UPDATE_ADDON privileges can exploit this vulnerability to initiate requests to loopback addresses, private networks, and cloud metadata endpoints, potentially leading to information disclosure and credential extraction.
Prior to version 8.0.3, Unleash's Markdown event formatter directly mutated the global template-escaping function of the shared mustache Node.js module, resulting in a process-wide security degradation where HTML/Markdown escaping was permanently disabled for the application lifetime.