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:
Bryan Frimin
2026-07-20 09:59:25 +02:00
parent b03acbd029
commit 43ce3a7c53
51 changed files with 626 additions and 458 deletions

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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 {