add cert manager with self signed cert support

This commit is contained in:
pascal
2026-01-15 17:54:16 +01:00
parent 7527e0ebdb
commit fcb849698f
8 changed files with 444 additions and 159 deletions

View File

@@ -0,0 +1,28 @@
package certmanager
import (
"context"
"crypto/tls"
"net/http"
)
// Manager defines the interface for certificate management
type Manager interface {
// IsEnabled returns whether certificate management is enabled
IsEnabled() bool
// AddDomain adds a domain to the allowed hosts list
AddDomain(domain string)
// RemoveDomain removes a domain from the allowed hosts list
RemoveDomain(domain string)
// IssueCertificate eagerly issues a certificate for a domain
IssueCertificate(ctx context.Context, domain string) error
// TLSConfig returns the TLS configuration for the HTTPS server
TLSConfig() *tls.Config
// HTTPHandler returns the HTTP handler for ACME challenges (or fallback)
HTTPHandler(fallback http.Handler) http.Handler
}

View File

@@ -0,0 +1,113 @@
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
}
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)
// Use GetCertificate to trigger certificate issuance
// This will go through the ACME challenge flow
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)
}

View File

@@ -0,0 +1,166 @@
package certmanager
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"fmt"
"math/big"
"net/http"
"sync"
"time"
log "github.com/sirupsen/logrus"
)
// SelfSignedManager handles self-signed certificate generation for local testing
type SelfSignedManager struct {
certificates map[string]*tls.Certificate // domain -> certificate cache
mu sync.RWMutex
}
// NewSelfSigned creates a new self-signed certificate manager
func NewSelfSigned() *SelfSignedManager {
log.Info("Self-signed certificate manager initialized")
return &SelfSignedManager{
certificates: make(map[string]*tls.Certificate),
}
}
// IsEnabled returns whether certificate management is enabled
func (m *SelfSignedManager) IsEnabled() bool {
return true
}
// AddDomain adds a domain to the manager (no-op for self-signed, but maintains interface)
func (m *SelfSignedManager) AddDomain(domain string) {
log.Infof("Added domain to self-signed manager: %s", domain)
}
// RemoveDomain removes a domain from the manager
func (m *SelfSignedManager) RemoveDomain(domain string) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.certificates, domain)
log.Infof("Removed domain from self-signed manager: %s", domain)
}
// IssueCertificate generates and caches a self-signed certificate for a domain
func (m *SelfSignedManager) IssueCertificate(ctx context.Context, domain string) error {
m.mu.Lock()
defer m.mu.Unlock()
// Check if we already have a certificate for this domain
if _, exists := m.certificates[domain]; exists {
log.Debugf("Self-signed certificate already exists for domain: %s", domain)
return nil
}
// Generate self-signed certificate
cert, err := m.generateCertificate(domain)
if err != nil {
return err
}
// Cache the certificate
m.certificates[domain] = cert
return nil
}
// TLSConfig returns the TLS configuration for the HTTPS server
func (m *SelfSignedManager) TLSConfig() *tls.Config {
return &tls.Config{
GetCertificate: m.getCertificate,
}
}
// HTTPHandler returns the fallback handler (no ACME challenges for self-signed)
func (m *SelfSignedManager) HTTPHandler(fallback http.Handler) http.Handler {
return fallback
}
// getCertificate returns a self-signed certificate for the requested domain
func (m *SelfSignedManager) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
m.mu.RLock()
cert, exists := m.certificates[hello.ServerName]
m.mu.RUnlock()
if exists {
return cert, nil
}
// Generate certificate on-demand if not cached
log.Infof("Generating self-signed certificate on-demand for: %s", hello.ServerName)
newCert, err := m.generateCertificate(hello.ServerName)
if err != nil {
return nil, err
}
// Cache it
m.mu.Lock()
m.certificates[hello.ServerName] = newCert
m.mu.Unlock()
return newCert, nil
}
// generateCertificate generates a self-signed certificate for a domain
func (m *SelfSignedManager) generateCertificate(domain string) (*tls.Certificate, error) {
// Generate private key
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, fmt.Errorf("failed to generate private key: %w", err)
}
// Create certificate template
notBefore := time.Now()
notAfter := notBefore.Add(365 * 24 * time.Hour) // Valid for 1 year
serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128))
if err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err)
}
template := x509.Certificate{
SerialNumber: serialNumber,
Subject: pkix.Name{
Organization: []string{"NetBird Local Development"},
CommonName: domain,
},
NotBefore: notBefore,
NotAfter: notAfter,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
BasicConstraintsValid: true,
DNSNames: []string{domain},
}
// Create self-signed certificate
certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
if err != nil {
return nil, fmt.Errorf("failed to create certificate: %w", err)
}
// Parse certificate
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, fmt.Errorf("failed to parse certificate: %w", err)
}
tlsCert := &tls.Certificate{
Certificate: [][]byte{certDER},
PrivateKey: priv,
Leaf: cert,
}
log.Infof("Generated self-signed certificate for domain: %s (expires: %s)",
domain, cert.NotAfter.Format(time.RFC3339))
return tlsCert, nil
}