Can Caddy do TLS passthrough

1. Caddy version (caddy version):

v2.3.0 h1:fnrqJLa3G5vfxcxmOH/+kJOcunPLhSBnjgIvjXV/QTA=

2. How I run Caddy:

a. System environment:

Linux, Ubuntu, systemd

c. Service/unit/compose file:


ExecStart=/usr/bin/caddy run --environ --config /etc/caddy/Caddyfile
ExecReload=/usr/bin/caddy reload --config /etc/caddy/Caddyfile


d. My complete Caddyfile or JSON config:

Main file:

    email <email>

(cf) {
    tls {
        dns cloudflare {env.CLOUDFLARE_API_TOKEN}

(local_only) {
    @external {
        not remote_ip
    respond @external 403

import /etc/caddy/Caddyfile.d/* {
   import cf

    reverse_proxy {
        header_up Host ""
        transport http {
            # tls_server_name ""

3. The problem I’m having:

The problem, or what I would like to do:

My Kubernetes cluster has an Ingress (think reverse proxy) that already has a valid certificate for the domain in the example ( and I would like to just use that certificate.

Currently my “public facing” HTTP server is Caddy, and I dont need it to create a Cert for the domain and then reverse proxy.

I would like to have Caddy just take in TLS packages which matches and pass them on to the next host, if that isn’t supported, I would like to:

Have Caddy get a Cert and terminate “”, then reverse_proxy it to my Kubernetes ingress, but over HTTPS. This will require it to pass the correct hostname and then validate the TLS cert presented.

I have tried different combinations of passing headers and using the SNI name (tls_server_name), but it does not seem to pass it correctly.

With the example in this post, I see a lot of the following errors:

Feb 06 17:39:09 core.terra caddy[2247810]: {"level":"error","ts":1612633149.7018335,"logger":"http.log.error","msg":"remote error: tls: internal error","request":{"remote_addr":"","proto":"HTTP/2.0","method":"GET","host":"","uri":"/","headers":{"User-Agent":["Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_6) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Safari/605.1.15"],"Accept-Language":["en-us"],"Accept-Encoding":["gzip, deflate, br"],"Cookie":["_session_=<REDACTED>="],"Accept":["text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"]},"tls":{"resumed":false,"version":772,"cipher_suite":4867,"proto":"h2","proto_mutual":true,"server_name":""}},"duration":0.006644412,"status":502,"err_id":"n8zar6gpz","err_trace":"reverseproxy.statusError (reverseproxy.go:783)"}

Thanks :slight_smile:

Let me know if you need more information.


Unfortunately, no it’s not possible with vanilla Caddy. But you can use the caddy-l4 plugin for this. But it doesn’t support Caddyfile config at this time.

But it should be possible to do what you want, i.e. terminate TLS at Caddy then do a new handshake to your upstream.

This line should not be necessary, Caddy passes through the Host from the original request.

I think this is on the right track. If the upstream cert isn’t in Caddy’s trust store, then you may need to specify tls_trusted_ca_certs.

Alternatively, if you can have Caddy grab the certificate/key from your cluster, then it could reuse that instead of having one issued via ACME.

1 Like

Thanks for the tip @francislavoie

I have built a version containing l4 now and I am able to setup a proper TLS passthrough when configuring a caddy that uses l4 alone and not any other app, like http.

It looks like when I use a mix of l4 and http, the http app will snap up the connection before l4 can deal with it and it results in:

Feb 07 16:20:06 core.terra caddy[2288202]: {"level":"debug","ts":1612714806.5956323,"logger":"http.stdlib","msg":"http: TLS handshake error from no certificate available for ''"}

This might be expected behaviour though, I am not sure if I should be able to use both http and l4?

If it is possible, maybe someone can point me in the direction of what I might do wrong? There is currently no mention of in the apps.http config.


What you probably want to do is bind l4 to your HTTPS port, then bind http to something else. In your l4 config, then, simply proxy to the http socket for all HTTP connections (that you’re not doing TLS passthru with).

1 Like

Hi @matt

I manage to get it working following your suggestion, not as elegant I suppose but it works.

Next thing to think about now is that it breaks my IP matchers :stuck_out_tongue:


This topic was automatically closed after 30 days. New replies are no longer allowed.