diff --git a/pkg/certmanager/cache_store.go b/pkg/certmanager/cache_store.go index a515d6d81..a47fa2632 100644 --- a/pkg/certmanager/cache_store.go +++ b/pkg/certmanager/cache_store.go @@ -59,8 +59,13 @@ func (w *CacheStore) WarmCache(ctx context.Context) error { err := w.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { + var caches coredata.CachedCertificates + if err := caches.DeleteUnreferenced(ctx, conn); err != nil { + return fmt.Errorf("cannot delete unreferenced certificate cache: %w", err) + } + certificates := coredata.Certificates{} - if err := certificates.LoadActive(ctx, conn, coredata.NewNoScope()); err != nil { + if err := certificates.LoadActiveReferenced(ctx, conn, coredata.NewNoScope()); err != nil { return fmt.Errorf("cannot load active certificates: %w", err) } diff --git a/pkg/certmanager/selector.go b/pkg/certmanager/selector.go index 0deaf15df..379d84805 100644 --- a/pkg/certmanager/selector.go +++ b/pkg/certmanager/selector.go @@ -23,6 +23,7 @@ package certmanager import ( "context" "crypto/tls" + "errors" "fmt" "sync" "time" @@ -89,6 +90,10 @@ func (s *Selector) loadFromDatabase(domain string) (*tls.Certificate, error) { err := s.pg.WithConn( ctx, func(ctx context.Context, conn pg.Querier) error { + if err := requireRoutableDomain(ctx, conn, domain); err != nil { + return err + } + var cache coredata.CachedCertificate if err := cache.LoadByDomain(ctx, conn, domain); err != nil { if err := s.rebuildCacheEntry(ctx, conn, domain); err != nil { @@ -123,6 +128,10 @@ func (s *Selector) loadFromDatabase(domain string) (*tls.Certificate, error) { } func (s *Selector) rebuildCacheEntry(ctx context.Context, conn pg.Querier, domain string) error { + if err := requireRoutableDomain(ctx, conn, domain); err != nil { + return err + } + var certificate coredata.Certificate if err := certificate.LoadByHostname(ctx, conn, coredata.NewNoScope(), domain); err != nil { return fmt.Errorf("cannot load certificate: %w", err) @@ -167,3 +176,22 @@ func (s *Selector) rebuildCacheEntry(ctx context.Context, conn pg.Querier, domai return nil } + +// requireRoutableDomain ensures the SNI hostname still maps to a custom domain +// row. Orphaned certificates left after domain deletion must not be served. +func requireRoutableDomain(ctx context.Context, conn pg.Querier, domain string) error { + var customDomain coredata.CustomDomain + if err := customDomain.LoadByDomain(ctx, conn, coredata.NewNoScope(), domain); err != nil { + if errors.Is(err, coredata.ErrResourceNotFound) { + return err + } + + return fmt.Errorf("cannot load custom domain: %w", err) + } + + if customDomain.CertificateID == nil { + return coredata.ErrResourceNotFound + } + + return nil +} diff --git a/pkg/coredata/cached_certificate.go b/pkg/coredata/cached_certificate.go index 258074937..609d6a062 100644 --- a/pkg/coredata/cached_certificate.go +++ b/pkg/coredata/cached_certificate.go @@ -170,6 +170,29 @@ WHERE return nil } +// DeleteUnreferenced removes cache entries whose certificate is no longer +// referenced by any custom domain, so deleted domains cannot keep a usable +// TLS cache entry. +func (cc *CachedCertificates) DeleteUnreferenced(ctx context.Context, conn pg.Querier) error { + q := ` +DELETE FROM + cached_certificates +WHERE + NOT EXISTS ( + SELECT 1 + FROM custom_domains + WHERE custom_domains.certificate_id = cached_certificates.certificate_id + ) +` + + _, err := conn.Exec(ctx, q, pgx.NamedArgs{}) + if err != nil { + return fmt.Errorf("cannot delete unreferenced certificate cache: %w", err) + } + + return nil +} + func (cc *CachedCertificate) RefreshFromCertificate(ctx context.Context, conn pg.Querier, certificate *Certificate, encryptionKey cipher.EncryptionKey) error { if certificate.SSLCertificate == nil { return fmt.Errorf("certificate has no parsed certificate") diff --git a/pkg/coredata/certificate.go b/pkg/coredata/certificate.go index 17f9861eb..fadb130a4 100644 --- a/pkg/coredata/certificate.go +++ b/pkg/coredata/certificate.go @@ -685,7 +685,10 @@ WHERE return nil } -func (certificates *Certificates) LoadActive( +// LoadActiveReferenced loads active certificates that are still referenced by +// at least one custom domain. Certificates left behind after a domain is +// deleted must not be warmed into the TLS cache or served by SNI alone. +func (certificates *Certificates) LoadActiveReferenced( ctx context.Context, conn pg.Querier, scope Scoper, @@ -714,6 +717,11 @@ WHERE %s AND status = @status AND ssl_certificate IS NOT NULL + AND EXISTS ( + SELECT 1 + FROM custom_domains + WHERE custom_domains.certificate_id = certificates.id + ) ` q = fmt.Sprintf(q, scope.SQLFragment()) @@ -723,7 +731,7 @@ WHERE rows, err := conn.Query(ctx, q, args) if err != nil { - return fmt.Errorf("cannot query active certificates: %w", err) + return fmt.Errorf("cannot query active referenced certificates: %w", err) } result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Certificate])