carlos / platform Public

edge: wildcardManager state — dynamic parents, SNI matching, PEM cache

Per-account parents derived from the route table (app.acct.domain →
acct.domain; canary depth and apex excluded), static parents from
CARLOS_WILDCARD_DOMAINS, refresh() wakes issuance when a parent has no
usable cert. certFor serves exactly-one-label matches and never a dead
cert.
Paul Campbell a3063a2cc7b1ca222de2fc4dcdfd99cb06e443d8 parent c38835e
2 files changed, +430 −0
  • internal/edge/wildcard.go +254 −0
  • internal/edge/wildcard_test.go +176 −0
diff --git a/internal/edge/wildcard.go b/internal/edge/wildcard.go
new file mode 100644
index 000000000..5bc8d5326
--- /dev/null
+++ b/internal/edge/wildcard.go
@@ -0,0 +1,254 @@
+package edge
+
+// Wildcard TLS (ACME DNS-01) for instant app hostnames — the port of
+// Eleven's wildcard.go this file's TLS wiring always reserved space for
+// (spec: docs/superpowers/specs/2026-08-05-wildcard-certs-design.md).
+//
+// A TLS wildcard matches exactly one label, and app hosts are two labels
+// under the apps domain (<app>.<acct>.<apps-domain>), so there is no single
+// *.<apps-domain> cert. Instead one wildcard per PARENT:
+//
+// - dynamic parents <acct>.<apps-domain>, derived from the live route
+// table under PLATFORM_APPS_DOMAIN — a new account's first route starts
+// issuance immediately, so every later app under it has working TLS the
+// moment its route exists (and CT logs see the account, not app names);
+// - static parents from CARLOS_WILDCARD_DOMAINS (tito: go.tito.io, whose
+// instance hosts are one label deep).
+//
+// Canary hosts (<name>.<app>.<acct>.…, one label too deep) and custom
+// domains stay per-host HTTP-01. The wildcard only changes how a handshake
+// GETS a certificate: routing, HostPolicy and the :80 handler are
+// untouched, and every failure (DNS provider outage, ACME outage,
+// unreadable cache) degrades to exactly the per-host behaviour — never a
+// dead edge.
+
+import (
+ "bytes"
+ "context"
+ "crypto/ecdsa"
+ "crypto/tls"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "log"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/crypto/acme/autocert"
+)
+
+const letsEncryptDirectory = "https://acme-v02.api.letsencrypt.org/directory"
+
+// wildcardRenewBefore is how close to expiry we re-issue. LE certs live 90
+// days; renewing with 30 left tolerates a month of provider outage.
+const wildcardRenewBefore = 30 * 24 * time.Hour
+
+type wildcardManager struct {
+ // cache is the shared cert cache (certcache.go) — the same backend
+ // autocert uses for per-host certs, so a future pooled-edge S3 backend
+ // shares wildcards too. Keys: wildcardPEMKey / wildcardAccountKeyName.
+ cache autocert.Cache
+ dns dnsProvider
+ directory string
+
+ staticDomains []string
+ appsDomain string
+ hosts func() []string // live route hosts (Edge.currentHosts)
+
+ mu sync.Mutex
+ parents []string // last computed parent set (sorted)
+ certs map[string]*tls.Certificate
+
+ // wake nudges run() out of its 12h sleep when refresh() finds a parent
+ // with no usable cert (a new account's first route). Capacity 1: a wake
+ // is a level, not a count.
+ wake chan struct{}
+}
+
+func newWildcardManager(staticDomains []string, appsDomain string, hosts func() []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{},
+ wake: make(chan struct{}, 1),
+ }
+}
+
+// parentsFor computes the wildcard parent set: the static domains plus
+// <acct>.<appsDomain> for every route host exactly two labels under
+// appsDomain (<app>.<acct>.<appsDomain>). Canary hosts (three labels), the
+// apex, and foreign domains contribute nothing. Sorted and deduped so the
+// set is deterministic for logs and tests.
+func parentsFor(static []string, appsDomain string, hosts []string) []string {
+ seen := map[string]bool{}
+ var out []string
+ add := func(p string) {
+ if !seen[p] {
+ seen[p] = true
+ out = append(out, p)
+ }
+ }
+ for _, d := range static {
+ add(strings.ToLower(d))
+ }
+ if appsDomain != "" {
+ suffix := "." + strings.ToLower(appsDomain)
+ for _, h := range hosts {
+ rest, ok := strings.CutSuffix(strings.ToLower(h), suffix)
+ if !ok {
+ continue
+ }
+ if labels := strings.Split(rest, "."); len(labels) == 2 {
+ add(labels[1] + suffix)
+ }
+ }
+ }
+ sort.Strings(out)
+ return out
+}
+
+// refresh recomputes the parent set from the live route table (called from
+// the reload hook on every route swap, and once at startup). If any parent
+// lacks a usable cert it wakes run() — that is what makes a NEW account's
+// wildcard start issuing the moment its first route lands, instead of on
+// the next 12h tick.
+func (w *wildcardManager) refresh() {
+ parents := parentsFor(w.staticDomains, w.appsDomain, w.hosts())
+ now := time.Now()
+ w.mu.Lock()
+ w.parents = parents
+ need := false
+ for _, p := range parents {
+ c := w.certs[p]
+ if c == nil || now.Add(wildcardRenewBefore).After(c.Leaf.NotAfter) {
+ need = true
+ break
+ }
+ }
+ w.mu.Unlock()
+ if need {
+ select {
+ case w.wake <- struct{}{}:
+ default:
+ }
+ }
+}
+
+// certFor returns the wildcard cert for serverName, or nil when the name
+// isn't exactly one label under a current parent (a wildcard doesn't cover
+// the apex or deeper levels) or no usable cert is loaded — nil sends the
+// handshake to the per-host autocert path.
+func (w *wildcardManager) certFor(serverName string) *tls.Certificate {
+ sn := strings.ToLower(strings.TrimSuffix(serverName, "."))
+ w.mu.Lock()
+ defer w.mu.Unlock()
+ for _, p := range w.parents {
+ sub, ok := strings.CutSuffix(sn, "."+p)
+ if !ok || sub == "" || strings.Contains(sub, ".") {
+ continue
+ }
+ cert := w.certs[p]
+ if cert == nil || time.Now().After(cert.Leaf.NotAfter) {
+ // Never serve a dead cert: a wildcard whose renewal has been
+ // broken past its actual expiry falls back to per-host issuance
+ // (the pre-wildcard behaviour) instead of hard-failing every
+ // handshake under the parent.
+ return nil
+ }
+ return cert
+ }
+ return nil
+}
+
+func (w *wildcardManager) needsObtain(parent string, now time.Time) bool {
+ w.mu.Lock()
+ cert := w.certs[parent]
+ w.mu.Unlock()
+ return cert == nil || now.Add(wildcardRenewBefore).After(cert.Leaf.NotAfter)
+}
+
+// loadCached pulls parent's wildcard from the cert cache into memory,
+// reporting whether a parseable cert was loaded. Called by run() before any
+// obtain attempt, which covers both startup (certs an earlier process
+// obtained) and a pooled-edge future (certs a sibling box obtained).
+func (w *wildcardManager) loadCached(ctx context.Context, parent string) bool {
+ raw, err := w.cache.Get(ctx, wildcardPEMKey(parent))
+ if errors.Is(err, autocert.ErrCacheMiss) {
+ return false
+ }
+ if err == nil {
+ var cert *tls.Certificate
+ if cert, err = parseWildcardPEM(raw); err == nil {
+ w.mu.Lock()
+ w.certs[parent] = cert
+ w.mu.Unlock()
+ log.Printf("edge: wildcard *.%s loaded from cache (valid until %s)", parent, cert.Leaf.NotAfter.Format(time.RFC3339))
+ return true
+ }
+ }
+ log.Printf("edge: wildcard *.%s: cached cert unreadable (%v) — will re-obtain", parent, err)
+ return false
+}
+
+// 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.
+func wildcardPEMKey(domain string) string {
+ return "wildcard-" + domain + ".pem"
+}
+
+// ---- PEM encode/parse (key + chain in one blob; the cert cache owns
+// placement, permissions, and write atomicity) -------------------------------
+
+func encodeWildcardPEM(cert *tls.Certificate, key *ecdsa.PrivateKey) ([]byte, error) {
+ var buf bytes.Buffer
+ der, err := x509.MarshalECPrivateKey(key)
+ if err != nil {
+ return nil, err
+ }
+ if err := pem.Encode(&buf, &pem.Block{Type: "EC PRIVATE KEY", Bytes: der}); err != nil {
+ return nil, err
+ }
+ for _, c := range cert.Certificate {
+ if err := pem.Encode(&buf, &pem.Block{Type: "CERTIFICATE", Bytes: c}); err != nil {
+ return nil, err
+ }
+ }
+ return buf.Bytes(), nil
+}
+
+func parseWildcardPEM(raw []byte) (*tls.Certificate, error) {
+ cert := &tls.Certificate{}
+ for {
+ var block *pem.Block
+ block, raw = pem.Decode(raw)
+ if block == nil {
+ break
+ }
+ switch block.Type {
+ case "EC PRIVATE KEY":
+ key, err := x509.ParseECPrivateKey(block.Bytes)
+ if err != nil {
+ return nil, err
+ }
+ cert.PrivateKey = key
+ case "CERTIFICATE":
+ cert.Certificate = append(cert.Certificate, block.Bytes)
+ }
+ }
+ if cert.PrivateKey == nil || len(cert.Certificate) == 0 {
+ return nil, errors.New("missing key or certificate in PEM")
+ }
+ leaf, err := x509.ParseCertificate(cert.Certificate[0])
+ if err != nil {
+ return nil, err
+ }
+ cert.Leaf = leaf
+ return cert, nil
+}
diff --git a/internal/edge/wildcard_test.go b/internal/edge/wildcard_test.go
new file mode 100644
index 000000000..e983e4de5
--- /dev/null
+++ b/internal/edge/wildcard_test.go
@@ -0,0 +1,176 @@
+package edge
+
+import (
+ "context"
+ "crypto/ecdsa"
+ "crypto/elliptic"
+ "crypto/rand"
+ "crypto/tls"
+ "crypto/x509"
+ "crypto/x509/pkix"
+ "math/big"
+ "reflect"
+ "testing"
+ "time"
+
+ "golang.org/x/crypto/acme/autocert"
+)
+
+// selfSignedWildcard mints a throwaway *.parent cert valid for the given
+// window — enough for matching/renewal/PEM tests, no ACME involved.
+func selfSignedWildcard(t *testing.T, parent string, notAfter time.Time) (*tls.Certificate, *ecdsa.PrivateKey) {
+ t.Helper()
+ key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+ if err != nil {
+ t.Fatal(err)
+ }
+ tmpl := &x509.Certificate{
+ SerialNumber: big.NewInt(1),
+ Subject: pkix.Name{CommonName: "*." + parent},
+ DNSNames: []string{"*." + parent},
+ NotBefore: time.Now().Add(-time.Hour),
+ NotAfter: notAfter,
+ }
+ der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ leaf, err := x509.ParseCertificate(der)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return &tls.Certificate{Certificate: [][]byte{der}, PrivateKey: key, Leaf: leaf}, key
+}
+
+func TestParentsFor(t *testing.T) {
+ hosts := []string{
+ "kass.abs.oncarlos.com", // app host → parent abs.oncarlos.com
+ "blog.abs.oncarlos.com", // same parent, deduped
+ "echo.bab.oncarlos.com", // second account
+ "canary.kass.abs.oncarlos.com", // canary depth → no wildcard parent
+ "oncarlos.com", // apex → skipped
+ "console.example.com", // foreign domain → skipped
+ "UPPER.CASE.ONCARLOS.COM", // case-normalized → case.oncarlos.com
+ }
+ got := parentsFor([]string{"go.tito.io"}, "oncarlos.com", hosts)
+ want := []string{"abs.oncarlos.com", "bab.oncarlos.com", "case.oncarlos.com", "go.tito.io"}
+ if !reflect.DeepEqual(got, want) {
+ t.Fatalf("parentsFor = %v, want %v", got, want)
+ }
+ // No apps domain → static only.
+ if got := parentsFor([]string{"go.tito.io"}, "", hosts); !reflect.DeepEqual(got, []string{"go.tito.io"}) {
+ t.Fatalf("static only = %v", got)
+ }
+ // Nothing at all.
+ if got := parentsFor(nil, "", nil); got != nil {
+ t.Fatalf("empty = %v, want nil", got)
+ }
+}
+
+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)
+ w.refresh()
+ cert, _ := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(60*24*time.Hour))
+ w.mu.Lock()
+ w.certs["abs.oncarlos.com"] = cert
+ w.mu.Unlock()
+
+ cases := []struct {
+ sni string
+ want bool
+ }{
+ {"kass.abs.oncarlos.com", true}, // exactly one label under parent
+ {"NEW.abs.oncarlos.com.", true}, // never-deployed name, case+dot normalized: instant
+ {"abs.oncarlos.com", false}, // the parent itself (wildcard doesn't cover apex)
+ {"x.y.abs.oncarlos.com", false}, // two labels deep
+ {"kass.bab.oncarlos.com", false}, // parent without a loaded cert (bab not in parents)
+ {"slug.go.tito.io", false}, // static parent present but no cert loaded yet
+ }
+ for _, c := range cases {
+ if got := w.certFor(c.sni) != nil; got != c.want {
+ t.Errorf("certFor(%q) served=%v, want %v", c.sni, got, c.want)
+ }
+ }
+
+ // An expired wildcard is never served — fall back to per-host issuance.
+ expired, _ := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(-time.Hour))
+ w.mu.Lock()
+ w.certs["abs.oncarlos.com"] = expired
+ w.mu.Unlock()
+ if w.certFor("kass.abs.oncarlos.com") != nil {
+ t.Fatal("expired wildcard was served")
+ }
+}
+
+func TestWildcardPEMRoundTripAndRenewDecision(t *testing.T) {
+ cert, key := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(60*24*time.Hour))
+ raw, err := encodeWildcardPEM(cert, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ back, err := parseWildcardPEM(raw)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !back.Leaf.NotAfter.Equal(cert.Leaf.NotAfter) {
+ t.Fatalf("roundtrip NotAfter = %v, want %v", back.Leaf.NotAfter, cert.Leaf.NotAfter)
+ }
+ if _, err := parseWildcardPEM([]byte("junk")); err == nil {
+ t.Fatal("parseWildcardPEM(junk): want error")
+ }
+
+ w := newWildcardManager(nil, "oncarlos.com", func() []string { return nil }, autocert.DirCache(t.TempDir()), nil)
+ if !w.needsObtain("abs.oncarlos.com", time.Now()) {
+ t.Fatal("no cert loaded: needsObtain should be true")
+ }
+ w.mu.Lock()
+ w.certs["abs.oncarlos.com"] = cert
+ w.mu.Unlock()
+ if w.needsObtain("abs.oncarlos.com", time.Now()) {
+ t.Fatal("60 days left: needsObtain should be false")
+ }
+ if !w.needsObtain("abs.oncarlos.com", cert.Leaf.NotAfter.Add(-15*24*time.Hour)) {
+ t.Fatal("15 days left (<30): needsObtain should be true")
+ }
+}
+
+func TestLoadCachedAndRefreshWake(t *testing.T) {
+ dir := t.TempDir()
+ cache := autocert.DirCache(dir)
+ cert, key := selfSignedWildcard(t, "abs.oncarlos.com", time.Now().Add(60*24*time.Hour))
+ raw, err := encodeWildcardPEM(cert, key)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if err := cache.Put(context.Background(), wildcardPEMKey("abs.oncarlos.com"), raw); err != nil {
+ t.Fatal(err)
+ }
+
+ w := newWildcardManager(nil, "oncarlos.com",
+ func() []string { return []string{"kass.abs.oncarlos.com"} }, cache, nil)
+ if !w.loadCached(context.Background(), "abs.oncarlos.com") {
+ t.Fatal("loadCached: want true for cached parent")
+ }
+ if w.loadCached(context.Background(), "bab.oncarlos.com") {
+ t.Fatal("loadCached: want false for uncached parent")
+ }
+
+ // refresh derives the parent and, since its cert is already loaded and
+ // fresh, does NOT wake the run loop; a parent with no cert does.
+ w.refresh()
+ select {
+ case <-w.wake:
+ t.Fatal("refresh with fresh cert should not wake")
+ default:
+ }
+ w2 := newWildcardManager(nil, "oncarlos.com",
+ func() []string { return []string{"kass.abs.oncarlos.com"} }, autocert.DirCache(t.TempDir()), nil)
+ w2.refresh()
+ select {
+ case <-w2.wake:
+ default:
+ t.Fatal("refresh with missing cert should wake the run loop")
+ }
+}