Limit TLS cache warming to live domains
After the certificates split, WarmCache loaded every ACTIVE certificate. Org deletes cascade-remove custom_domains but leave certificates behind, so orphans could regain a usable SNI cache entry on rebuild. Warm and serve only certs still referenced by a domain, and purge unreferenced cache rows. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -59,8 +59,13 @@ func (w *CacheStore) WarmCache(ctx context.Context) error {
|
|||||||
err := w.pg.WithConn(
|
err := w.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
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{}
|
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)
|
return fmt.Errorf("cannot load active certificates: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ package certmanager
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -89,6 +90,10 @@ func (s *Selector) loadFromDatabase(domain string) (*tls.Certificate, error) {
|
|||||||
err := s.pg.WithConn(
|
err := s.pg.WithConn(
|
||||||
ctx,
|
ctx,
|
||||||
func(ctx context.Context, conn pg.Querier) error {
|
func(ctx context.Context, conn pg.Querier) error {
|
||||||
|
if err := requireRoutableDomain(ctx, conn, domain); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
var cache coredata.CachedCertificate
|
var cache coredata.CachedCertificate
|
||||||
if err := cache.LoadByDomain(ctx, conn, domain); err != nil {
|
if err := cache.LoadByDomain(ctx, conn, domain); err != nil {
|
||||||
if err := s.rebuildCacheEntry(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 {
|
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
|
var certificate coredata.Certificate
|
||||||
if err := certificate.LoadByHostname(ctx, conn, coredata.NewNoScope(), domain); err != nil {
|
if err := certificate.LoadByHostname(ctx, conn, coredata.NewNoScope(), domain); err != nil {
|
||||||
return fmt.Errorf("cannot load certificate: %w", err)
|
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
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -170,6 +170,29 @@ WHERE
|
|||||||
return nil
|
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 {
|
func (cc *CachedCertificate) RefreshFromCertificate(ctx context.Context, conn pg.Querier, certificate *Certificate, encryptionKey cipher.EncryptionKey) error {
|
||||||
if certificate.SSLCertificate == nil {
|
if certificate.SSLCertificate == nil {
|
||||||
return fmt.Errorf("certificate has no parsed certificate")
|
return fmt.Errorf("certificate has no parsed certificate")
|
||||||
|
|||||||
@@ -685,7 +685,10 @@ WHERE
|
|||||||
return nil
|
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,
|
ctx context.Context,
|
||||||
conn pg.Querier,
|
conn pg.Querier,
|
||||||
scope Scoper,
|
scope Scoper,
|
||||||
@@ -714,6 +717,11 @@ WHERE
|
|||||||
%s
|
%s
|
||||||
AND status = @status
|
AND status = @status
|
||||||
AND ssl_certificate IS NOT NULL
|
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())
|
q = fmt.Sprintf(q, scope.SQLFragment())
|
||||||
@@ -723,7 +731,7 @@ WHERE
|
|||||||
|
|
||||||
rows, err := conn.Query(ctx, q, args)
|
rows, err := conn.Query(ctx, q, args)
|
||||||
if err != nil {
|
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])
|
result, err := pgx.CollectRows(rows, pgx.RowToAddrOfStructByName[Certificate])
|
||||||
|
|||||||
Reference in New Issue
Block a user