Confusion about Caddy 2.10.0's new wildcard stuff

1. The problem I’m having:

Been a while since I’ve used Caddy, and I figured it was worth trying it out with the 2.10.0 update.

My interpretation with the new update is that Caddy will only generate a wildcard certificate when possible as a default, however through testing I cannot get it to replicate this behavior without explicitly using a wildcard.

In the blow example, it generated a certificate for the full FQDN, not the wildcard. I realize that I can just put a wildcard in and it resolves the issue, but is this intended?

I used to use Caddy years ago and my greatest regret was that it generated individual certs and so when I saw the latest changelog I was excited to try out Caddy again.

2. Error messages and/or full log output:

caddy-1  | {"level":"info","ts":1747536071.0738835,"logger":"docker-proxy","msg":"Running caddy proxy server"}
caddy-1  | {"level":"info","ts":1747536071.0773175,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
caddy-1  | {"level":"info","ts":1747536071.077419,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
caddy-1  | {"level":"info","ts":1747536071.077424,"logger":"docker-proxy","msg":"Running caddy proxy controller"}
caddy-1  | {"level":"info","ts":1747536071.0777287,"logger":"docker-proxy","msg":"Start","CaddyfilePath":"/etc/caddy/Caddyfile","EnvFile":"","LabelPrefix":"caddy","PollingInterval":30,"ProxyServiceTasks":true,"ProcessCaddyfile":true,"ScanStoppedContainers":false,"IngressNetworks":"[]","DockerSockets":[""],"DockerCertsPath":[""],"DockerAPIsVersion":[""]}
caddy-1  | {"level":"info","ts":1747536071.0779781,"logger":"docker-proxy","msg":"Caddy ContainerID","ID":"354156ebc4309955f72fb477406a47639619606dd1da6c7540be47f3953d5c86"}
caddy-1  | {"level":"info","ts":1747536071.0780141,"logger":"docker-proxy","msg":"Connecting to docker events","DockerSocket":""}
caddy-1  | {"level":"info","ts":1747536071.0791142,"logger":"docker-proxy","msg":"IngressNetworksMap","ingres":"map[606e44ec52c8562af4cefc79943d0d225da561ef638e35187cb6d664a4290359:true caddy:true]"}
caddy-1  | {"level":"info","ts":1747536071.0826154,"logger":"docker-proxy","msg":"Swarm is available","new":false}
caddy-1  | {"level":"info","ts":1747536071.0864053,"logger":"docker-proxy","msg":"New Caddyfile","caddyfile":"{\n\tacme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}\n\tauto_https prefer_wildcard\n}\ntest.redacted.com {\n\trespond \"Hello World\"\n}\n"}
caddy-1  | {"level":"info","ts":1747536071.086585,"logger":"docker-proxy","msg":"New Config JSON","json":"{\"apps\":{\"http\":{\"servers\":{\"srv0\":{\"listen\":[\":443\"],\"routes\":[{\"match\":[{\"host\":[\"test.redacted.com\"]}],\"handle\":[{\"handler\":\"subroute\",\"routes\":[{\"handle\":[{\"body\":\"Hello World\",\"handler\":\"static_response\"}]}]}],\"terminal\":true}]}}},\"tls\":{\"automation\":{\"policies\":[{\"subjects\":[\"test.redacted.com\"],\"issuers\":[{\"challenges\":{\"dns\":{\"provider\":{\"api_token\":\"{env.CLOUDFLARE_API_TOKEN}\",\"name\":\"cloudflare\"}}},\"module\":\"acme\"}]}]}}}}"}
caddy-1  | {"level":"info","ts":1747536071.0866148,"logger":"docker-proxy","msg":"Sending configuration to","server":"localhost"}
caddy-1  | {"level":"info","ts":1747536071.0869803,"logger":"admin.api","msg":"received request","method":"POST","host":"localhost:2019","uri":"/load","remote_ip":"127.0.0.1","remote_port":"53888","headers":{"Accept-Encoding":["gzip"],"Content-Length":["467"],"Content-Type":["application/json"],"User-Agent":["Go-http-client/1.1"]}}
caddy-1  | {"level":"info","ts":1747536071.0875742,"logger":"admin","msg":"admin endpoint started","address":"localhost:2019","enforce_origin":false,"origins":["//localhost:2019","//[::1]:2019","//127.0.0.1:2019"]}
caddy-1  | {"level":"info","ts":1747536071.0878258,"logger":"tls.cache.maintenance","msg":"started background certificate maintenance","cache":"0xc000b96200"}
caddy-1  | {"level":"info","ts":1747536071.0879316,"logger":"http.auto_https","msg":"server is listening only on the HTTPS port but has no TLS connection policies; adding one to enable TLS","server_name":"srv0","https_port":443}
caddy-1  | {"level":"info","ts":1747536071.0879447,"logger":"http.auto_https","msg":"enabling automatic HTTP->HTTPS redirects","server_name":"srv0"}
caddy-1  | {"level":"info","ts":1747536071.0880811,"logger":"http","msg":"enabling HTTP/3 listener","addr":":443"}
caddy-1  | {"level":"info","ts":1747536071.08816,"logger":"http.log","msg":"server running","name":"srv0","protocols":["h1","h2","h3"]}
caddy-1  | {"level":"warn","ts":1747536071.088187,"logger":"http","msg":"HTTP/2 skipped because it requires TLS","network":"tcp","addr":":80"}
caddy-1  | {"level":"warn","ts":1747536071.0881906,"logger":"http","msg":"HTTP/3 skipped because it requires TLS","network":"tcp","addr":":80"}
caddy-1  | {"level":"info","ts":1747536071.0881917,"logger":"http.log","msg":"server running","name":"remaining_auto_https_redirects","protocols":["h1","h2","h3"]}
caddy-1  | {"level":"info","ts":1747536071.0881937,"logger":"http","msg":"enabling automatic TLS certificate management","domains":["test.redacted.com"]}
caddy-1  | {"level":"info","ts":1747536071.0885038,"msg":"autosaved config (load with --resume flag)","file":"/config/caddy/autosave.json"}
caddy-1  | {"level":"info","ts":1747536071.0885088,"logger":"admin.api","msg":"load complete"}
caddy-1  | {"level":"info","ts":1747536071.0885646,"logger":"docker-proxy","msg":"Successfully configured","server":"localhost"}
caddy-1  | {"level":"info","ts":1747536071.0890148,"logger":"admin","msg":"stopped previous server","address":"localhost:2019"}
caddy-1  | {"level":"info","ts":1747536071.1013532,"logger":"tls","msg":"storage cleaning happened too recently; skipping for now","storage":"FileStorage:/data/caddy","instance":"6ef8b798-ee09-4631-94ab-1e1faf30d58b","try_again":174762,"try_again_in":8}
caddy-1  | {"level":"info","ts":1747536071.1016986,"logger":"tls","msg":"finished cleaning storage units"}
caddy-1  | {"level":"info","ts":1747536071.1095421,"logger":"tls.obtain","msg":"acquiring lock","identifier":"test.redacted.com"}
caddy-1  | {"level":"info","ts":1747536071.1161454,"logger":"tls.obtain","msg":"lock acquired","identifier":"test.redacted.com"}
caddy-1  | {"level":"info","ts":1747536071.1166034,"logger":"tls.obtain","msg":"obtaining certificate","identifier":"test.redacted.com"}
caddy-1  | {"level":"info","ts":1747536071.1212285,"logger":"tls.issuance.acme","msg":"waiting on internal rate limiter","identifiers":["test.redacted.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}
caddy-1  | {"level":"info","ts":1747536071.121238,"logger":"tls.issuance.acme","msg":"done waiting on internal rate limiter","identifiers":["test.redacted.com"],"ca":"https://acme-v02.api.letsencrypt.org/directory","account":""}
caddy-1  | {"level":"info","ts":1747536071.1212451,"logger":"tls.issuance.acme","msg":"using ACME account","account_id":"https://acme-v02.api.letsencrypt.org/acme/acct/redacted","account_contact":[]}
caddy-1  | {"level":"info","ts":1747536072.7148938,"msg":"trying to solve challenge","identifier":"test.redacted.com","challenge_type":"dns-01","ca":"https://acme-v02.api.letsencrypt.org/directory"}
caddy-1  | {"level":"info","ts":1747536081.5241692,"msg":"authorization finalized","identifier":"test.redacted.com","authz_status":"valid"}
caddy-1  | {"level":"info","ts":1747536081.524185,"msg":"validations succeeded; finalizing order","order":"https://acme-v02.api.letsencrypt.org/acme/order/redacted/redacted"}
caddy-1  | {"level":"info","ts":1747536084.922559,"msg":"got renewal info","names":["test.redacted.com"],"window_start":175264,"window_end":175279,"selected_time":175272,"recheck_after":174755,"explanation_url":""}
caddy-1  | {"level":"info","ts":1747536085.432382,"msg":"got renewal info","names":["test.redacted.com"],"window_start":175264,"window_end":175279,"selected_time":175277,"recheck_after":174755,"explanation_url":""}
caddy-1  | {"level":"info","ts":1747536085.432476,"msg":"successfully downloaded available certificate chains","count":2,"first_url":"https://acme-v02.api.letsencrypt.org/acme/cert/redacted"}
caddy-1  | {"level":"info","ts":1747536085.4757652,"logger":"tls.obtain","msg":"certificate obtained successfully","identifier":"test.redacted.com","issuer":"acme-v02.api.letsencrypt.org-directory"}
caddy-1  | {"level":"info","ts":1747536085.4758313,"logger":"tls.obtain","msg":"releasing lock","identifier":"test.redacted.com"}
caddy-1  | {"level":"warn","ts":1747536085.4790637,"logger":"tls","msg":"stapling OCSP","error":"no OCSP stapling for [test.redacted.com]: no OCSP server specified in certificate","identifiers":["test.redacted.com"]}

Id’s and domains have been redacted.

3. Caddy version:

v2.10.0 h1:fonubSaQKF1YANl8TXqGcn4IbIRUDdfAkpcsfI/vX5U=

4. How I installed and ran Caddy:

FROM caddy:builder-alpine AS builder

RUN xcaddy build \
    --with github.com/caddy-dns/cloudflare \
    --with github.com/lucaslorentz/caddy-docker-proxy/v2

FROM caddy:alpine

COPY --from=builder /usr/bin/caddy /usr/bin/caddy

CMD ["caddy", "docker-proxy"]

a. System environment:

Docker

b. Command:

docker compose up

c. Service/unit/compose file:

services:
  caddy:
    build:
      context: .
    restart: unless-stopped
    environment:
      CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
      CADDY_DOCKER_CADDYFILE_PATH: /etc/caddy/Caddyfile
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./data:data
      - /var/run/docker.sock:/var/run/docker.sock
    configs:
      - source: caddyfile
        target: /etc/caddy/Caddyfile

configs:
  caddyfile:
    content: |
      {
        acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
      }

      test.redacted.com {
        respond "Hello World"
      }

networks:
  default:
    name: caddy
    external: true

Also tried with auto_https prefer_wildcard in the global.

d. My complete Caddy config:

      {
        acme_dns cloudflare {env.CLOUDFLARE_API_TOKEN}
      }

      test.redacted.com {
        respond "Hello World"
      }

You have no wildcard site in your Caddyfile.

Yes. If you have a wildcard site, Caddy will obtain a wildcard certificate and use it for all sites covered by that certificate - unless you specifically tell it not to. Since you don’t have a wildcard site, Caddy isn’t getting a wildcard certificate.

  • Wildcards used by default: Previously, Caddy would obtain individual certificates for every domain in your config literally; now wildcards, if present, will be utilized for subdomains, rather than obtaining individual certificates. This change was motivated by the novel possibility for subdomain privacy afforded by ECH. It can be overridden with tls force_automate in the Caddyfile. The experimental auto_https prefer_wildcardoption has been removed.
2 Likes

Now that Caddy reuses previously generated wildcard certificates by default, it seems the wildcard certificate example in the official documentation could be simplified. The use of a host matcher along with a handle block appears unnecessary for the purpose of reusing a wildcard certificate. It might be a good idea to update the example to reflect this, keeping the configuration more concise and focused.

2 Likes