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:
@@ -1,5 +1,22 @@
|
||||
## Changelog
|
||||
|
||||
### v0.42.0 — real Let's Encrypt cert: wildcard proactive issuance (2026-06-11)
|
||||
|
||||
The base-infra traefik obtained **no** real cert (acme.json empty) — both routers relied on the
|
||||
websecure entrypoint-default `certResolver`, which does not trigger proactive DNS-01 issuance, so
|
||||
everything ran on traefik's self-signed default (masked externally by the tunnel's `noTLSVerify`).
|
||||
This blocks LAN-direct (a LAN client TLS-handshakes straight to traefik and needs the real cert).
|
||||
|
||||
- **`internal/infra/templates/traefik.yml.tmpl`** — the websecure entrypoint's `http.tls` now declares
|
||||
`domains: [{main: "*.<domain>", sans: ["<domain>"]}]` so traefik **proactively obtains the wildcard
|
||||
`*.<domain>` + apex at startup** (via Cloudflare DNS-01). Every router then serves the real cert by
|
||||
SNI match — no per-app `certresolver` labels to forget, cert ready before the first client connects.
|
||||
Gated on `.CFAPIToken` (wildcards require DNS-01; HTTP-01 can't issue them).
|
||||
- **`infra.TraefikData`** gains a `Domain` field; **`stacks.ensureTraefik`** now wires
|
||||
`Domain: cfg.Customer.Domain` into `RenderTraefik` (previously unset).
|
||||
- Validated staging→prod on guest 9201 (Fake LE → real LE), then GATE: `felhom.<domain>` +
|
||||
`files.<domain>` return `200 0` (real cert, TLS verify OK) from a real LAN host.
|
||||
|
||||
### v0.41.2 — fix controller-route auto-connect + dead dashboard cross-drive block (2026-06-11)
|
||||
|
||||
Two fixes found while live-validating v0.41.1 routing on guest 9201:
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user