Proxy applications
Secure an HTTPS upstream
Verify an upstream certificate with the correct name and trust pool instead of disabling TLS checks.
When your upstream is on localhost, plain HTTP between Caddy and the app is fine. Nothing leaves the machine. But when the upstream lives on another VM or in another datacenter, that hop crosses a network. It deserves encryption too.
Speak TLS to the upstream
Caddy talks TLS to the upstream when you put the scheme in the address:
app.example.com {
reverse_proxy https://api.internal.example.com
}
With https://, Caddy encrypts the connection and verifies the upstream’s certificate. The chain must be trusted and the name must match. These are exactly the checks a browser makes.
The private CA problem
This is where internal services get awkward. Internal APIs often carry certificates from a private CA, a certificate authority your company runs and Caddy’s trust store has never heard of.
The proxy starts returning 502s. 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.
The wrong fix
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 can’t tell your API from an attacker’s server sitting in the middle. It will send headers, cookies, and request bodies to anyone who intercepts the connection. Encrypted but unverified TLS protects you from nobody who matters.
The right fix
Trust the private CA explicitly, and keep every other check on:
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. You still know you’re talking to the right server, signed by the issuer you chose.
Test outside Caddy first
Before you blame Caddy, verify the trust chain from the server itself with curl and the same CA file:
curl --cacert /etc/caddy/internal-ca.pem https://api.internal.example.com/health
If curl succeeds, Caddy will too. If curl fails, the problem is the certificate or the name, not your Caddyfile. I like this check because it separates “my proxy config is wrong” from “the certificate is wrong” in one line.
One common variant: you dial the upstream by IP address while the certificate names a host. Verification fails because 10.0.0.12 doesn’t match api.internal.example.com. Fix the address, or set tls_server_name in the transport so verification checks the name the certificate actually carries.
Lesson completed