Rename pkg in certmanager

Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
Bryan Frimin
2025-09-30 21:43:43 +02:00
parent fe58eb527e
commit 6ff8386818
7 changed files with 162 additions and 94 deletions

View File

@@ -1,24 +0,0 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cert
// DNSRecordInstruction provides DNS configuration instructions
type DNSRecordInstruction struct {
Type string `json:"type"` // TXT, CNAME
Name string `json:"name"` // Full DNS record name
Value string `json:"value"` // Value to set
TTL int `json:"ttl"` // Recommended TTL
Purpose string `json:"purpose"` // verification, acme_challenge
}

View File

@@ -12,19 +12,21 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cert
package certmanager
import (
"context"
"crypto"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"net/http"
"time"
"github.com/getprobo/probo/pkg/crypto/keys"
cryptopem "github.com/getprobo/probo/pkg/crypto/pem"
"github.com/getprobo/probo/pkg/crypto/pem"
"github.com/getprobo/probo/pkg/version"
"go.gearno.de/kit/httpclient"
"go.gearno.de/kit/log"
@@ -55,17 +57,28 @@ type (
}
)
func NewACMEService(email string, keyType keys.Type, directoryURL string, logger *log.Logger) (*ACMEService, error) {
func NewACMEService(email string, keyType keys.Type, directoryURL string, insecureTLS bool, logger *log.Logger) (*ACMEService, error) {
accountKey, err := keys.Generate(keyType)
if err != nil {
return nil, fmt.Errorf("cannot generate account key: %w", err)
}
httpClient := httpclient.DefaultPooledClient(
httpclient.WithLogger(logger),
// httpclient.WithTracerProvider(tp),
// httpclient.WithRegisterer(r),
)
var httpClient *http.Client
if insecureTLS {
transport := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
httpClient = &http.Client{
Transport: transport,
Timeout: 30 * time.Second,
}
logger.Warn("ACME service configured with insecure TLS - use only for local testing")
} else {
httpClient = httpclient.DefaultPooledClient(
httpclient.WithLogger(logger),
)
}
client := &acme.Client{
Key: accountKey,
@@ -183,8 +196,8 @@ func (s *ACMEService) CompleteHTTPChallenge(
return nil, fmt.Errorf("cannot parse certificate: %w", err)
}
certPEM := cryptopem.EncodeCertificate(der[0])
keyPEM, err := cryptopem.EncodePrivateKey(certKey)
certPEM := pem.EncodeCertificate(der[0])
keyPEM, err := pem.EncodePrivateKey(certKey)
if err != nil {
return nil, fmt.Errorf("cannot encode key: %w", err)
}
@@ -193,7 +206,7 @@ func (s *ACMEService) CompleteHTTPChallenge(
if len(der) > 1 {
chainDER = der[1:]
}
chainPEM := cryptopem.EncodeCertificateChain(chainDER)
chainPEM := pem.EncodeCertificateChain(chainDER)
return &Certificate{
CertPEM: certPEM,
@@ -268,8 +281,8 @@ func (s *ACMEService) renewWithExistingAuth(ctx context.Context, domain string)
return nil, fmt.Errorf("cannot parse certificate: %w", err)
}
certPEM := cryptopem.EncodeCertificate(der[0])
keyPEM, err := cryptopem.EncodePrivateKey(certKey)
certPEM := pem.EncodeCertificate(der[0])
keyPEM, err := pem.EncodePrivateKey(certKey)
if err != nil {
return nil, fmt.Errorf("cannot encode key: %w", err)
}
@@ -278,7 +291,7 @@ func (s *ACMEService) renewWithExistingAuth(ctx context.Context, domain string)
if len(der) > 1 {
chainDER = der[1:]
}
chainPEM := cryptopem.EncodeCertificateChain(chainDER)
chainPEM := pem.EncodeCertificateChain(chainDER)
return &Certificate{
CertPEM: certPEM,

View File

@@ -0,0 +1,100 @@
// Copyright (c) 2025 Probo Inc <hello@getprobo.com>.
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package certmanager
import (
"context"
"net/http"
"strings"
"github.com/getprobo/probo/pkg/coredata"
"github.com/getprobo/probo/pkg/crypto/cipher"
"go.gearno.de/kit/log"
"go.gearno.de/kit/pg"
)
type ACMEChallengeHandler struct {
pg *pg.Client
encryptionKey cipher.EncryptionKey
logger *log.Logger
}
func NewACMEChallengeHandler(
pg *pg.Client,
encryptionKey cipher.EncryptionKey,
logger *log.Logger,
) *ACMEChallengeHandler {
return &ACMEChallengeHandler{
pg: pg,
encryptionKey: encryptionKey,
logger: logger.Named("acme-challenge-handler"),
}
}
func (h *ACMEChallengeHandler) Handle(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.HasPrefix(r.URL.Path, "/.well-known/acme-challenge/") {
next.ServeHTTP(w, r)
return
}
token := strings.TrimPrefix(r.URL.Path, "/.well-known/acme-challenge/")
if token == "" {
http.NotFound(w, r)
return
}
keyAuth, err := h.getKeyAuthForToken(r.Context(), token)
if err != nil {
h.logger.WarnCtx(
r.Context(),
"cannot get key auth for token",
log.String("token", token),
log.Error(err),
)
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
w.Write([]byte(keyAuth))
})
}
func (h *ACMEChallengeHandler) getKeyAuthForToken(ctx context.Context, token string) (string, error) {
var keyAuth string
err := h.pg.WithConn(
ctx,
func(conn pg.Conn) error {
domain := &coredata.CustomDomain{}
if err := domain.LoadByHTTPChallengeToken(ctx, conn, coredata.NewNoScope(), h.encryptionKey, token); err != nil {
return err
}
if domain.HTTPChallengeKeyAuth == nil {
return http.ErrNotSupported
}
keyAuth = *domain.HTTPChallengeKeyAuth
return nil
},
)
return keyAuth, err
}

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cert
package certmanager
import (
"context"
@@ -26,26 +26,26 @@ import (
)
type (
CacheWarmer struct {
CacheStore struct {
pg *pg.Client
encryptionKey cipher.EncryptionKey
logger *log.Logger
}
)
func NewCacheWarmer(
func NewCacheStore(
pg *pg.Client,
encryptionKey cipher.EncryptionKey,
logger *log.Logger,
) *CacheWarmer {
return &CacheWarmer{
) *CacheStore {
return &CacheStore{
pg: pg,
encryptionKey: encryptionKey,
logger: logger.Named("cert.cache_warmer"),
logger: logger.Named("certmanager.cache-store"),
}
}
func (w *CacheWarmer) WarmCache(ctx context.Context) error {
func (w *CacheStore) WarmCache(ctx context.Context) error {
w.logger.InfoCtx(ctx, "warming certificate cache")
startTime := time.Now()
@@ -93,7 +93,7 @@ func (w *CacheWarmer) WarmCache(ctx context.Context) error {
return nil
}
func (w *CacheWarmer) warmDomain(ctx context.Context, conn pg.Conn, domain *coredata.CustomDomain) error {
func (w *CacheStore) warmDomain(ctx context.Context, conn pg.Conn, domain *coredata.CustomDomain) error {
var loadedDomain coredata.CustomDomain
scope := coredata.NewScope(domain.OrganizationID.TenantID())
if err := loadedDomain.LoadByID(ctx, conn, scope, w.encryptionKey, domain.ID); err != nil {
@@ -137,7 +137,7 @@ func (w *CacheWarmer) warmDomain(ctx context.Context, conn pg.Conn, domain *core
return nil
}
func (w *CacheWarmer) RefreshCache(ctx context.Context) error {
func (w *CacheStore) RefreshCache(ctx context.Context) error {
w.logger.InfoCtx(ctx, "refreshing certificate cache")
return w.pg.WithConn(
@@ -153,7 +153,7 @@ func (w *CacheWarmer) RefreshCache(ctx context.Context) error {
)
}
func (w *CacheWarmer) WarmSingleDomain(ctx context.Context, domainName string) error {
func (w *CacheStore) WarmSingleDomain(ctx context.Context, domainName string) error {
return w.pg.WithConn(
ctx,
func(conn pg.Conn) error {

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cert
package certmanager
import (
"context"
@@ -26,7 +26,7 @@ import (
)
type (
CertificateProvisioner struct {
Provisioner struct {
pg *pg.Client
acmeService *ACMEService
encryptionKey cipher.EncryptionKey
@@ -35,23 +35,23 @@ type (
}
)
func NewCertificateProvisioner(
func NewProvisioner(
pg *pg.Client,
acmeService *ACMEService,
encryptionKey cipher.EncryptionKey,
interval time.Duration,
logger *log.Logger,
) *CertificateProvisioner {
return &CertificateProvisioner{
) *Provisioner {
return &Provisioner{
pg: pg,
acmeService: acmeService,
encryptionKey: encryptionKey,
interval: interval,
logger: logger.Named("cert.provisioner"),
logger: logger.Named("certmanager.provisioner"),
}
}
func (p *CertificateProvisioner) Run(ctx context.Context) error {
func (p *Provisioner) Run(ctx context.Context) error {
p.logger.InfoCtx(ctx, "certificate provisioner starting", log.Duration("interval", p.interval))
if err := p.checkPendingDomains(ctx); err != nil {
@@ -74,7 +74,7 @@ func (p *CertificateProvisioner) Run(ctx context.Context) error {
}
}
func (p *CertificateProvisioner) checkPendingDomains(ctx context.Context) error {
func (p *Provisioner) checkPendingDomains(ctx context.Context) error {
return p.pg.WithConn(ctx, func(conn pg.Conn) error {
var domains coredata.CustomDomains
if err := domains.ListDomainsWithPendingHTTPChallenges(ctx, conn, coredata.NewNoScope()); err != nil {
@@ -108,7 +108,7 @@ func (p *CertificateProvisioner) checkPendingDomains(ctx context.Context) error
})
}
func (p *CertificateProvisioner) completeDomainCertificate(
func (p *Provisioner) completeDomainCertificate(
ctx context.Context,
conn pg.Conn,
domain *coredata.CustomDomain,

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cert
package certmanager
import (
"context"
@@ -28,7 +28,7 @@ import (
)
type (
CertificateRenewer struct {
Renewer struct {
pg *pg.Client
acmeService *ACMEService
encryptionKey cipher.EncryptionKey
@@ -37,24 +37,24 @@ type (
}
)
func NewCertificateRenewer(
func NewRenewer(
pg *pg.Client,
acmeService *ACMEService,
encryptionKey cipher.EncryptionKey,
interval time.Duration,
logger *log.Logger,
) *CertificateRenewer {
return &CertificateRenewer{
) *Renewer {
return &Renewer{
pg: pg,
acmeService: acmeService,
encryptionKey: encryptionKey,
interval: interval,
logger: logger.Named("cert.certificate-renewer"),
logger: logger.Named("certmanager.renewer"),
}
}
func (r *CertificateRenewer) Run(ctx context.Context) error {
r.logger.InfoCtx(ctx, "certificate certificate-renewer starting")
func (r *Renewer) Run(ctx context.Context) error {
r.logger.InfoCtx(ctx, "certificate renewer starting")
if err := r.checkAndRenew(ctx); err != nil {
r.logger.ErrorCtx(ctx, "cannot perform initial renewal check", log.Error(err))
@@ -63,7 +63,7 @@ func (r *CertificateRenewer) Run(ctx context.Context) error {
for {
select {
case <-ctx.Done():
r.logger.InfoCtx(ctx, "certificate certificate-renewer shutting down")
r.logger.InfoCtx(ctx, "certificate renewer shutting down")
return ctx.Err()
case <-time.After(r.interval):
if err := r.checkAndRenew(ctx); err != nil {
@@ -73,7 +73,7 @@ func (r *CertificateRenewer) Run(ctx context.Context) error {
}
}
func (r *CertificateRenewer) checkAndRenew(ctx context.Context) error {
func (r *Renewer) checkAndRenew(ctx context.Context) error {
return r.pg.WithConn(
ctx,
func(conn pg.Conn) error {
@@ -84,7 +84,7 @@ func (r *CertificateRenewer) checkAndRenew(ctx context.Context) error {
} else if cacheCount == 0 {
r.logger.InfoCtx(ctx, "certificate cache is empty, rebuilding from custom_domains")
warmer := NewCacheWarmer(r.pg, r.encryptionKey, r.logger)
warmer := NewCacheStore(r.pg, r.encryptionKey, r.logger)
if err := warmer.WarmCache(ctx); err != nil {
r.logger.ErrorCtx(ctx, "cannot rebuild certificate cache", log.Error(err))
} else {
@@ -128,7 +128,7 @@ func (r *CertificateRenewer) checkAndRenew(ctx context.Context) error {
)
}
func (r *CertificateRenewer) renewDomain(ctx context.Context, conn pg.Conn, domain *coredata.CustomDomain) error {
func (r *Renewer) renewDomain(ctx context.Context, conn pg.Conn, domain *coredata.CustomDomain) error {
scope := coredata.NewScope(domain.OrganizationID.TenantID())
lockedDomain := &coredata.CustomDomain{}

View File

@@ -12,7 +12,7 @@
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
package cert
package certmanager
import (
"context"
@@ -30,37 +30,27 @@ type (
Selector struct {
pg *pg.Client
cache sync.Map
defaultDomain string
encryptionKey cipher.EncryptionKey
defaultCert *tls.Certificate
defaultCertMutex sync.RWMutex
}
)
func NewSelector(
pg *pg.Client,
defaultDomain string,
encryptionKey cipher.EncryptionKey,
) *Selector {
return &Selector{
pg: pg,
defaultDomain: defaultDomain,
encryptionKey: encryptionKey,
}
}
func (s *Selector) SetDefaultCertificate(cert *tls.Certificate) {
s.defaultCertMutex.Lock()
defer s.defaultCertMutex.Unlock()
s.defaultCert = cert
}
func (s *Selector) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
domain := hello.ServerName
// Empty domain, use default
// Empty domain, return error
if domain == "" {
return s.getDefaultCertificate()
return nil, fmt.Errorf("no SNI provided")
}
if cached, ok := s.cache.Load(domain); ok {
@@ -71,7 +61,7 @@ func (s *Selector) GetCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
cert, err := s.loadFromDatabase(domain)
if err != nil {
return s.getDefaultCertificate()
return nil, err
}
s.cache.Store(domain, cert)
@@ -163,17 +153,6 @@ func (s *Selector) rebuildCacheEntry(ctx context.Context, conn pg.Conn, domain s
return nil
}
// getDefaultCertificate returns the default wildcard certificate
func (s *Selector) getDefaultCertificate() (*tls.Certificate, error) {
s.defaultCertMutex.RLock()
defer s.defaultCertMutex.RUnlock()
if s.defaultCert == nil {
return nil, fmt.Errorf("no default certificate configured")
}
return s.defaultCert, nil
}
func (s *Selector) ClearCache() {
s.cache.Range(