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
+23 -12
View File
@@ -25,9 +25,9 @@ func allRendered(t *testing.T) []string {
t.Helper()
var out []string
for _, td := range []TraefikData{
{Domain: "example.com", ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"},
{Domain: "example.com", ACMEEmail: "admin@example.com"}, // email, no CF token → HTTP-01
{Domain: "example.com"}, // token-less / LAN-only
{ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"},
{ACMEEmail: "admin@example.com"}, // email, no CF token → HTTP-01
{}, // token-less / LAN-only
} {
files, err := RenderTraefik(td)
if err != nil {
@@ -64,7 +64,7 @@ func TestNoLatestTagSurvives(t *testing.T) {
}
func TestTraefikWithCloudflareToken(t *testing.T) {
files, err := RenderTraefik(TraefikData{Domain: "example.com", ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"})
files, err := RenderTraefik(TraefikData{ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"})
if err != nil {
t.Fatal(err)
}
@@ -75,9 +75,9 @@ func TestTraefikWithCloudflareToken(t *testing.T) {
if !strings.Contains(yml, "dnsChallenge") || !strings.Contains(yml, "provider: cloudflare") {
t.Error("expected Cloudflare DNS-01 challenge when CF API token set")
}
// Wildcard proactive issuance (DNS-01 path): the entrypoint must request *.<domain> + apex.
if !strings.Contains(yml, `main: "*.example.com"`) || !strings.Contains(yml, `- "example.com"`) {
t.Errorf("expected wildcard domains block (*.example.com + apex) on the DNS-01 path:\n%s", yml)
// The wildcard is NOT in traefik.yml — the entrypoint-level domains doesn't trigger issuance.
if strings.Contains(yml, "domains:") {
t.Error("traefik.yml must not carry the entrypoint domains block (proven not to issue)")
}
if strings.Contains(yml, "httpChallenge") {
t.Error("HTTP-01 must NOT appear when a CF API token is set")
@@ -121,9 +121,6 @@ func TestTraefikEmailNoCloudflareToken(t *testing.T) {
if strings.Contains(yml, "dnsChallenge") {
t.Error("DNS-01 must NOT appear without a CF token")
}
if strings.Contains(yml, "main: \"*.") {
t.Error("wildcard domains block must NOT appear on the HTTP-01 path (wildcards need DNS-01)")
}
if _, ok := files[".env"]; ok {
t.Error("no .env should be emitted without a CF API token")
}
@@ -170,7 +167,8 @@ func TestCloudflaredRender(t *testing.T) {
}
func TestControllerRoute(t *testing.T) {
r := RenderControllerRoute("demo-felhom.eu")
// Wildcard path (DNS-01 ACME): the route anchors *.<domain> + apex proactive issuance.
r := RenderControllerRoute("demo-felhom.eu", true)
if !strings.Contains(r, "Host(`felhom.demo-felhom.eu`)") {
t.Errorf("domain not wired into controller route rule: %q", r)
}
@@ -180,9 +178,22 @@ func TestControllerRoute(t *testing.T) {
if !strings.Contains(r, "websecure") {
t.Error("controller route must be on the websecure entrypoint")
}
if !strings.Contains(r, "certResolver: letsencrypt") ||
!strings.Contains(r, `main: "*.demo-felhom.eu"`) || !strings.Contains(r, `- "demo-felhom.eu"`) {
t.Errorf("wildcard issuance anchor missing on the DNS-01 controller route:\n%s", r)
}
var v any
if err := yaml.Unmarshal([]byte(r), &v); err != nil {
t.Fatalf("controller route is not valid YAML: %v\n%s", err, r)
t.Fatalf("controller route (wildcard) is not valid YAML: %v\n%s", err, r)
}
// Non-ACME path: plain TLS, no resolver/domains, still valid YAML.
plain := RenderControllerRoute("demo-felhom.eu", false)
if strings.Contains(plain, "certResolver") || strings.Contains(plain, "domains:") {
t.Errorf("non-ACME route must not carry certResolver/domains:\n%s", plain)
}
if err := yaml.Unmarshal([]byte(plain), &v); err != nil {
t.Fatalf("controller route (plain) is not valid YAML: %v\n%s", err, plain)
}
}