SSL Bridging Examples

I’m going to keep this short since it’s more so a general question, not specific to my setup.

I’m interested in doing SSL bridging between two caddy instances. Are there any examples of two caddyfiles that do this? I’d like to adapt it.

I plan on having the first instance serve a wildcard certificate from Let’s Encrypt, while the second one will serve a self-signed certificate from Caddy’s internal CA.

Caddy #1:

# ...

*.example.com, example.com {
    # ...
    reverse_proxy https://caddy2.internal.domain
}

Caddy #2:

# ...

caddy2.internal.domain {
    tls internal
    # ...
}

Steps:

  1. Install, configure, and start Caddy #2
  2. Retrieve Caddy’s CA certificate from Caddy #2 (see Local HTTPS or Admin API for more details)
  3. Install Caddy #2’s CA certificate into a system trust store on the Caddy #1 server
  4. Install, configure, and start Caddy #1
1 Like

Here’s a few things I found, from what I believe is most relevant to least relevant.

Thanks to both of you.

I currently have it set to this:

Caddy #1:

# ...

*.example.com {

    @test host test1.example.com test2.example.com
	handle @test {
		reverse_proxy https://192.168.10.5:443
	}
}

Caddy #2:

*.example.com {
    tls internal

	@test1 host test1.example.com
	handle @test1 {
		reverse_proxy http://test1:8080
	}

    @test2 host test2.example.com
	handle @test2 {
		reverse_proxy http://test2:8081
	}
}

But it fails to work.

Logs:

{"level":"debug","ts":1742685435.7805002,"logger":"events","msg":"event","name":"tls_get_certificate","id":"38abc1bd-0136-438f-8b75-7fe7c788ae9d","origin":"tls","data":{"client_hello":{"CipherSuites":[49195,49199,49196,49200,52393,52392,49161,49171,49162,49172,4865,4866,4867],"ServerName":"","SupportedCurves":[25497,29,23,24,25],"SupportedPoints":"AA==","SignatureSchemes":[2052,1027,2055,2053,2054,1025,1281,1537,1283,1539,513,515],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"192.168.10.2","Port":37566,"Zone":""},"LocalAddr":{"IP":"172.18.0.2","Port":443,"Zone":""}}}}
{"level":"debug","ts":1742685435.7805333,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"172.18.0.2"}
{"level":"debug","ts":1742685435.7805414,"logger":"tls.handshake","msg":"no certificate matching TLS ClientHello","remote_ip":"192.168.10.2","remote_port":"37566","server_name":"","remote":"192.168.10.2:37566","identifier":"172.18.0.2","cipher_suites":[49195,49199,49196,49200,52393,52392,49161,49171,49162,49172,4865,4866,4867],"cert_cache_fill":0.0001,"load_or_obtain_if_necessary":true,"on_demand":false}
{"level":"debug","ts":1742685435.780636,"logger":"http.stdlib","msg":"http: TLS handshake error from 192.168.10.2:37566: no certificate available for '172.18.0.2'"}
{"level":"debug","ts":1742685435.8219447,"logger":"events","msg":"event","name":"tls_get_certificate","id":"ea9f250e-fc40-40e7-b8d9-1c35511b07c0","origin":"tls","data":{"client_hello":{"CipherSuites":[49195,49199,49196,49200,52393,52392,49161,49171,49162,49172,4865,4866,4867],"ServerName":"","SupportedCurves":[25497,29,23,24,25],"SupportedPoints":"AA==","SignatureSchemes":[2052,1027,2055,2053,2054,1025,1281,1537,1283,1539,513,515],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771],"RemoteAddr":{"IP":"192.168.10.2","Port":37580,"Zone":""},"LocalAddr":{"IP":"172.18.0.2","Port":443,"Zone":""}}}}
{"level":"debug","ts":1742685435.821979,"logger":"tls.handshake","msg":"no matching certificates and no custom selection logic","identifier":"172.18.0.2"}
{"level":"debug","ts":1742685435.8219862,"logger":"tls.handshake","msg":"no certificate matching TLS ClientHello","remote_ip":"192.168.10.2","remote_port":"37580","server_name":"","remote":"192.168.10.2:37580","identifier":"172.18.0.2","cipher_suites":[49195,49199,49196,49200,52393,52392,49161,49171,49162,49172,4865,4866,4867],"cert_cache_fill":0.0001,"load_or_obtain_if_necessary":true,"on_demand":false}
{"level":"debug","ts":1742685435.8220563,"logger":"http.stdlib","msg":"http: TLS handshake error from 192.168.10.2:37580: no certificate available for '172.18.0.2'"}

Is there a way to adjust the headers so it can send the correct host it matched with?

reverse_proxy https://192.168.10.5:443 {
    header_up Host {upstream_hostport}
}

Or just type a specific host value instead of {upstream_hostport}

How is Caddy running on these machines? Are they in containers?

That didn’t work either, but it made me notice that the server name was empty, so I tried adding it in manually:

*.example.com {

    @test host test1.example.com test2.example.com
	handle @test {
		reverse_proxy https://192.168.10.5:443 {
			transport http {
				tls_server_name test1.example.com
				tls_insecure_skip_verify # Required, otherwise it won't accept the self-signed certificate the second instance is using
			}
	}
}

And it worked. But since I’m hard-coding the server name, it won’t work when having multiple hosts per match. Is there a variable I could use that’ll find the one that matched? I tried using ‘upstream_hostport’, but that turns into ‘192.168.10.5:443’, the IP of the second instance, which is not what I want.

Yes, they’re both running in Docker.

We need the Docker configuration and network parameters for both.

I’m sorry, I’m dumb! I should have said {host}.

{upstream_hostport} is coming from the reverse_proxy upstream definition.

1 Like

True - unless you install Caddy’s CA cert from Caddy #2 into the system-wide trusted CA store on Caddy #1.

If you’re not worried about a MITM attack on the traffic between your Caddy servers, then using tls_insecure_skip_verify is fine.

1 Like

That worked, thank you!

Good point. I’ll consider doing this eventually.

No need. I have it working now.

1 Like

Here’s the updated configuration for anyone else who’s interested:

Caddy #1:

*.example.com {

    @test host test1.example.com test2.example.com
	handle @test {
		reverse_proxy https://192.168.10.5:443 {
			transport http {
				tls_server_name {host}
				tls_insecure_skip_verify # Required, unless Caddy #2's internal CA is manually installed in Caddy's #1 CA store.
			}
	}
}

Caddy #2:

*.example.com {
    tls internal

	@test1 host test1.example.com
	handle @test1 {
		reverse_proxy http://test1:8080
	}

    @test2 host test2.example.com
	handle @test2 {
		reverse_proxy http://test2:8081
	}
}

Configure tls_trust_pool to avoid insecurity trap

3 Likes

Added, thanks!

Final configuration:

Caddy #1:

*.example.com {

    @test host test1.example.com test2.example.com
	handle @test {
		reverse_proxy https://192.168.10.5:443 {
			transport http {
				tls_server_name {host}
				tls_trust_pool file /path/to/crt # Trusts the internal CA of the second instance
				# tls_insecure_skip_verify # Should only be used temporarily if not yet trusted
			}
	}
}

Caddy #2:

*.example.com {
    tls internal

	@test1 host test1.example.com
	handle @test1 {
		reverse_proxy http://test1:8080
	}

    @test2 host test2.example.com
	handle @test2 {
		reverse_proxy http://test2:8081
	}
}
2 Likes

I’ve noticed the leaf certificates expire after 12 hours, so I looked into changing the lifetime of the certificates, and it lead me to this configuration for Caddy #2:


{
	pki {
		ca local {
			intermediate_lifetime 3599d
		}
	}

}

*.example.com {
	tls {
		issuer internal {
            lifetime 3598d
        }
	}
	
	@test1 host test1.example.com
	handle @test1 {
		reverse_proxy http://test1:8080
	}

    @test2 host test2.example.com
	handle @test2 {
		reverse_proxy http://test2:8081
	}
}

The certificates will now practically last 10 years, but is there a way to extend it further? It’s limited by the lifetime of the root certificate being 10 years, so I tried looking into how to increase it, but I couldn’t find how to. If it’s not possible, then I might try manually creating a root certificate that lasts the length of time I want.

Edit: The configuration doesn’t work after reloading. I get errors such as “no certificate available”, which I think means it doesn’t know it’s meant to create/use internal certificates. I tested this by removing the old certificates and reloading, and it made no attempt at creating any local certificates.

1 Like

Why would you want to do that? Your Caddy #1 trusts anything issued by Caddy #2 because you have this:

tls_trust_pool file /path/to/crt # Trusts the internal CA of the second instance

Why does it matter that Caddy #2’s leaf certificate TTL is 12 hours? You don’t have to do anything to renew them - it all happens automatically. A 10-year TTL for leaf certs is just wrong.

I kept getting errors about trust whenever I reloaded it after a certain amount of time (assuming 12 hours), so I wanted to try to expand the lifetime of it. But yes, what you said is correct. It should trust all certificates signed by the CA, so I’m not sure why it was having issues trusting it after the leaf expires.

I’ll revert these changes and see if it comes up again.

It depends on how you’re running Caddy. If it’s in a Docker container, you need to make sure the folder with the CA certificate is on a persistent volume. Otherwise, every time you reload it, a new CA certificate will be issued.