Files
netbird/proxy/internal/reverseproxy/certmanager/letsencrypt.go
2026-01-16 12:01:52 +01:00

112 lines
3.1 KiB
Go

package certmanager
import (
"context"
"crypto/tls"
"fmt"
"net/http"
"sync"
"time"
log "github.com/sirupsen/logrus"
"golang.org/x/crypto/acme/autocert"
)
// LetsEncryptManager handles TLS certificate issuance via Let's Encrypt
type LetsEncryptManager struct {
autocertManager *autocert.Manager
allowedHosts map[string]bool
mu sync.RWMutex
}
// LetsEncryptConfig holds Let's Encrypt certificate manager configuration
type LetsEncryptConfig struct {
// Email for Let's Encrypt registration (required)
Email string
// CertCacheDir is the directory to cache certificates
CertCacheDir string
}
// NewLetsEncrypt creates a new Let's Encrypt certificate manager
func NewLetsEncrypt(config LetsEncryptConfig) *LetsEncryptManager {
m := &LetsEncryptManager{
allowedHosts: make(map[string]bool),
}
m.autocertManager = &autocert.Manager{
Prompt: autocert.AcceptTOS,
HostPolicy: m.hostPolicy,
Cache: autocert.DirCache(config.CertCacheDir),
Email: config.Email,
RenewBefore: 0, // Use default 30 days prior to expiration
}
log.Info("Let's Encrypt certificate manager initialized")
return m
}
// IsEnabled returns whether certificate management is enabled
func (m *LetsEncryptManager) IsEnabled() bool {
return true
}
// AddDomain adds a domain to the allowed hosts list
func (m *LetsEncryptManager) AddDomain(domain string) {
m.mu.Lock()
defer m.mu.Unlock()
m.allowedHosts[domain] = true
log.Infof("Added domain to Let's Encrypt manager: %s", domain)
}
// RemoveDomain removes a domain from the allowed hosts list
func (m *LetsEncryptManager) RemoveDomain(domain string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.allowedHosts, domain)
log.Infof("Removed domain from Let's Encrypt manager: %s", domain)
}
// IssueCertificate eagerly issues a Let's Encrypt certificate for a domain
func (m *LetsEncryptManager) IssueCertificate(ctx context.Context, domain string) error {
log.Infof("Issuing Let's Encrypt certificate for domain: %s", domain)
hello := &tls.ClientHelloInfo{
ServerName: domain,
}
cert, err := m.autocertManager.GetCertificate(hello)
if err != nil {
return fmt.Errorf("failed to issue certificate for domain %s: %w", domain, err)
}
log.Infof("Successfully issued Let's Encrypt certificate for domain: %s (expires: %s)",
domain, cert.Leaf.NotAfter.Format(time.RFC3339))
return nil
}
// TLSConfig returns the TLS configuration for the HTTPS server
func (m *LetsEncryptManager) TLSConfig() *tls.Config {
return m.autocertManager.TLSConfig()
}
// HTTPHandler returns the HTTP handler for ACME challenges
func (m *LetsEncryptManager) HTTPHandler(fallback http.Handler) http.Handler {
return m.autocertManager.HTTPHandler(fallback)
}
// hostPolicy validates that a requested host is in the allowed hosts list
func (m *LetsEncryptManager) hostPolicy(ctx context.Context, host string) error {
m.mu.RLock()
defer m.mu.RUnlock()
if m.allowedHosts[host] {
log.Debugf("ACME challenge accepted for domain: %s", host)
return nil
}
log.Warnf("ACME challenge rejected for unconfigured domain: %s", host)
return fmt.Errorf("host %s not configured", host)
}