Harden compliance portal auth and TLS
Align console references and OAuth branding with the compliance-page model, and fix certificate cache eviction, portal OAuth handlers, and magic-link edge cases left after the trust-center rename. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -59,8 +59,14 @@ func (w *CacheStore) WarmCache(ctx context.Context) error {
|
||||
err := w.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
var domains coredata.CustomDomains
|
||||
keepCertificateIDs, err := domains.LoadReferencedCertificateIDs(ctx, conn)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot load referenced certificate ids: %w", err)
|
||||
}
|
||||
|
||||
var caches coredata.CachedCertificates
|
||||
if err := caches.DeleteUnreferenced(ctx, conn); err != nil {
|
||||
if err := caches.DeleteWhereCertificateIDNotIn(ctx, conn, keepCertificateIDs); err != nil {
|
||||
return fmt.Errorf("cannot delete unreferenced certificate cache: %w", err)
|
||||
}
|
||||
|
||||
@@ -116,19 +122,6 @@ func (w *CacheStore) warmCertificate(ctx context.Context, conn pg.Querier, certi
|
||||
return fmt.Errorf("cannot parse certificate: %w", err)
|
||||
}
|
||||
|
||||
if len(loadedCertificate.SSLCertificatePEM) == 0 {
|
||||
return fmt.Errorf("certificate has no certificate PEM")
|
||||
}
|
||||
|
||||
privateKeyPEM, err := loadedCertificate.DecryptPrivateKey(w.encryptionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot decrypt private key: %w", err)
|
||||
}
|
||||
|
||||
if len(privateKeyPEM) == 0 {
|
||||
return fmt.Errorf("certificate has no private key PEM")
|
||||
}
|
||||
|
||||
if loadedCertificate.SSLExpiresAt == nil {
|
||||
return fmt.Errorf("certificate has no expiry date")
|
||||
}
|
||||
@@ -137,18 +130,9 @@ func (w *CacheStore) warmCertificate(ctx context.Context, conn pg.Querier, certi
|
||||
return fmt.Errorf("certificate has expired")
|
||||
}
|
||||
|
||||
cache := &coredata.CachedCertificate{
|
||||
Domain: loadedCertificate.Hostname,
|
||||
CertificatePEM: string(loadedCertificate.SSLCertificatePEM),
|
||||
PrivateKeyPEM: string(privateKeyPEM),
|
||||
CertificateChain: loadedCertificate.SSLCertificateChain,
|
||||
ExpiresAt: *loadedCertificate.SSLExpiresAt,
|
||||
CachedAt: time.Now(),
|
||||
CertificateID: loadedCertificate.ID,
|
||||
}
|
||||
|
||||
if err := cache.Upsert(ctx, conn); err != nil {
|
||||
return fmt.Errorf("cannot upsert cache entry: %w", err)
|
||||
var cache coredata.CachedCertificate
|
||||
if err := cache.RefreshFromCertificate(ctx, conn, &loadedCertificate, w.encryptionKey); err != nil {
|
||||
return fmt.Errorf("cannot refresh certificate cache: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -97,6 +97,12 @@ func (h *renewHandler) Process(ctx context.Context, certificate coredata.Certifi
|
||||
func(ctx context.Context, tx pg.Tx) error {
|
||||
fullCertificate := &coredata.Certificate{}
|
||||
if err := fullCertificate.LoadByIDForUpdateSkipLocked(ctx, tx, coredata.NewNoScope(), certificate.ID); err != nil {
|
||||
// Another provision/renewal cycle may already hold the row
|
||||
// (SKIP LOCKED) or the certificate may have been deleted.
|
||||
if errors.Is(err, coredata.ErrResourceNotFound) {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("cannot load certificate for renewal: %w", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go.gearno.de/kit/pg"
|
||||
"go.probo.inc/probo/pkg/coredata"
|
||||
@@ -68,7 +67,14 @@ func (s *Selector) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
|
||||
|
||||
if cached, ok := s.cache.Load(domain); ok {
|
||||
if cert, ok := cached.(*tls.Certificate); ok {
|
||||
return cert, nil
|
||||
if err := s.checkRoutable(domain); err == nil {
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// The domain was deleted or is no longer routable since the
|
||||
// cache entry was stored; evict it and fall through to a fresh
|
||||
// database load below.
|
||||
s.cache.Delete(domain)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,6 +88,21 @@ func (s *Selector) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
|
||||
return cert, nil
|
||||
}
|
||||
|
||||
// checkRoutable reports whether domain is still a routable custom domain
|
||||
// with an active certificate. It is used to revalidate memory-cache hits so
|
||||
// certificates for deleted or de-provisioned domains stop being served
|
||||
// without waiting for process restart.
|
||||
func (s *Selector) checkRoutable(domain string) error {
|
||||
ctx := context.Background()
|
||||
|
||||
return s.pg.WithConn(
|
||||
ctx,
|
||||
func(ctx context.Context, conn pg.Querier) error {
|
||||
return requireRoutableDomain(ctx, conn, domain)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Selector) loadFromDatabase(domain string) (*tls.Certificate, error) {
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -153,32 +174,20 @@ func (s *Selector) rebuildCacheEntry(ctx context.Context, conn pg.Querier, domai
|
||||
return fmt.Errorf("certificate has no encrypted private key data")
|
||||
}
|
||||
|
||||
privateKeyPEM, err := certificate.DecryptPrivateKey(s.encryptionKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot decrypt private key: %w", err)
|
||||
if certificate.SSLExpiresAt == nil {
|
||||
return fmt.Errorf("certificate has no expiry")
|
||||
}
|
||||
|
||||
s.cache.Store(domain, certificate.SSLCertificate)
|
||||
|
||||
cache := &coredata.CachedCertificate{
|
||||
Domain: certificate.Hostname,
|
||||
CertificatePEM: string(certificate.SSLCertificatePEM),
|
||||
PrivateKeyPEM: string(privateKeyPEM),
|
||||
CertificateChain: certificate.SSLCertificateChain,
|
||||
ExpiresAt: *certificate.SSLExpiresAt,
|
||||
CachedAt: time.Now(),
|
||||
CertificateID: certificate.ID,
|
||||
}
|
||||
|
||||
if err := cache.Upsert(ctx, conn); err != nil {
|
||||
var cache coredata.CachedCertificate
|
||||
if err := cache.RefreshFromCertificate(ctx, conn, &certificate, s.encryptionKey); err != nil {
|
||||
return fmt.Errorf("cannot insert cache entry: %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user