Does Caddy matches requests depending on the declared port or can it be faked?

1. The problem I’m having:

I’m configuring two domains: one public (port 443) and one on port 12000 protected by the network firewall. Will the private domain answer only to connections on port 12000 or faking SNI/Host header would allow an attacker to reach that reverse proxy?

I’ve been trying to reach it and the private certificate is presented but then it always answers 404 as expected.

I want to depend on this security behaviour to have the same Caddy for everything on the machine.

If it is not secure enough, how could I differentiate Tailscale traffic specifically?

2. Error messages and/or full log output:

It connects through port 443, it receives the certificate for the private domain, but then it reaches a 404 as expected.

curl -kv --connect-to test.otter-cat.ts.net:12000:34.76.170.44:443 'https://test.otter-cat.ts.net:12000/health'
* Connecting to hostname: 34.76.170.44
* Connecting to port: 443
*   Trying 34.76.170.44:443...
* Connected to (nil) (34.76.170.44) port 443 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.2 (IN), TLS header, Certificate Status (22):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.2 (IN), TLS header, Finished (20):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.2 (OUT), TLS header, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_128_GCM_SHA256
* ALPN, server accepted to use h2
* Server certificate:
*  subject: CN=test.otter-cat.ts.net
*  start date: Jul  4 05:15:13 2024 GMT
*  expire date: Oct  2 05:15:12 2024 GMT
*  issuer: C=US; O=Let's Encrypt; CN=E5
*  SSL certificate verify result: unable to get local issuer certificate (20), continuing anyway.
* Using HTTP2, server supports multiplexing
* Connection state changed (HTTP/2 confirmed)
* Copying HTTP/2 data in stream buffer to connection buffer after upgrade: len=0
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* Using Stream ID: 1 (easy handle 0x56202e06aeb0)
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
> GET /health HTTP/2
> Host: test.otter-cat.ts.net:12000
> user-agent: curl/7.81.0
> accept: */*
>
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* Connection state changed (MAX_CONCURRENT_STREAMS == 250)!
* TLSv1.2 (OUT), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
* TLSv1.2 (IN), TLS header, Supplemental data (23):
< HTTP/2 404
< alt-svc: h3=":443"; ma=2592000
< server: Caddy
< content-length: 0
< date: Mon, 29 Jul 2024 06:15:59 GMT
<
* Connection #0 to host (nil) left intact

3. Caddy version:

v2.7.6 h1:w0NymbG2m9PcvKWsrXO6EEkY9Ru4FJK8uQbYcev1p3A=

4. How I installed and ran Caddy:

Docker Compose.

a. System environment:

Linux, Docker, Tailscale.

b. Command:

Standard Docker command.

c. Service/unit/compose file:

Dockerfile:

FROM caddy:2.7.6

COPY caddy/origin /etc/ssl/origin
COPY caddy/Caddyfile /etc/caddy/Caddyfile

docker-compose.yml:

services:
  caddy:
    container_name: caddy
    build: ./caddy
    restart: unless-stopped
    ports: ['80:80', '443:443', '12000:12000']
    volumes:
      - /srv/data/caddy:/data
      - /var/run/tailscale/tailscaled.sock:/var/run/tailscale/tailscaled.sock

d. My complete Caddy config:

(proxy) {
	log {
		format json
	}
	skip_log /health

	reverse_proxy {
		to {args[0]}:{args[1]}
		lb_retries 40
		lb_try_duration 25s
		lb_try_interval 250ms
		transport http {
			dial_timeout 5s
			read_timeout 30s
			write_timeout 30s
			response_header_timeout 30s
		}
	}
}

test.onetbooking.com {
	import proxy engine 8080
	tls /etc/ssl/origin/cert.pem /etc/ssl/origin/key.pem
}

https://test.otter-cat.ts.net:12000 {
	import proxy flows 8080
}

:80 {
	respond 404
}

:443 {
	respond 404
}

5. Links to relevant resources:

This doesn’t make sense, this would never get hit because it requires a successful TLS connection to be able to perform HTTP routing. You can remove this.

If you adapt your config to JSON (with the caddy adapt -p command), you’ll get a better sense of how it works. You’ll see that you have separate servers per port. Config for servers don’t leak across eachother, so it’s isolated. If port 12000 is hit, it won’t ever run handlers for 443.

But if you have two domains on one port, then SNI determines the TLS cert that gets chosen, and the Host header determines HTTP routing.

2 Likes

Thank you!

I’ve used the JSON config multiple times and somehow didn’t think to do this. Another lesson I have learned from this post.

The JSON config explains everything much better. Now it is clear how the ports & routes interact.

Thank you very much, you’ve answered my question and even went beyond that to teach me new things.

1 Like

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