domains: cert-expiry wildcard fallback covers static + delegated parents
hostCertExpiry's fallback now strips a generic one label off host to find its covering wildcard-<parent>.pem, instead of gating on the appsDomain shape via wildcardParent. Closes the pre-existing hole where a host under a static parent (go.tito.io) or a delegated customer domain (woodstar.app) reported certless while the edge served a valid wildcard. wildcardParent existed solely to compute the parent, so it's deleted along with TestWildcardParent; the apex-non-match and appsDomain-derived behaviors are covered by TestCertExpiryWildcardFallback.
2 files changed,
+35
−51
internal/domains/reconcile.go+21 −32internal/domains/reconcile_internal_test.go+14 −19
diff --git a/internal/domains/reconcile.go b/internal/domains/reconcile.go| index c5d4f72f5..235b09a5e 100644 |
| --- a/internal/domains/reconcile.go |
| +++ b/internal/domains/reconcile.go |
| @@ -721,42 +721,31 @@ func mintedRoute(host, app, account string, src registry.Route) registry.Route { |
| return rt |
| } |
| -// wildcardParent returns the parent domain whose per-account wildcard cert |
| -// would cover host — host exactly two labels under appsDomain |
| -// (<app>.<acct>.<appsDomain>) → <acct>.<appsDomain> — or "" when no wildcard |
| -// could cover it (canary depth, a parent apex, a foreign domain, no apps |
| -// domain configured). |
| -func wildcardParent(host, appsDomain string) string { |
| - if appsDomain == "" { |
| - return "" |
| - } |
| - suffix := "." + strings.ToLower(appsDomain) |
| - rest, ok := strings.CutSuffix(strings.ToLower(host), suffix) |
| - if !ok { |
| - return "" |
| - } |
| - if labels := strings.Split(rest, "."); len(labels) == 2 { |
| - return labels[1] + suffix |
| - } |
| - return "" |
| -} |
| - |
| // hostCertExpiry resolves host's cert expiry from the acme cache dir as the |
| -// LATER of the per-host autocert entry and the covering per-account |
| -// wildcard's PEM (wildcard-<parent>.pem). Two reasons a bare per-host lookup |
| -// isn't enough: a wildcard-covered host never gets a per-host entry at all |
| -// (without the wildcard fallback status would call it certless forever), and |
| -// a host GRANDFATHERED onto a per-host cert from before its wildcard existed |
| -// keeps that file on disk forever — autocert stops renewing it once the |
| -// wildcard starts serving the host, so the stale file's expiry would |
| -// shadow the wildcard's and status would cry "expiring" over a cert the edge |
| -// no longer serves. The edge always serves whichever cert is actually valid, |
| -// so the max of the two is the only truthful answer. 0 = unknown, never a |
| -// guess. |
| +// LATER of the per-host autocert entry and the covering wildcard's PEM |
| +// (wildcard-<parent>.pem). Two reasons a bare per-host lookup isn't enough: a |
| +// wildcard-covered host never gets a per-host entry at all (without the |
| +// wildcard fallback status would call it certless forever), and a host |
| +// GRANDFATHERED onto a per-host cert from before its wildcard existed keeps |
| +// that file on disk forever — autocert stops renewing it once the wildcard |
| +// starts serving the host, so the stale file's expiry would shadow the |
| +// wildcard's and status would cry "expiring" over a cert the edge no longer |
| +// serves. The edge always serves whichever cert is actually valid, so the max |
| +// of the two is the only truthful answer. 0 = unknown, never a guess. |
| +// |
| +// The wildcard parent is host's generic one-label strip, not an |
| +// appsDomain-shaped derivation (appsDomain is otherwise unused here): one |
| +// label folds into "*.", so any host's covering wildcard is its one-label |
| +// parent's, whether that parent is an <acct>.<apps-domain> derivation, a |
| +// static domain (go.tito.io), or a delegated customer domain (woodstar.app). |
| +// Trying the read is cheaper than knowing which — a miss is a miss. An apex |
| +// host's own strip lands one label short of the parent that names ITS |
| +// covering wildcard file, so the apex never matches its own wildcard. |
| func hostCertExpiry(acmeDir, host, appsDomain string) int64 { |
| perHost := certExpiry(filepath.Join(acmeDir, host)) |
| wildcard := int64(0) |
| - if parent := wildcardParent(host, appsDomain); parent != "" { |
| + if i := strings.IndexByte(host, '.'); i > 0 { |
| + parent := strings.ToLower(host[i+1:]) |
| wildcard = certExpiry(filepath.Join(acmeDir, "wildcard-"+parent+".pem")) |
| } |
| if wildcard > perHost { |
diff --git a/internal/domains/reconcile_internal_test.go b/internal/domains/reconcile_internal_test.go| index a354f3d1c..4075fd2a9 100644 |
| --- a/internal/domains/reconcile_internal_test.go |
| +++ b/internal/domains/reconcile_internal_test.go |
| @@ -1,6 +1,6 @@ |
| package domains |
| -// Internal-package tests for wildcardParent/hostCertExpiry: reconcile_test.go |
| +// Internal-package tests for hostCertExpiry: reconcile_test.go |
| // is package domains_test and cannot reach these unexported helpers, so this |
| // file follows slugname_test.go's existing convention of a same-package test |
| // file living alongside it. |
| @@ -58,24 +58,6 @@ func writeWildcardPEM(t *testing.T, path, name string, notAfter time.Time) { |
| } |
| } |
| -func TestWildcardParent(t *testing.T) { |
| - cases := []struct { |
| - host, appsDomain, want string |
| - }{ |
| - {"kass.abs.oncarlos.com", "oncarlos.com", "abs.oncarlos.com"}, |
| - {"canary.kass.abs.oncarlos.com", "oncarlos.com", ""}, // canary depth: not covered |
| - {"abs.oncarlos.com", "oncarlos.com", ""}, // apex of a parent |
| - {"custom.example.com", "oncarlos.com", ""}, // foreign domain |
| - {"kass.abs.oncarlos.com", "", ""}, // no apps domain configured |
| - {"Kass.ABS.oncarlos.com", "oncarlos.com", "abs.oncarlos.com"}, |
| - } |
| - for _, c := range cases { |
| - if got := wildcardParent(c.host, c.appsDomain); got != c.want { |
| - t.Errorf("wildcardParent(%q, %q) = %q, want %q", c.host, c.appsDomain, got, c.want) |
| - } |
| - } |
| -} |
| - |
| // A wildcard-covered host has no per-host acme cache entry — its expiry must |
| // come from the covering wildcard-<parent>.pem instead of reporting 0 |
| // (certless) forever. |
| @@ -113,6 +95,19 @@ func TestCertExpiryWildcardFallback(t *testing.T) { |
| if got := hostCertExpiry(dir, "kass.zzz.oncarlos.com", "oncarlos.com"); got != 0 { |
| t.Fatalf("hostCertExpiry with no files = %d, want 0", got) |
| } |
| + |
| + // A host under a NON-apps-domain wildcard parent (static go.tito.io / |
| + // delegated woodstar.app): the fallback must strip one label and try |
| + // the parent's wildcard PEM, not require the apps-domain shape. |
| + delegatedNotAfter := time.Now().Add(60 * 24 * time.Hour).Truncate(time.Second) |
| + writeWildcardPEM(t, filepath.Join(dir, "wildcard-woodstar.app.pem"), "*.woodstar.app", delegatedNotAfter) |
| + if got := hostCertExpiry(dir, "paul.woodstar.app", "oncarlos.com"); got != delegatedNotAfter.Unix() { |
| + t.Fatalf("delegated-parent fallback = %d, want %d", got, delegatedNotAfter.Unix()) |
| + } |
| + // The apex itself must NOT match its own wildcard. |
| + if got := hostCertExpiry(dir, "woodstar.app", "oncarlos.com"); got != 0 { |
| + t.Fatalf("apex must not claim the wildcard: got %d, want 0", got) |
| + } |
| } |
| // TestMintedRouteCarriesRegionAndDropsSocketWhenFleetPlaced: a minted |