carlos / platform Public

edge: delegated-challenge wildcard parents — publish DNS-01 TXTs at a delegation target in our own zone

Paul Campbell a823fc66410a0e3ec0eae691ec8b1780225c75e2 parent 00c3344
2 files changed, +135 −24
  • internal/edge/wildcard.go +62 −13
  • internal/edge/wildcard_test.go +73 −11
diff --git a/internal/edge/wildcard.go b/internal/edge/wildcard.go
index 251c8e0fc..ff76decc9 100644
--- a/internal/edge/wildcard.go
+++ b/internal/edge/wildcard.go
@@ -65,6 +65,19 @@ type wildcardManager struct {
appsDomain string
hosts func() []string // live route hosts (Edge.currentHosts)
+ // delegated lists customer domains approved for delegated-challenge
+ // wildcard issuance (spec §4: claim-derived; nil = none). Their
+ // challenge TXTs publish into delegationZone — our own zone — at
+ // _acme-challenge.<parent>.<delegationZone>, which the customer has
+ // CNAMEd _acme-challenge.<parent> to. The platform never touches,
+ // or knows, the customer's DNS provider.
+ delegated func() []string
+ delegationZone string
+
+ // delegatedSet is refresh()'s snapshot of delegated(), read by
+ // challengeFQDN under mu (issuance runs on run()'s goroutine).
+ delegatedSet map[string]bool
+
mu sync.Mutex
parents []string // last computed parent set (sorted)
certs map[string]*tls.Certificate
@@ -81,17 +94,20 @@ type wildcardManager struct {
wake chan struct{}
}
-func newWildcardManager(staticDomains []string, appsDomain string, hosts func() []string, cache autocert.Cache, dns dnsProvider) *wildcardManager {
+func newWildcardManager(staticDomains []string, appsDomain string, hosts func() []string, delegated func() []string, delegationZone string, cache autocert.Cache, dns dnsProvider) *wildcardManager {
return &wildcardManager{
- cache: cache,
- dns: dns,
- directory: envOr("CARLOS_ACME_DIRECTORY", letsEncryptDirectory),
- staticDomains: staticDomains,
- appsDomain: strings.ToLower(strings.TrimSpace(appsDomain)),
- hosts: hosts,
- certs: map[string]*tls.Certificate{},
- nextAttempt: map[string]time.Time{},
- wake: make(chan struct{}, 1),
+ cache: cache,
+ dns: dns,
+ directory: envOr("CARLOS_ACME_DIRECTORY", letsEncryptDirectory),
+ staticDomains: staticDomains,
+ appsDomain: strings.ToLower(strings.TrimSpace(appsDomain)),
+ hosts: hosts,
+ delegated: delegated,
+ delegationZone: strings.ToLower(strings.TrimSpace(delegationZone)),
+ certs: map[string]*tls.Certificate{},
+ nextAttempt: map[string]time.Time{},
+ delegatedSet: map[string]bool{},
+ wake: make(chan struct{}, 1),
}
}
@@ -136,10 +152,23 @@ func parentsFor(static []string, appsDomain string, hosts []string) []string {
// still skips any parent whose nextAttempt hasn't passed, so a wake only
// advances parents that aren't already backing off.
func (w *wildcardManager) refresh() {
- parents := parentsFor(w.staticDomains, w.appsDomain, w.hosts())
+ var delegated []string
+ if w.delegated != nil {
+ for _, d := range w.delegated() {
+ if d = strings.ToLower(strings.TrimSpace(d)); d != "" {
+ delegated = append(delegated, d)
+ }
+ }
+ }
+ parents := parentsFor(append(append([]string(nil), w.staticDomains...), delegated...), w.appsDomain, w.hosts())
now := time.Now()
w.mu.Lock()
w.parents = parents
+ set := make(map[string]bool, len(delegated))
+ for _, d := range delegated {
+ set[d] = true
+ }
+ w.delegatedSet = set
need := false
for _, p := range parents {
c := w.certs[p]
@@ -244,6 +273,26 @@ func coversParent(cert *tls.Certificate, parent string) bool {
return ok && pub.Equal(cert.Leaf.PublicKey)
}
+// challengeFQDN returns where parent's DNS-01 TXT is published: the
+// delegation target inside our own zone for delegated parents, or
+// directly at _acme-challenge.<parent> otherwise. With no delegation
+// zone configured the direct form is used for everyone — the
+// misconfigured state degrades to the historical behavior and per-host
+// issuance keeps covering (obtain will fail on the CA's lookup, log,
+// and back off — never a dead edge).
+func (w *wildcardManager) challengeFQDN(parent string) string {
+ if w.delegationZone != "" && w.isDelegated(parent) {
+ return "_acme-challenge." + parent + "." + w.delegationZone
+ }
+ return "_acme-challenge." + parent
+}
+
+func (w *wildcardManager) isDelegated(parent string) bool {
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ return w.delegatedSet[parent]
+}
+
// wildcardPEMKey is the cache key for a parent's wildcard cert+key PEM —
// Eleven's file name, so a dir cache written by that lineage reads with no
// migration, and the keys sit additively beside autocert's per-host entries.
@@ -448,7 +497,7 @@ func (w *wildcardManager) solveDNS01(ctx context.Context, client *acme.Client, p
return err
}
- challengeFQDN := "_acme-challenge." + parent
+ challengeFQDN := w.challengeFQDN(parent)
cleanup, err := w.dns.present(ctx, challengeFQDN, txt)
if err != nil {
return fmt.Errorf("publish challenge TXT: %v", err)
@@ -532,7 +581,7 @@ func newWildcardFromEnv(e *Edge, cache autocert.Cache) *wildcardManager {
log.Printf("edge: wildcard certs off (%s) — per-host issuance continues", reason)
return nil
}
- return newWildcardManager(static, appsDomain, e.currentHosts, cache, dns)
+ return newWildcardManager(static, appsDomain, e.currentHosts, nil, os.Getenv("CARLOS_ACME_DELEGATION_ZONE"), cache, dns)
}
// wrapGetCertificate serves the wildcard for covered names and hands
diff --git a/internal/edge/wildcard_test.go b/internal/edge/wildcard_test.go
index 4925d6918..73c607fe5 100644
--- a/internal/edge/wildcard_test.go
+++ b/internal/edge/wildcard_test.go
@@ -73,7 +73,7 @@ func TestParentsFor(t *testing.T) {
func TestCertForMatchesExactlyOneLabel(t *testing.T) {
w := newWildcardManager([]string{"go.tito.io"}, "oncarlos.com",
func() []string { return []string{"kass.abs.oncarlos.com"} },
- autocert.DirCache(t.TempDir()), nil)
+ nil, "", autocert.DirCache(t.TempDir()), nil)
w.refresh()
cert, _ := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(60*24*time.Hour))
w.mu.Lock()
@@ -124,7 +124,7 @@ func TestWildcardPEMRoundTripAndRenewDecision(t *testing.T) {
t.Fatal("parseWildcardPEM(junk): want error")
}
- w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, autocert.DirCache(t.TempDir()), nil)
+ w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, nil, "", autocert.DirCache(t.TempDir()), nil)
if !w.needsObtain("abs.oncarlos.com", time.Now()) {
t.Fatal("no cert loaded: needsObtain should be true")
}
@@ -152,7 +152,7 @@ func TestLoadCachedAndRefreshWake(t *testing.T) {
}
w := newWildcardManager(nil, "oncarlos.com",
- func() []string { return []string{"kass.abs.oncarlos.com"} }, cache, nil)
+ func() []string { return []string{"kass.abs.oncarlos.com"} }, nil, "", cache, nil)
if !w.loadCached(context.Background(), "abs.oncarlos.com") {
t.Fatal("loadCached: want true for cached parent")
}
@@ -169,7 +169,7 @@ func TestLoadCachedAndRefreshWake(t *testing.T) {
default:
}
w2 := newWildcardManager(nil, "oncarlos.com",
- func() []string { return []string{"kass.abs.oncarlos.com"} }, autocert.DirCache(t.TempDir()), nil)
+ func() []string { return []string{"kass.abs.oncarlos.com"} }, nil, "", autocert.DirCache(t.TempDir()), nil)
w2.refresh()
select {
case <-w2.wake:
@@ -190,7 +190,7 @@ func TestLoadCachedRejectsWrongCoverage(t *testing.T) {
if err := cache.Put(context.Background(), wildcardPEMKey("abs.oncarlos.com"), rawWrongParent); err != nil {
t.Fatal(err)
}
- w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, cache, nil)
+ w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, nil, "", cache, nil)
if w.loadCached(context.Background(), "abs.oncarlos.com") {
t.Fatal("loadCached: want false for cert covering the wrong parent")
}
@@ -215,7 +215,7 @@ func TestLoadCachedRejectsWrongCoverage(t *testing.T) {
if err := cache2.Put(context.Background(), wildcardPEMKey("abs.oncarlos.com"), rawMismatch); err != nil {
t.Fatal(err)
}
- w2 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, cache2, nil)
+ w2 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, nil, "", cache2, nil)
if w2.loadCached(context.Background(), "abs.oncarlos.com") {
t.Fatal("loadCached: want false for key/leaf mismatch")
}
@@ -248,12 +248,12 @@ func (f *fakeDNS) present(_ context.Context, fqdn, value string) (func(context.C
func TestWildcardAccountKeyStableAcrossManagers(t *testing.T) {
dir := t.TempDir()
cache := autocert.DirCache(dir)
- w1 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, cache, &fakeDNS{})
+ w1 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, nil, "", cache, &fakeDNS{})
k1, err := w1.accountKey(context.Background())
if err != nil {
t.Fatal(err)
}
- w2 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, cache, &fakeDNS{})
+ w2 := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, nil, "", cache, &fakeDNS{})
k2, err := w2.accountKey(context.Background())
if err != nil {
t.Fatal(err)
@@ -274,7 +274,7 @@ func (f faultyCache) Get(context.Context, string) ([]byte, error) {
func TestAccountKeyBackendFaultDoesNotMint(t *testing.T) {
w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil },
- faultyCache{autocert.DirCache(t.TempDir())}, &fakeDNS{})
+ nil, "", faultyCache{autocert.DirCache(t.TempDir())}, &fakeDNS{})
if _, err := w.accountKey(context.Background()); err == nil {
t.Fatal("backend fault: want error, got a (freshly minted) key")
}
@@ -286,7 +286,7 @@ func TestAccountKeyBackendFaultDoesNotMint(t *testing.T) {
// (delete) clears the entry entirely.
func TestPerParentBackoff(t *testing.T) {
w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil },
- autocert.DirCache(t.TempDir()), &fakeDNS{})
+ nil, "", autocert.DirCache(t.TempDir()), &fakeDNS{})
if w.backingOff("abs.oncarlos.com", time.Now()) {
t.Fatal("no nextAttempt recorded yet: backingOff should be false")
@@ -321,7 +321,7 @@ func TestPerParentBackoff(t *testing.T) {
func TestWrappedGetCertificateFallsThrough(t *testing.T) {
w := newWildcardManager(nil, "oncarlos.com",
func() []string { return []string{"kass.abs.oncarlos.com"} },
- autocert.DirCache(t.TempDir()), &fakeDNS{})
+ nil, "", autocert.DirCache(t.TempDir()), &fakeDNS{})
w.refresh()
cert, _ := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(60*24*time.Hour))
w.mu.Lock()
@@ -418,3 +418,65 @@ func TestCurrentHostsPlatformOnly(t *testing.T) {
t.Fatalf("currentHosts = %v, want %v", got, want)
}
}
+
+func TestDelegatedParentsJoinTheSet(t *testing.T) {
+ w := newWildcardManager(nil, "oncarlos.com", func() []string {
+ return []string{"app.bab.oncarlos.com"}
+ }, func() []string {
+ return []string{"woodstar.app"}
+ }, "acme.oncarlos.com", autocert.DirCache(t.TempDir()), &fakeDNS{})
+ w.refresh()
+ w.mu.Lock()
+ parents := append([]string(nil), w.parents...)
+ w.mu.Unlock()
+ want := []string{"bab.oncarlos.com", "woodstar.app"}
+ if !reflect.DeepEqual(parents, want) {
+ t.Fatalf("parents = %v, want %v", parents, want)
+ }
+}
+
+func TestChallengeFQDNDelegation(t *testing.T) {
+ w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil },
+ func() []string { return []string{"woodstar.app"} },
+ "acme.oncarlos.com", autocert.DirCache(t.TempDir()), &fakeDNS{})
+ w.refresh()
+ // A delegated parent's TXT publishes into OUR zone, at the target
+ // the customer's CNAME points to.
+ if got, want := w.challengeFQDN("woodstar.app"), "_acme-challenge.woodstar.app.acme.oncarlos.com"; got != want {
+ t.Errorf("delegated: %q, want %q", got, want)
+ }
+ // A platform-zone parent publishes directly, exactly as before.
+ if got, want := w.challengeFQDN("bab.oncarlos.com"), "_acme-challenge.bab.oncarlos.com"; got != want {
+ t.Errorf("own-zone: %q, want %q", got, want)
+ }
+}
+
+func TestChallengeFQDNWithoutZoneNeverDelegates(t *testing.T) {
+ // No delegation zone configured: even a listed delegated parent
+ // publishes directly (misconfiguration degrades to the historical
+ // behavior, which per-host issuance then covers — never a dead edge).
+ w := newWildcardManager(nil, "", func() []string { return nil },
+ func() []string { return []string{"woodstar.app"} },
+ "", autocert.DirCache(t.TempDir()), &fakeDNS{})
+ w.refresh()
+ if got, want := w.challengeFQDN("woodstar.app"), "_acme-challenge.woodstar.app"; got != want {
+ t.Errorf("%q, want %q", got, want)
+ }
+}
+
+func TestNilDelegatedSourceIsFine(t *testing.T) {
+ // Every existing construction site passes nil — the zero-source
+ // manager must behave exactly as before this feature existed.
+ w := newWildcardManager([]string{"go.tito.io"}, "", func() []string { return nil },
+ nil, "acme.oncarlos.com", autocert.DirCache(t.TempDir()), &fakeDNS{})
+ w.refresh()
+ w.mu.Lock()
+ parents := append([]string(nil), w.parents...)
+ w.mu.Unlock()
+ if !reflect.DeepEqual(parents, []string{"go.tito.io"}) {
+ t.Fatalf("parents = %v, want [go.tito.io]", parents)
+ }
+ if got, want := w.challengeFQDN("go.tito.io"), "_acme-challenge.go.tito.io"; got != want {
+ t.Errorf("%q, want %q", got, want)
+ }
+}