Wildcard domain certs with subdomain-specific client certs

1. Output of caddy version:

v2.6.1

2. How I run Caddy:

Via docker compose, config see below.

a. System environment:

Docker on openSUSE Tumbleweed

b. Command:

Nothing, handled by docker compose, config see below.

c. Service/unit/compose file:

docker compose file:

  caddy:
    build: /mnt/containers/caddy
    container_name: caddy
    restart: unless-stopped
    volumes:
      - /mnt/containers/caddy/site:/srv
      - /mnt/containers/caddy/data:/data
      - /mnt/containers/caddy/config:/config
      - /mnt/containers/caddy/caddyfile:/etc/caddy/Caddyfile
      - /mnt/containers/caddy/certs:/certs
    ports:
      - 80:80
      - 443:443/tcp
      - 443:443/udp

dockerfile:

# syntax=docker/dockerfile:1
FROM docker.io/library/caddy:builder AS builder
RUN xcaddy build --with github.com/caddy-dns/dynv6

FROM docker.io/library/caddy:latest
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

d. My complete Caddy config:

Caddyfile fully working with wildcard certificates:

*.planck.v6.rocks {
  tls {
    dns dynv6 [api-key]
  }
  encode gzip
  header {
    Strict-Transport-Security "max-age=31536000;"
    X-Content-Type-Options "nosniff"
    X-XSS-Protection "1; mode=block"
    X-Frame-Options "SAMEORIGIN"
    X-Robots-Tag "none"
    -Server
  }

  @homer host homer.planck.v6.rocks
  handle @homer {
    reverse_proxy homer:8080
  }

  @freshrss host freshrss.planck.v6.rocks
  handle @freshrss {
    reverse_proxy freshrss:80
  }

  @nextcloud host nextcloud.planck.v6.rocks
  handle @nextcloud {
    redir /.well-known/carddav /remote.php/carddav 301
    redir /.well-known/caldav /remote.php/caldav 301
    reverse_proxy nextcloud:80
  }

  @bookstack host bookstack.planck.v6.rocks
  handle @bookstack {
    reverse_proxy bookstack:80
  }

  @kavita host kavita.planck.v6.rocks
  handle @kavita {
    reverse_proxy kavita:5000
  }

  handle {
    abort
  }
}

Version without wildcard certificates, but with client certs (my_client_certs block), loaded for only some of the subdomains:

(my_local_only) {
  @my_remote_block {
    not remote_ip 192.168.1.0/24
  }
  respond @my_remote_block 403
}

(my_client_certs) {
  tls {
    client_auth {
      mode require_and_verify
      trusted_ca_cert_file [ca-file]
      trusted_leaf_cert_file [leaf-file]
    }
  }
}

(my_basic_auth) {
  basicauth {
    [username] [pw-hash]
  }
}

(my_header_config) {
  encode gzip
  header {
    Strict-Transport-Security "max-age=31536000;"
    X-Content-Type-Options "nosniff"
    X-XSS-Protection "1; mode=block"
    X-Frame-Options "SAMEORIGIN"
    X-Robots-Tag "none"
    -Server
  }
}

nextcloud.planck.v6.rocks {
  import my_header_config
  
  redir /.well-known/carddav /remote.php/carddav 301
  redir /.well-known/caldav /remote.php/caldav 301
  
  reverse_proxy nextcloud:80
}

freshrss.planck.v6.rocks {
  import my_client_certs
  import my_header_config
  
  reverse_proxy freshrss:80
}

homer.planck.v6.rocks {
  import my_client_certs
  import my_header_config
  import my_basic_auth
  
  reverse_proxy homer:8080
}

3. The problem I’m having:

I’m trying to use both tls client certs and a wildcard domain cert, but I only want the tls client certs enabled for some, not all, subdomains.
The two caddyfiles above do either-or (wildcard cert without subdomain client certs, or subdomain client certs without wildcard cert). So far, I have not found a way to do both at the same time with one caddyfile.

Reason for needing wildcard certs: Most of the time, my services are not available to the internet, port 443 is closed and the wildcard DNS entry points to my internal wireguard IP. Services are only available via my wireguard point-to-point tunnel, which is why a regular LE cert will not be able to be renewed. But a wildcard cert still works with these conditions.

Reason for needing client certs: Sometimes, I want to open up my services to the internet after all, at least temporarily. I want to do so safely. So for services like Homer, that come without any auth, I’d like to have at least basic auth enabled. And for services I only access via a web UI, I want client certs as additional security measure, so I don’t have to fully trust the services own auth or basic auth.

Reason for needing client certs only for some subdomains: Many apps don’t support client certs. Sadly, NextCloud is one of them. So while I do want client certs in general, for some services I don’t want them enforced, because it’d break app access. Which means client certs need to be enabled/disabled on subdomain-level.

4. Error messages and/or full log output:

None since individually, both types of config are working, I just don’t know how to combine them properly.

5. What I already tried:

Set up the caddyfile to use wildcard certs.
Set up the caddyfile to use regular certs with individual tls blocks for subdomains.

One possible solution I came up with is to simply use LEGo to generate the LE certs externally and manually specify the existing (external) wildcard cert within the regular non-wildcard subdomain-specific caddyfile entry. Something like:

freshrss.planck.v6.rocks {
  tls [cert-file] [key-file] {
    client_auth {
      mode require_and_verify
      trusted_ca_cert_file [ca-file]
      trusted_leaf_cert_file [leaf-file]
    }
  }
  
  reverse_proxy freshrss:80
}

This should enable me to use client certs individually for subdomains while still being able to use the (externally managed) wildcard cert, and being able to regularly (externally) renew the wildcard cert without having to open my services to the internet.
The drawback is, obviously, that caddy won’t manage the certs. I’d much rather use caddy, if in any way possible.

I haven’t found a way to configure the caddyfile to make this possible, but I think it might be possible with the json config. I have never used it, wasn’t able to untangle the documentation and am generally a bit lost on how to approach this if there is a json solution at all.

6. Links to relevant resources:

1 Like

Thanks for the detailed information!

Unfortunately, that’s not possible right now, via the Caddyfile. In JSON config, it is possible, though.

Client auth modes are controlled via TLS connection policies and it’s possible to use match to only enable that policy for a particular SNI host.

In the Caddyfile, we have no way to customize policy matchers because the concept of “sites” manages the policies automatically when adapting the config. I’m honestly not sure how it would need to look syntactically to work correctly, because the tls directive handles many different concepts at the same time.

For now, I guess you could run caddy adapt on your Caddyfile to get the JSON output, then script something with jq or some other JSON manipulation tool to change the connection policies to match what you need.

2 Likes

Maybe it’s possible to do this the other way around? Instead of using the wildcard domain as base and ask for client certs only for subdomains, I could use the subdomains as in the 2nd caddyfile example (each can have its own tls directive, no conflicts here), if only it was possible to tell caddy to manage a wildcard cert for those instead of the normal subdomain cert? This is quite close to the “solution” of managing the wildcard cert externally and handing the cert files to caddy to use for each regular subdomain entry (which should work, right?).

I’ve found the “wildcard” caddyfile directive for Caddy v1, but apparently it got removed in Caddy v2. With something like this, it should be possible? Am I reading it right that this is not possible with a caddyfile any more?

If this is the case, thinking along the lines of the json config: Do you think it’d be easier to start with the wildcard caddyfile and try to adapt that for the subdomain-specific tls directives or start with the regular (one entry for each subdomain) caddyfile and adapt that in order to make caddy manage a single wildcard cert instead of individual subdomain certs?

1 Like

Unfortunately, no there’s no way to do it the other way either right now in the Caddyfile.

There was a lengthy discussion here on GitHub about wildcard cert config patterns:

Our conclusion ultimately was that this pattern is what we should recommend for wildcard certs.

Of course that pattern assumes that you need the same TLS config and same logging config for every subdomain, because the tls and log directives are site-scoped, they can’t be used within handle blocks because they don’t get matched the same way. Request matchers only run while the HTTP middleware pipeline is running, but TLS happens before all HTTP middlewares so that’s impossible – for logging, it’s because it’s in a separate area of the JSON config so it’s complicated for a different reason.

I don’t want to say this’ll never be improved upon, but it seems like a hard problem to solve and we haven’t come up with any ideas for making it feel comfortable or natural to use.

I also envision that it would significantly complicate some already-complicated logic to reconcile the TLS and logging configs for all configured sites. We could do it as global options in the servers part and that might be okay for TLS connection policies. But I don’t want to continually pile things in there. Worried about bloat.

I definitely think the one-site-for-the-wildcard-with-host-matchers-within pattern is the way to go. That’s the simplest base config to start from. All you need to do after that is merge in the tls_connection_policies you need for each domain, matching with match > sni.

2 Likes

Thanks for all the info, I’ll try to look into the json config then!

If the caddyfile config ever gets expanded a bit, I do think that a reasonably simple option to force the use of wildcard certs would be absolutely awesome, whereever it gets placed.

3 Likes

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