v0.42.1: wildcard cert via controller route (entrypoint domains don't issue)

Empirically (staging on 9201): traefik v3 issues a cert from a router-level
tls.domains but NOT from the entrypoint http.tls.domains. So the wildcard moves
to RenderControllerRoute (the always-present anchor): when DNS-01 ACME is
configured it carries tls.certResolver+domains *.<domain>+apex, and every other
router serves that wildcard by SNI (no per-app labels). Reverts v0.42.0's dead
entrypoint-domains + TraefikData.Domain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 18:04:39 +02:00
parent 84c3e84641
commit e61e7dd8fc
5 changed files with 66 additions and 44 deletions
+27 -11
View File
@@ -40,11 +40,11 @@ type FileSpec struct {
}
// TraefikData is the per-customer input for the traefik stack. ACMEEmail empty → no Let's Encrypt
// (traefik serves self-signed); CFAPIToken empty → HTTP-01 instead of Cloudflare DNS-01, and no .env
// (and no wildcard — HTTP-01 can't issue wildcards). Domain drives the wildcard proactive-issuance
// SAN (`*.<Domain>` + apex) when DNS-01 is in use.
// (traefik serves self-signed); CFAPIToken empty → HTTP-01 instead of Cloudflare DNS-01, and no .env.
// (Wildcard proactive issuance is driven by the controller route, NOT here — see RenderControllerRoute:
// the entrypoint-level `http.tls.domains` does NOT trigger issuance in traefik v3, a router-level
// `tls.domains` does.)
type TraefikData struct {
Domain string
ACMEEmail string
CFAPIToken string
}
@@ -167,10 +167,27 @@ networks:
// RenderControllerRoute returns a traefik file-provider dynamic config routing the controller's own
// dashboard — Host(felhom.<domain>) → http://felhom-controller:8080 on websecure. This can only be
// produced POST config-pull (the v2 bootstrap.json carries no domain), which is why the controller
// wires its OWN route at bring-up instead of via a static Docker label at bootstrap time. `tls: {}`
// inherits the websecure entrypoint's default certResolver (letsencrypt) when ACME is configured, and
// otherwise falls back to traefik's default self-signed cert.
func RenderControllerRoute(domain string) string {
// wires its OWN route at bring-up instead of via a static Docker label at bootstrap time.
//
// When wildcardTLS is true (DNS-01 ACME configured = CF API token + email), this route is ALSO the
// **wildcard-issuance anchor**: its router-level `tls.domains` makes traefik proactively obtain
// `*.<domain>` + apex via Cloudflare DNS-01 at startup. Every other router (filebrowser, future apps)
// then serves that one wildcard by SNI match — no per-app certresolver labels, real cert before the
// first client connects. (Empirically, traefik v3 issues from a router-level `tls.domains` but NOT
// from the entrypoint-level `http.tls.domains` — hence this lives here, not in traefik.yml.)
// When wildcardTLS is false (no DNS-01: HTTP-01 or no ACME — wildcards need DNS-01), it emits a plain
// TLS router (traefik's self-signed default until/unless a cert exists).
func RenderControllerRoute(domain string, wildcardTLS bool) string {
tlsBlock := " tls: {}\n"
if wildcardTLS {
tlsBlock = fmt.Sprintf(` tls:
certResolver: letsencrypt
domains:
- main: "*.%s"
sans:
- "%s"
`, domain, domain)
}
return fmt.Sprintf(`# Traefik dynamic route for the felhom-controller dashboard — managed by felhom-controller.
# WARNING: auto-generated at base-infra bring-up. Manual edits are overwritten.
http:
@@ -180,13 +197,12 @@ http:
entryPoints:
- websecure
service: felhom-controller
tls: {}
services:
%s services:
felhom-controller:
loadBalancer:
servers:
- url: "http://felhom-controller:8080"
`, domain)
`, domain, tlsBlock)
}
// RenderFileBrowserConfig returns a FileBrowser Quantum config.yaml with one source per registered