v0.42.0: real Let's Encrypt cert via wildcard proactive issuance

traefik's websecure entrypoint now declares http.tls.domains *.<domain>+apex so
it proactively obtains the wildcard via Cloudflare DNS-01 at startup (cert ready
before first client, every router serves it by SNI). Gated on CFAPIToken (DNS-01).
TraefikData gains Domain; ensureTraefik wires cfg.Customer.Domain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-11 17:48:15 +02:00
parent 80216e6ce5
commit 84c3e84641
5 changed files with 42 additions and 5 deletions
+4 -1
View File
@@ -40,8 +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.
// (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.
type TraefikData struct {
Domain string
ACMEEmail string
CFAPIToken string
}
+11 -4
View File
@@ -25,9 +25,9 @@ func allRendered(t *testing.T) []string {
t.Helper()
var out []string
for _, td := range []TraefikData{
{ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"},
{ACMEEmail: "admin@example.com"}, // email, no CF token → HTTP-01
{}, // token-less / LAN-only
{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
} {
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{ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"})
files, err := RenderTraefik(TraefikData{Domain: "example.com", ACMEEmail: "admin@example.com", CFAPIToken: "cf-api-tok"})
if err != nil {
t.Fatal(err)
}
@@ -75,6 +75,10 @@ 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)
}
if strings.Contains(yml, "httpChallenge") {
t.Error("HTTP-01 must NOT appear when a CF API token is set")
}
@@ -117,6 +121,9 @@ 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")
}
@@ -19,6 +19,15 @@ entryPoints:
http:
tls:
certResolver: letsencrypt
{{- if .CFAPIToken}}
# Wildcard proactive issuance (DNS-01 only — HTTP-01 can't do wildcards): traefik obtains
# *.<domain> (+ apex) at startup, so every router serves the real cert by SNI match with no
# per-app labels and the cert is ready before the first client connects.
domains:
- main: "*.{{.Domain}}"
sans:
- "{{.Domain}}"
{{- end}}
{{- end}}
providers:
+1
View File
@@ -92,6 +92,7 @@ func (m *Manager) ensureTraefik(dir string) error {
return fmt.Errorf("chmod acme.json: %w", err)
}
files, err := infra.RenderTraefik(infra.TraefikData{
Domain: m.cfg.Customer.Domain,
ACMEEmail: m.cfg.Customer.Email,
CFAPIToken: m.cfg.Infrastructure.CFAPIToken,
})