Proxy applications

Secure an HTTPS upstream

Verify an upstream certificate with the correct name and trust pool instead of disabling TLS checks.

10 minute lesson

~~~

When your upstream is on localhost, plain HTTP between Caddy and the app is fine. But when the upstream lives on another machine — another VM, another datacenter — that hop crosses a network, and it deserves encryption too.

Caddy speaks TLS to the upstream when you write the scheme into the address:

app.example.com {
  reverse_proxy https://api.internal.example.com
}

With https://, Caddy encrypts the upstream connection and verifies the upstream’s certificate: the chain must be trusted and the name must match, exactly the checks a browser would make.

That’s where internal services get awkward. Internal APIs often carry certificates from a private CA, one that Caddy’s trust store has never heard of. The proxy starts returning 502s, and the runtime log tells you why:

journalctl -u caddy | grep -i 'certificate'

You’ll find a line like tls: failed to verify certificate: x509: certificate signed by unknown authority.

There are two ways out. The wrong one is tls_insecure_skip_verify, which turns verification off entirely. Never use tls_insecure_skip_verify in production. Without verification, Caddy will happily send headers, cookies, and request bodies to anyone who can intercept the connection. Encrypted-but-unverified TLS protects you from nobody who matters.

The right fix is to trust the private CA explicitly, keeping every check enabled:

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

tls_trust_pool file loads your CA’s root certificate as the trust anchor for this upstream. Hostname verification stays on, so you still know you’re talking to the right server, signed by the issuer you chose.

Verify the trust chain from the server itself before blaming Caddy:

curl --cacert /etc/caddy/internal-ca.pem https://api.internal.example.com/health

If curl succeeds with the same CA file, Caddy will too. If curl fails, the problem is the certificate or the name, not your Caddyfile. One common variant: you dial the upstream by IP address while the certificate names a host. Fix the address, or set tls_server_name in the transport so verification checks the name the certificate actually carries.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →