Wildcard certificates for specific domains & custom (self-signed) for others—Why Is everything using custom certificate?

1. The problem I’m having:

Hello! :wave:

I am trying to apply the following logic in my Caddy Server configuration. I want to be able to configure different domains/addresses using the automatic TLS provided by Let’s Encrypt to get wildcard certificates (integrating it with Cloudflare). For all those domains/IPs or blocks that I don’t explicitly define in the configuration that may come to point to my server, I want to configure it to serve a custom (self-signed) certificate by default (as a fallback).

In order to address the above, I have created the following configuration.

{
	debug

	email {$TLS__EMAIL}
}

:443 {
	tls /security/certificates/selfsigned.crt /security/certificates/selfsigned.key
	
	respond "Hello, world (fallback)!" 200
}

*.example.com, example.com {
	tls {$TLS__EMAIL} {
		dns cloudflare {$TLS__CLOUDFLARE_API_TOKEN}
		resolvers 1.1.1.1 8.8.8.8 8.8.4.4
	}

	respond "Hello, world (example.com)!" 200
}
~ ❯ curl https://test.example.com
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

~ ❯ curl -k https://test.example.com
Hello, world (example.com)!%

~ ❯ curl https://test.anotherdomain.com
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.se/docs/sslcerts.html

curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.

~ ❯ curl -k https://test.anotherdomain.com
Hello, world (fallback)!%

What is the problem? If I apply this configuration on my server, despite having explicitly defined the tls configuration for my domains, they are all served with the custom (self-signed) certifcate. What I would expect is that the domains would use the tls configuration specified for them, obtaining and serving a specific or adhoc generated wildcard certificate. On the other hand, if other unidentified HTTPS requests come to my server (through an unregistered domain pointing to the server, through the server IP, etc.) the intention is to serve the custom certificate.

Is this behavior not supported by Caddy Server?

Thank you very much! :pray:

2. Error messages and/or full log output:

ingress__caddy  | {"level":"debug","ts":1739897695.673702,"logger":"events","msg":"event","name":"tls_get_certificate","id":"ffcc3e75-f663-4e09-b616-aa3a9e19d04f","origin":"tls","data":{"client_hello":{"CipherSuites":[4867,4866,4865,52393,52392,52394,49200,49196,49192,49188,49172,49162,159,107,57,65413,196,136,129,157,61,53,192,132,49199,49195,49191,49187,49171,49161,158,103,51,190,69,156,60,47,186,65,49169,49159,5,4,49170,49160,22,10,255],"ServerName":"test.example.com","SupportedCurves":[29,23,24,25],"SupportedPoints":"AA==","SignatureSchemes":[2054,1537,1539,2053,1281,1283,2052,1025,1027,513,515],"SupportedProtos":["h2","http/1.1"],"SupportedVersions":[772,771,770,769],"RemoteAddr":{"IP":"37.11.133.51","Port":59759,"Zone":""},"LocalAddr":{"IP":"172.18.0.2","Port":443,"Zone":""}}}}
ingress__caddy  | {"level":"debug","ts":1739897695.6739562,"logger":"tls.handshake","msg":"no matching certificate; will choose from all certificates","identifier":"test.example.com"}
ingress__caddy  | {"level":"debug","ts":1739897695.6739671,"logger":"tls.handshake","msg":"choosing certificate","identifier":"test.example.com","num_choices":3}
ingress__caddy  | {"level":"debug","ts":1739897695.673991,"logger":"tls.handshake","msg":"custom certificate selection results","identifier":"test.example.com","subjects":["clancy"],"managed":false,"issuer_key":"","hash":"caaa6fb80b03135330bff9b4c62ddb51d08c0070bb2c288734aef1dd1558e4f7"}
ingress__caddy  | {"level":"debug","ts":1739897695.674003,"logger":"tls.handshake","msg":"matched certificate in cache","remote_ip":"37.11.133.51","remote_port":"59759","subjects":["clancy"],"managed":false,"expiration":2055242426,"hash":"caaa6fb80b03135330bff9b4c62ddb51d08c0070bb2c288734aef1dd1558e4f7"}

3. Caddy version:

v2.9.1 h1:OEYiZ7DbCzAWVb6TNEkjRcSCRGHVoZsJinoDR/n9oaY=

4. How I installed and ran Caddy:

Using docker with the following image:

  • caddy:latest

a. System environment:

  • OS: Ubuntu 24.04
  • Docker Version: `Docker version 27.3.1, build ce12230``

b. Command:

docker compose up

c. Service/unit/compose file:

services:
  caddy:
    image: caddy:latest
    container_name: caddy
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - ./certificates:/security/certificates

d. My complete Caddy config:

{
	debug

	email {$TLS__EMAIL}
}

:443 {
	tls /security/certificates/selfsigned.crt /security/certificates/selfsigned.key
	
	respond "Hello, world (fallback)!" 200
}

*.example.com, example.com {
	tls {$TLS__EMAIL} {
		dns cloudflare {$TLS__CLOUDFLARE_API_TOKEN}
		resolvers 1.1.1.1 8.8.8.8 8.8.4.4
	}

	respond "Hello, world (example.com)!" 200
}

5. Links to relevant resources:

N/A

I’ve been mulling over this the last few days. I’m not knowledgeable on the exact intricacies of how Caddy’s Caddyfile works, but I believe your required solution would fall in the tls directive. I think you should be using on_demand_tls because you don’t know what will be required ahead of time. As for how all of this will reflect in a Caddyfile, I do not know.

I believe using :443 and then other site blocks will not work, because it will all default to the :443 block’s rules.

Hello, @TheRettom :wave: Thank you for your reply.

on_demand_tls is not a valid solution for me, because in my case I want to configure a custom certificate.

On the other hand, using a :443 block in conjunction with specific addresses should work correctly, as it really does with the respond directive, but not for the tls directive. That is, if there is an address configured and a request comes in with that origin, it should be answered with that block, but if the origin is identified it should come in through the :443 block.

At the moment the workaround I am using is the following:

{
	debug

	email {$TLS__EMAIL}
	default_sni invalid.invalid
}

invalid.invalid {
	tls /security/certificates/invalid.invalid.crt /security/certificates/invalid.invalid.key
}

:443 {
	respond "Hello, world (fallback)!" 200
}

*.example.com, example.com {
	tls {$TLS__EMAIL} {
		dns cloudflare {$TLS__CLOUDFLARE_API_TOKEN}
		resolvers 1.1.1.1 8.8.8.8 8.8.4.4
	}

	respond "Hello, world (example.com)!" 200
}

What do you think about this configuration?

Thanks! :pray:

Hm. The only other thing I could think of for a solution, and I’m not entirely sure if it would work, is adding to the :443 block.

:443 {
	tls internal /security/certificates/selfsigned.crt /security/certificates/selfsigned.key
	respond "Hello, world (fallback)!" 200
}

If that doesn’t work, then it’s beyond me. Unless someone else can chime in, I have no idea.

Hello, @TheRettom!

The configuration you specify is just what I mention in the post and it doesn’t work. How does it differ from what I have explained? Also, the configuration you have specified is not valid. You cannot set internal and a custom certificate at the same time.

Thanks.

Regards!

1 Like