Want to HTTPS an unknown set of subdomains without doing the DNS challenge required for wildcard certs?

Use on-demand TLS instead. It still has a bit of a challenge involved to prevent abuse though: it’ll call out to an HTTP endpoint to ask, “should I get a cert for this domain?”

You tell it where to send that query with the ask directive of the global on_demand_tls configuration block at the top of your Caddyfile:

{
	on_demand_tls {
		ask http://example.com
	}
}

This won’t change how any existing sites get their certs. It only comes into play when a site handler explicitly enables on_demand within its tls block:

example.com, *.example.com {
    tls {
        on_demand
        issuer acme {
            email ops@example.com
        }
    }
}

When Caddy receives a request to some random subdomain defined within that site block (subdomain.example.com), it will send an HTTP GET to the configured ask endpoint, like http://example.com/?domain=subdomain.example.com. If it gets a 200 success code back, it will go ahead and obtain the certificate; otherwise, it will not.

I think the intent is to probably have some sort of API or database or other such logic to keep track of which subdomains are permitted, but I don’t feel like standing up additional infrastructure. I also don’t want to point it at a URL that will just automatically return a 200 for any domain.

So I tweaked my site block to handle that for me.

The @tls_allow matcher return a 200 for requests for subdomains below the configured domain so that Caddy will go ahead and fetch those certs for me. @tls_deny returns a 403 for any other domain validation requests. And httpredir (without a domain query) will redirect to HTTPS so that we don’t get stuck in plaintext hell.

example.com, *.example.com, http://example.com {
	@tls_allow `protocol('http') && method('GET') && {query.domain}.endsWith("example.com")`
	@tls_deny `protocol('http') && method('GET') && ({query.domain}.size() > 0 && !{query.domain}.endsWith("example.com"))`
	@httpredir `protocol('http') && {query.domain}.size() == 0`
	handle @tls_allow {
		respond 200
	}
	handle @tls_deny {
		respond 403
	}
	handle @httpredir {
		redir https://{host}{uri}
	}
	handle {
		reverse_proxy http://localhost:8080
	}
	tls {
		on_demand
		issuer acme {
			email ops@example.com
		}
	}
}