Rename pkg in certmanager
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -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,
|
||||
100
pkg/certmanager/acme_challenge_handler.go
Normal file
100
pkg/certmanager/acme_challenge_handler.go
Normal 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
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -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,
|
||||
@@ -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{}
|
||||
@@ -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(
|
||||
Reference in New Issue
Block a user