pomerium/internal/autocert/manager.go

533 lines
14 KiB
Go

// Package autocert implements automatic management of TLS certificates.
package autocert
import (
"context"
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"net"
"net/http"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/caddyserver/certmagic"
"github.com/mholt/acmez/v3/acme"
"github.com/pires/go-proxyproto"
"github.com/rs/zerolog"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/httputil"
"github.com/pomerium/pomerium/internal/log"
"github.com/pomerium/pomerium/internal/telemetry/metrics"
"github.com/pomerium/pomerium/internal/urlutil"
"github.com/pomerium/pomerium/pkg/cryptutil"
)
var (
errObtainCertFailed = errors.New("obtain cert failed")
errRenewCertFailed = errors.New("renew cert failed")
// RenewCert is not thread-safe
renewCertLock sync.Mutex
)
const (
ocspRespCacheSize = 50000
renewalInterval = time.Minute * 10
renewalTimeout = time.Hour
)
// Manager manages TLS certificates.
type Manager struct {
src config.Source
acmeTemplate certmagic.ACMEIssuer
mu sync.RWMutex
config *config.Config
certmagic *certmagic.Config
acmeMgr atomic.Pointer[certmagic.ACMEIssuer]
srv *http.Server
acmeTLSALPNLock sync.Mutex
acmeTLSALPNPort string
acmeTLSALPNListener net.Listener
acmeTLSALPNConfig *tls.Config
*ocspCache
config.ChangeDispatcher
}
// New creates a new autocert manager.
func New(ctx context.Context, src config.Source) (*Manager, error) {
return newManager(ctx, src, certmagic.DefaultACME, renewalInterval)
}
func newManager(
ctx context.Context,
src config.Source,
acmeTemplate certmagic.ACMEIssuer,
checkInterval time.Duration,
) (*Manager, error) {
ctx = log.WithContext(ctx, func(c zerolog.Context) zerolog.Context {
return c.Str("service", "autocert-manager")
})
ocspRespCache, err := newOCSPCache(ocspRespCacheSize)
if err != nil {
return nil, err
}
logger := getCertMagicLogger()
acmeTemplate.Logger = logger
mgr := &Manager{
src: src,
acmeTemplate: acmeTemplate,
ocspCache: ocspRespCache,
}
// set certmagic default storage cache, otherwise cert renewal loop will be based off
// certmagic's own default location
certmagicStorage, err := GetCertMagicStorage(ctx, src.GetConfig().Options.AutocertOptions.Folder)
if err != nil {
return nil, err
}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(_ certmagic.Certificate) (*certmagic.Config, error) {
return mgr.certmagic, nil
},
Logger: logger,
})
mgr.certmagic = certmagic.New(cache, certmagic.Config{
Logger: logger,
Storage: certmagicStorage,
})
err = mgr.update(ctx, src.GetConfig())
if err != nil {
return nil, err
}
mgr.src.OnConfigChange(ctx, func(ctx context.Context, cfg *config.Config) {
err := mgr.update(ctx, cfg)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("autocert: error updating config")
return
}
cfg = mgr.GetConfig()
mgr.Trigger(ctx, cfg)
})
go func() {
ticker := time.NewTicker(checkInterval)
defer ticker.Stop()
for {
select {
case <-ctx.Done():
cache.Stop()
return
case <-ticker.C:
err := mgr.renewConfigCerts(ctx)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("autocert: error updating config")
return
}
}
}
}()
return mgr, nil
}
func (mgr *Manager) getCertMagicConfig(ctx context.Context, cfg *config.Config) (*certmagic.Config, error) {
mgr.certmagic.MustStaple = cfg.Options.AutocertOptions.MustStaple
mgr.certmagic.OnDemand = nil // disable on-demand
var err error
mgr.certmagic.Storage, err = GetCertMagicStorage(ctx, cfg.Options.AutocertOptions.Folder)
if err != nil {
return nil, err
}
certs, err := cfg.AllCertificates()
if err != nil {
return nil, err
}
// add existing certs to the cache, and staple OCSP
for _, cert := range certs {
if _, err := mgr.certmagic.CacheUnmanagedTLSCertificate(ctx, cert, nil); err != nil {
return nil, fmt.Errorf("config: failed caching cert: %w", err)
}
}
acmeMgr := certmagic.NewACMEIssuer(mgr.certmagic, mgr.acmeTemplate)
acmeMgr.DisableHTTPChallenge = !shouldEnableHTTPChallenge(cfg)
err = configureCertificateAuthority(acmeMgr, cfg.Options.AutocertOptions)
if err != nil {
return nil, err
}
err = configureExternalAccountBinding(acmeMgr, cfg.Options.AutocertOptions)
if err != nil {
return nil, err
}
err = configureTrustedRoots(acmeMgr, cfg.Options.AutocertOptions)
if err != nil {
return nil, err
}
mgr.certmagic.Issuers = []certmagic.Issuer{acmeMgr}
mgr.acmeMgr.Store(acmeMgr)
return mgr.certmagic, nil
}
func (mgr *Manager) renewConfigCerts(ctx context.Context) error {
ctx, cancel := context.WithTimeout(ctx, renewalTimeout)
defer cancel()
mgr.mu.Lock()
defer mgr.mu.Unlock()
cfg := mgr.config
cm, err := mgr.getCertMagicConfig(ctx, cfg)
if err != nil {
return err
}
needsReload := false
var renew, ocsp []string
log.Ctx(ctx).Debug().Strs("domains", sourceHostnames(cfg)).Msg("checking domains")
for _, domain := range sourceHostnames(cfg) {
cert, err := cm.CacheManagedCertificate(ctx, domain)
if err != nil {
// this happens for unmanaged certificates
continue
}
if cert.NeedsRenewal(cm) {
renew = append(renew, domain)
needsReload = true
}
if mgr.ocspCache.updated(domain, cert.OCSPStaple) {
ocsp = append(ocsp, domain)
needsReload = true
}
}
if !needsReload {
return nil
}
ctx = log.WithContext(ctx, func(c zerolog.Context) zerolog.Context {
if len(renew) > 0 {
c = c.Strs("renew-domains", renew)
}
if len(ocsp) > 0 {
c = c.Strs("ocsp-refresh", ocsp)
}
return c
})
log.Ctx(ctx).Info().Msg("updating certificates")
cfg = mgr.src.GetConfig().Clone()
mgr.updateServer(ctx, cfg)
mgr.updateACMETLSALPNServer(ctx, cfg)
if err := mgr.updateAutocert(ctx, cfg); err != nil {
return err
}
mgr.config = cfg
mgr.Trigger(ctx, cfg)
return nil
}
func (mgr *Manager) update(ctx context.Context, cfg *config.Config) error {
cfg = cfg.Clone()
mgr.mu.Lock()
defer mgr.mu.Unlock()
defer func() { mgr.config = cfg }()
mgr.updateServer(ctx, cfg)
mgr.updateACMETLSALPNServer(ctx, cfg)
return mgr.updateAutocert(ctx, cfg)
}
// obtainCert obtains a certificate for given domain, use cached manager if cert exists there.
func (mgr *Manager) obtainCert(ctx context.Context, domain string, cm *certmagic.Config) (certmagic.Certificate, error) {
cert, err := cm.CacheManagedCertificate(ctx, domain)
if err != nil {
log.Ctx(ctx).Info().Str("domain", domain).Msg("obtaining certificate")
err = cm.ObtainCertSync(ctx, domain)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("autocert failed to obtain client certificate")
return certmagic.Certificate{}, errObtainCertFailed
}
metrics.RecordAutocertRenewal()
cert, err = cm.CacheManagedCertificate(ctx, domain)
}
return cert, err
}
// renewCert attempts to renew given certificate.
func (mgr *Manager) renewCert(ctx context.Context, domain string, cert certmagic.Certificate, cm *certmagic.Config) (certmagic.Certificate, error) {
expired := time.Now().After(cert.Leaf.NotAfter)
log.Ctx(ctx).Info().Str("domain", domain).Msg("renewing certificate")
renewCertLock.Lock()
err := cm.RenewCertSync(ctx, domain, false)
renewCertLock.Unlock()
if err != nil {
if expired {
return certmagic.Certificate{}, errRenewCertFailed
}
log.Ctx(ctx).Error().Err(err).Msg("renew client certificated failed, use existing cert")
}
return cm.CacheManagedCertificate(ctx, domain)
}
func (mgr *Manager) updateAutocert(ctx context.Context, cfg *config.Config) error {
if !cfg.Options.AutocertOptions.Enable {
mgr.acmeMgr.Store(nil)
return nil
}
cm, err := mgr.getCertMagicConfig(ctx, cfg)
if err != nil {
return err
}
for _, domain := range sourceHostnames(cfg) {
cert, err := mgr.obtainCert(ctx, domain, cm)
if err == nil && cert.NeedsRenewal(cm) {
cert, err = mgr.renewCert(ctx, domain, cert, cm)
}
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("autocert: failed to obtain client certificate")
continue
}
log.Ctx(ctx).Info().Strs("names", cert.Names).Msg("autocert: added certificate")
cfg.AutoCertificates = append(cfg.AutoCertificates, cert.Certificate)
}
metrics.RecordAutocertCertificates(cfg.AutoCertificates)
return nil
}
func (mgr *Manager) updateServer(ctx context.Context, cfg *config.Config) {
if mgr.srv != nil {
// nothing to do if the address hasn't changed
if mgr.srv.Addr == cfg.Options.HTTPRedirectAddr {
return
}
// close immediately, don't care about the error
_ = mgr.srv.Close()
mgr.srv = nil
}
if cfg.Options.HTTPRedirectAddr == "" {
return
}
redirect := httputil.RedirectHandler()
hsrv := &http.Server{
Addr: cfg.Options.HTTPRedirectAddr,
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if mgr.handleHTTPChallenge(w, r) {
return
}
redirect.ServeHTTP(w, r)
}),
}
go func() {
li, err := net.Listen("tcp", cfg.Options.HTTPRedirectAddr)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("failed to listen on http redirect addr")
return
}
defer li.Close()
if cfg.Options.UseProxyProtocol {
li = &proxyproto.Listener{
Listener: li,
ReadHeaderTimeout: 10 * time.Second,
}
}
log.Ctx(ctx).Info().Str("addr", hsrv.Addr).Msg("starting http redirect server")
err = hsrv.Serve(li)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("failed to run http redirect server")
}
}()
mgr.srv = hsrv
}
func (mgr *Manager) updateACMETLSALPNServer(ctx context.Context, cfg *config.Config) {
mgr.acmeTLSALPNLock.Lock()
defer mgr.acmeTLSALPNLock.Unlock()
// store the updated TLS config
mgr.acmeTLSALPNConfig = mgr.certmagic.TLSConfig().Clone()
// if the port hasn't changed, we're done
if mgr.acmeTLSALPNPort == cfg.ACMETLSALPNPort {
return
}
// store the updated port
mgr.acmeTLSALPNPort = cfg.ACMETLSALPNPort
if mgr.acmeTLSALPNListener != nil {
_ = mgr.acmeTLSALPNListener.Close()
mgr.acmeTLSALPNListener = nil
}
// start the listener
addr := net.JoinHostPort("127.0.0.1", cfg.ACMETLSALPNPort)
ln, err := net.Listen("tcp", addr)
if err != nil {
log.Ctx(ctx).Error().Err(err).Msg("failed to run acme tls alpn server")
return
}
mgr.acmeTLSALPNListener = ln
// accept connections
go func() {
for {
conn, err := ln.Accept()
if errors.Is(err, net.ErrClosed) {
return
} else if err != nil {
continue
}
// initiate the TLS handshake
mgr.acmeTLSALPNLock.Lock()
tlsConfig := mgr.acmeTLSALPNConfig.Clone()
mgr.acmeTLSALPNLock.Unlock()
orig := tlsConfig.GetCertificate
tlsConfig.GetCertificate = func(chi *tls.ClientHelloInfo) (*tls.Certificate, error) {
log.Ctx(ctx).Info().Str("server-name", chi.ServerName).
Msg("received request for ACME TLS ALPN certificate")
return orig(chi)
}
_ = tls.Server(conn, tlsConfig).HandshakeContext(ctx)
_ = conn.Close()
}
}()
}
func (mgr *Manager) handleHTTPChallenge(w http.ResponseWriter, r *http.Request) bool {
return mgr.acmeMgr.Load().HandleHTTPChallenge(w, r)
}
// GetConfig gets the config.
func (mgr *Manager) GetConfig() *config.Config {
mgr.mu.RLock()
defer mgr.mu.RUnlock()
return mgr.config
}
// configureCertificateAuthority configures the acmeMgr ACME Certificate Authority settings.
func configureCertificateAuthority(acmeMgr *certmagic.ACMEIssuer, opts config.AutocertOptions) error {
acmeMgr.Agreed = true
if opts.UseStaging {
acmeMgr.CA = acmeMgr.TestCA
}
if opts.CA != "" {
acmeMgr.CA = opts.CA // when a CA is specified, it overrides the staging setting
}
if opts.Email != "" {
acmeMgr.Email = opts.Email
} else {
acmeMgr.Email = " " // intentionally set to a space so that certmagic doesn't prompt for an email address
}
return nil
}
// configureExternalAccountBinding configures the acmeMgr ACME External Account Binding settings.
func configureExternalAccountBinding(acmeMgr *certmagic.ACMEIssuer, opts config.AutocertOptions) error {
if opts.EABKeyID != "" || opts.EABMACKey != "" {
acmeMgr.ExternalAccount = &acme.EAB{}
}
if opts.EABKeyID != "" {
acmeMgr.ExternalAccount.KeyID = opts.EABKeyID
}
if opts.EABMACKey != "" {
_, err := base64.RawURLEncoding.DecodeString(opts.EABMACKey)
if err != nil {
return fmt.Errorf("config: decoding base64-urlencoded MAC Key: %w", err)
}
acmeMgr.ExternalAccount.MACKey = opts.EABMACKey
}
return nil
}
// configureTrustedRoots configures the acmeMgr x509 roots to trust when communicating with an ACME CA.
func configureTrustedRoots(acmeMgr *certmagic.ACMEIssuer, opts config.AutocertOptions) error {
if opts.TrustedCA != "" {
// pool effectively contains the certificate(s) in the TrustedCA base64 PEM appended to the system roots
pool, err := cryptutil.GetCertPool(opts.TrustedCA, "")
if err != nil {
return fmt.Errorf("config: creating trusted certificate pool: %w", err)
}
acmeMgr.TrustedRoots = pool
}
if opts.TrustedCAFile != "" {
// pool effectively contains the certificate(s) in TrustedCAFile appended to the system roots
pool, err := cryptutil.GetCertPool("", opts.TrustedCAFile)
if err != nil {
return fmt.Errorf("config: creating trusted certificate pool: %w", err)
}
acmeMgr.TrustedRoots = pool
}
return nil
}
func sourceHostnames(cfg *config.Config) []string {
if cfg.Options.NumPolicies() == 0 {
return nil
}
dedupe := map[string]struct{}{}
for p := range cfg.Options.GetAllPolicies() {
if u, _ := urlutil.ParseAndValidateURL(p.From); u != nil && !strings.Contains(u.Host, "*") {
dedupe[u.Hostname()] = struct{}{}
}
}
if cfg.Options.AuthenticateURLString != "" {
if u, _ := cfg.Options.GetAuthenticateURL(); u != nil {
dedupe[u.Hostname()] = struct{}{}
}
}
// remove any hosted authenticate URLs
for _, domain := range urlutil.HostedAuthenticateDomains {
delete(dedupe, domain)
}
var h []string
for k := range dedupe {
h = append(h, k)
}
sort.Strings(h)
return h
}
func shouldEnableHTTPChallenge(cfg *config.Config) bool {
if cfg == nil || cfg.Options == nil {
return false
}
_, p, err := net.SplitHostPort(cfg.Options.HTTPRedirectAddr)
if err != nil {
return false
}
return p == "80"
}