Operations · Caddy · reverse proxy

Caddy 2.11 changed the Host header for HTTPS upstreams.

The new default makes TLS proxying harder to misconfigure, but it can surprise applications that depend on receiving the browser-facing hostname. Here is what changed, how to recognize the failure, and when restoring the previous behavior is appropriate.

The short version

When Caddy proxies to an HTTPS upstream, it now sets the upstream HTTP Host value to that upstream’s address. If an application genuinely needs the original client-facing value, set header_up Host {hostport} explicitly.

A reverse proxy handles two related names when it connects to an HTTPS backend. TLS uses a Server Name Indication value to select and verify a certificate. HTTP then carries a Host header that can select a virtual host, influence authorization, and shape generated URLs. Letting those names disagree by accident is risky. Caddy now chooses agreement by default.

What changed

Before Caddy 2.11, reverse_proxy normally preserved the incoming request’s Host value. Operators proxying to an HTTPS backend were advised to override it with the upstream address. Pull request 7454 made that recommendation automatic.

The documentation describes the behavior as present since Caddy 2.11.0. The first stable release in that line was v2.11.1, published on 23 February 2026, and its release notes explicitly list the automatic Host rewrite.

BehaviorHost sent to an HTTPS upstream
Before Caddy 2.11The original client-facing host
Caddy 2.11 and laterThe selected upstream host and port

The implementation applies the transport default first and any user-configured request header operations afterward. An explicit header_up Host therefore remains authoritative.

What is affected

The automatic rewrite applies when the HTTP transport has TLS enabled, including upstreams written with an https:// address. Plain HTTP upstreams keep the established behavior because most local HTTP applications expect the original host.

You are most likely to notice the change when all of these are true:

  • Caddy was upgraded from a 2.10 release to the 2.11 release line.
  • The reverse proxy connects to its upstream over HTTPS.
  • The public hostname differs from the upstream address.
  • The application uses Host instead of X-Forwarded-Host to understand its public address.

How breakage presents itself

The community reports are useful because a Host mismatch does not produce one universal error. Different upstreams react at different layers.

  • Redirects point to an internal hostname or private address.
  • Authentication or sign-in flows loop or return to the wrong origin.
  • Virtual hosts return HTTP 421 Misdirected Request.
  • An application returns 500 while other pages appear normal.
  • WebSocket connections fail even though the main interface loads.

Reports attached to the Caddy change mention WordPress, Apache virtual hosts, TrueNAS, UniFi, Dell iDRAC, Gitea, and Kiwi TCMS. That list is evidence of application behavior, not proof that every deployment of those products is affected.

Why Caddy changed the default

For an HTTPS upstream, Caddy’s maintainers want the TLS ServerName and the HTTP authority to agree unless the operator deliberately chooses otherwise. A connection authenticated for one name followed by an HTTP request for another can reach unexpected virtual hosts or resources. The previous behavior made that security-sensitive mistake easy to create and difficult to see.

The change is security-adjacent rather than a vulnerability fix by itself. It removes a footgun. It also turns an implicit compatibility behavior into an explicit configuration choice for the smaller set of applications that require it.

Choose the migration that matches the application

Prefer the new default

If the backend supports proxy headers, configure it to trust Caddy and use X-Forwarded-Host for the original public hostname. Caddy continues to send that header by default. Keep the upstream Host aligned with the name used by TLS.

Restore the original Host deliberately

If the backend cannot use proxy headers and requires the public host, opt out inside that reverse proxy:

example.com {
    reverse_proxy https://app.internal.example {
        header_up Host {hostport}
    }
}

Use {hostport} rather than {host} when the incoming authority may include a nonstandard port. This restores the complete original Host value.

Operator take: This override intentionally permits the TLS name and HTTP Host name to differ. Treat that as an architecture decision. Confirm which virtual host the backend selects and which certificate Caddy verifies.

Keep upstream TLS verifiable

An internal CA is a better solution than disabling verification. When Caddy connects by an address that differs from the certificate name, configure the expected TLS name and trust the issuing CA:

example.com {
    reverse_proxy https://10.0.0.20:8443 {
        transport http {
            tls_server_name app.internal.example
            tls_trust_pool file /etc/caddy/internal-ca.pem
        }
    }
}

Avoid reaching first for tls_insecure_skip_verify. It disables authentication of the upstream TLS connection and does not solve the application’s public-host discovery problem.

Upgrade checklist

  1. Inventory every reverse_proxy target that uses HTTPS.
  2. Identify targets whose internal address differs from their public hostname.
  3. Test redirects, login flows, virtual-host selection, APIs, and WebSockets.
  4. Check whether the application supports and trusts X-Forwarded-Host.
  5. Add header_up Host {hostport} only where the dependency is understood.
  6. Verify upstream certificates with a trusted CA and the intended TLS name.

A breaking surprise and a better default

The operator frustration is understandable. A proxy upgrade that changes redirects or produces opaque application errors feels like a breaking change because it is one at the behavior boundary. The new default is still defensible: accidental disagreement between SNI and Host is subtle, security-sensitive, and much harder to detect than an application that visibly stops working.

The practical response is not to undo the change globally. Keep the safer default, find the exceptional upstreams, and document every compatibility override beside the application that requires it.

Sources