veza/veza-backend-api/internal/infrastructure/ssl/certificate_manager.go
2025-12-03 20:29:37 +01:00

597 lines
16 KiB
Go

package ssl
import (
"context"
"crypto/x509"
"fmt"
"sync"
"time"
"go.uber.org/zap"
)
// CertificateManager gère les certificats SSL automatiquement
type CertificateManager struct {
logger *zap.Logger
config CertificateConfig
certStore map[string]*Certificate
providers map[string]CertificateProvider
monitor *CertificateMonitor
scheduler *RenewalScheduler
mu sync.RWMutex
isRunning bool
}
// CertificateConfig configuration des certificats
type CertificateConfig struct {
Enabled bool `yaml:"enabled"`
AutoRenewal bool `yaml:"auto_renewal"`
RenewalThreshold time.Duration `yaml:"renewal_threshold"` // 30 jours par défaut
CheckInterval time.Duration `yaml:"check_interval"` // 6 heures par défaut
Provider string `yaml:"provider"` // "letsencrypt", "self-signed", "manual"
EmailNotifications bool `yaml:"email_notifications"`
SlackNotifications bool `yaml:"slack_notifications"`
StoreType string `yaml:"store_type"` // "filesystem", "kubernetes", "vault"
StorePath string `yaml:"store_path"`
BackupEnabled bool `yaml:"backup_enabled"`
BackupPath string `yaml:"backup_path"`
Domains []DomainConfig `yaml:"domains"`
}
// DomainConfig configuration par domaine
type DomainConfig struct {
Domain string `yaml:"domain"`
Aliases []string `yaml:"aliases"`
CertificatePath string `yaml:"certificate_path"`
PrivateKeyPath string `yaml:"private_key_path"`
AutoRenew bool `yaml:"auto_renew"`
Provider string `yaml:"provider"`
Contact string `yaml:"contact"`
}
// Certificate représente un certificat SSL
type Certificate struct {
ID string `json:"id"`
Domain string `json:"domain"`
Aliases []string `json:"aliases"`
Provider string `json:"provider"`
Certificate *x509.Certificate `json:"-"`
PrivateKey interface{} `json:"-"`
PEMData []byte `json:"-"`
KeyData []byte `json:"-"`
Status CertificateStatus `json:"status"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
LastChecked time.Time `json:"last_checked"`
AutoRenew bool `json:"auto_renew"`
Contact string `json:"contact"`
Metadata map[string]interface{} `json:"metadata"`
}
// CertificateStatus statut du certificat
type CertificateStatus string
const (
CertStatusValid CertificateStatus = "valid"
CertStatusExpiring CertificateStatus = "expiring"
CertStatusExpired CertificateStatus = "expired"
CertStatusRevoking CertificateStatus = "revoking"
CertStatusRevoked CertificateStatus = "revoked"
CertStatusRenewing CertificateStatus = "renewing"
CertStatusError CertificateStatus = "error"
)
// CertificateProvider interface pour les fournisseurs de certificats
type CertificateProvider interface {
GenerateCertificate(ctx context.Context, domain string, aliases []string, contact string) (*Certificate, error)
RenewCertificate(ctx context.Context, cert *Certificate) (*Certificate, error)
RevokeCertificate(ctx context.Context, cert *Certificate) error
ValidateCertificate(ctx context.Context, cert *Certificate) error
GetCertificateInfo(ctx context.Context, domain string) (*Certificate, error)
}
// CertificateMonitor surveille l'état des certificats
type CertificateMonitor struct {
logger *zap.Logger
manager *CertificateManager
isRunning bool
mu sync.RWMutex
}
// RenewalScheduler planifie les renouvellements
type RenewalScheduler struct {
logger *zap.Logger
manager *CertificateManager
renewalQueue chan *Certificate
isRunning bool
mu sync.RWMutex
}
// NewCertificateManager crée un nouveau gestionnaire de certificats
func NewCertificateManager(config CertificateConfig, logger *zap.Logger) *CertificateManager {
cm := &CertificateManager{
logger: logger,
config: config,
certStore: make(map[string]*Certificate),
providers: make(map[string]CertificateProvider),
}
// Initialiser le monitor
cm.monitor = &CertificateMonitor{
logger: logger,
manager: cm,
}
// Initialiser le scheduler
cm.scheduler = &RenewalScheduler{
logger: logger,
manager: cm,
renewalQueue: make(chan *Certificate, 100),
}
return cm
}
// Initialize initialise le gestionnaire de certificats
func (cm *CertificateManager) Initialize(ctx context.Context) error {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.logger.Info("Initializing Certificate Manager")
// Initialiser les providers
if err := cm.initializeProviders(); err != nil {
return fmt.Errorf("failed to initialize providers: %w", err)
}
// Charger les certificats existants
if err := cm.loadExistingCertificates(); err != nil {
return fmt.Errorf("failed to load existing certificates: %w", err)
}
cm.logger.Info("Certificate Manager initialized successfully")
return nil
}
// Start démarre le gestionnaire de certificats
func (cm *CertificateManager) Start(ctx context.Context) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if cm.isRunning {
return nil
}
cm.logger.Info("Starting Certificate Manager")
// Démarrer le monitor
go cm.monitor.Start(ctx)
// Démarrer le scheduler
go cm.scheduler.Start(ctx)
// Démarrer le monitoring périodique
go cm.startPeriodicChecks(ctx)
cm.isRunning = true
cm.logger.Info("Certificate Manager started successfully")
return nil
}
// Stop arrête le gestionnaire de certificats
func (cm *CertificateManager) Stop(ctx context.Context) error {
cm.mu.Lock()
defer cm.mu.Unlock()
if !cm.isRunning {
return nil
}
cm.logger.Info("Stopping Certificate Manager")
// Arrêter les composants
cm.monitor.Stop()
cm.scheduler.Stop()
cm.isRunning = false
cm.logger.Info("Certificate Manager stopped")
return nil
}
// GetCertificate récupère un certificat par domaine
func (cm *CertificateManager) GetCertificate(domain string) (*Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
cert, exists := cm.certStore[domain]
if !exists {
return nil, fmt.Errorf("certificate not found for domain: %s", domain)
}
return cert, nil
}
// RequestCertificate demande un nouveau certificat
func (cm *CertificateManager) RequestCertificate(ctx context.Context, domain string, aliases []string, contact string) (*Certificate, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.logger.Info("Requesting certificate", zap.String("domain", domain))
// Vérifier si le certificat existe déjà
if cert, exists := cm.certStore[domain]; exists {
if cert.Status == CertStatusValid && time.Until(cert.ExpiresAt) > cm.config.RenewalThreshold {
cm.logger.Info("Certificate already exists and is valid", zap.String("domain", domain))
return cert, nil
}
}
// Obtenir le provider
provider, err := cm.getProvider(cm.config.Provider)
if err != nil {
return nil, fmt.Errorf("failed to get provider: %w", err)
}
// Générer le certificat
cert, err := provider.GenerateCertificate(ctx, domain, aliases, contact)
if err != nil {
return nil, fmt.Errorf("failed to generate certificate: %w", err)
}
// Stocker le certificat
cm.certStore[domain] = cert
// Sauvegarder sur disque
if err := cm.saveCertificate(cert); err != nil {
cm.logger.Error("Failed to save certificate", zap.Error(err))
}
cm.logger.Info("Certificate generated successfully", zap.String("domain", domain))
return cert, nil
}
// RenewCertificate renouvelle un certificat
func (cm *CertificateManager) RenewCertificate(ctx context.Context, domain string) (*Certificate, error) {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.logger.Info("Renewing certificate", zap.String("domain", domain))
cert, exists := cm.certStore[domain]
if !exists {
return nil, fmt.Errorf("certificate not found for domain: %s", domain)
}
// Obtenir le provider
provider, err := cm.getProvider(cert.Provider)
if err != nil {
return nil, fmt.Errorf("failed to get provider: %w", err)
}
// Marquer comme en cours de renouvellement
cert.Status = CertStatusRenewing
// Renouveler le certificat
newCert, err := provider.RenewCertificate(ctx, cert)
if err != nil {
cert.Status = CertStatusError
return nil, fmt.Errorf("failed to renew certificate: %w", err)
}
// Remplacer le certificat
cm.certStore[domain] = newCert
// Sauvegarder sur disque
if err := cm.saveCertificate(newCert); err != nil {
cm.logger.Error("Failed to save renewed certificate", zap.Error(err))
}
cm.logger.Info("Certificate renewed successfully", zap.String("domain", domain))
return newCert, nil
}
// RevokeCertificate révoque un certificat
func (cm *CertificateManager) RevokeCertificate(ctx context.Context, domain string) error {
cm.mu.Lock()
defer cm.mu.Unlock()
cm.logger.Info("Revoking certificate", zap.String("domain", domain))
cert, exists := cm.certStore[domain]
if !exists {
return fmt.Errorf("certificate not found for domain: %s", domain)
}
// Obtenir le provider
provider, err := cm.getProvider(cert.Provider)
if err != nil {
return fmt.Errorf("failed to get provider: %w", err)
}
// Révoquer le certificat
if err := provider.RevokeCertificate(ctx, cert); err != nil {
return fmt.Errorf("failed to revoke certificate: %w", err)
}
// Marquer comme révoqué
cert.Status = CertStatusRevoked
cm.logger.Info("Certificate revoked successfully", zap.String("domain", domain))
return nil
}
// ListCertificates liste tous les certificats
func (cm *CertificateManager) ListCertificates() map[string]*Certificate {
cm.mu.RLock()
defer cm.mu.RUnlock()
result := make(map[string]*Certificate)
for k, v := range cm.certStore {
result[k] = v
}
return result
}
// GetCertificateStatus retourne le statut d'un certificat
func (cm *CertificateManager) GetCertificateStatus(domain string) (CertificateStatus, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
cert, exists := cm.certStore[domain]
if !exists {
return "", fmt.Errorf("certificate not found for domain: %s", domain)
}
return cert.Status, nil
}
// CheckCertificateExpiry vérifie l'expiration des certificats
func (cm *CertificateManager) CheckCertificateExpiry() ([]*Certificate, error) {
cm.mu.RLock()
defer cm.mu.RUnlock()
var expiringCerts []*Certificate
now := time.Now()
for _, cert := range cm.certStore {
timeUntilExpiry := cert.ExpiresAt.Sub(now)
// Mettre à jour le statut
if timeUntilExpiry <= 0 {
cert.Status = CertStatusExpired
} else if timeUntilExpiry <= cm.config.RenewalThreshold {
cert.Status = CertStatusExpiring
expiringCerts = append(expiringCerts, cert)
} else {
cert.Status = CertStatusValid
}
cert.LastChecked = now
}
return expiringCerts, nil
}
// Méthodes privées
func (cm *CertificateManager) initializeProviders() error {
// Initialiser le provider Let's Encrypt
letsEncryptProvider := NewLetsEncryptProvider(cm.logger)
cm.providers["letsencrypt"] = letsEncryptProvider
// Initialiser le provider self-signed
selfSignedProvider := NewSelfSignedProvider(cm.logger)
cm.providers["self-signed"] = selfSignedProvider
return nil
}
func (cm *CertificateManager) loadExistingCertificates() error {
// Charger les certificats depuis le store configuré
// Implémentation simplifiée
for _, domainConfig := range cm.config.Domains {
if domainConfig.CertificatePath != "" {
cert, err := cm.loadCertificateFromFile(domainConfig)
if err != nil {
cm.logger.Warn("Failed to load certificate from file",
zap.String("domain", domainConfig.Domain),
zap.Error(err))
continue
}
cm.certStore[domainConfig.Domain] = cert
}
}
return nil
}
func (cm *CertificateManager) loadCertificateFromFile(config DomainConfig) (*Certificate, error) {
// Implémentation simplifiée - charger depuis fichier
cert := &Certificate{
Domain: config.Domain,
Aliases: config.Aliases,
Provider: config.Provider,
Status: CertStatusValid,
IssuedAt: time.Now().AddDate(0, -1, 0), // 1 mois avant
ExpiresAt: time.Now().AddDate(0, 2, 0), // 2 mois après
LastChecked: time.Now(),
AutoRenew: config.AutoRenew,
Contact: config.Contact,
Metadata: make(map[string]interface{}),
}
return cert, nil
}
func (cm *CertificateManager) saveCertificate(cert *Certificate) error {
// Sauvegarder le certificat selon la configuration
// Implémentation simplifiée
cm.logger.Info("Certificate saved", zap.String("domain", cert.Domain))
return nil
}
func (cm *CertificateManager) getProvider(providerName string) (CertificateProvider, error) {
provider, exists := cm.providers[providerName]
if !exists {
return nil, fmt.Errorf("provider not found: %s", providerName)
}
return provider, nil
}
func (cm *CertificateManager) startPeriodicChecks(ctx context.Context) {
ticker := time.NewTicker(cm.config.CheckInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
cm.performPeriodicCheck()
case <-ctx.Done():
return
}
}
}
func (cm *CertificateManager) performPeriodicCheck() {
cm.logger.Debug("Performing periodic certificate check")
// Vérifier l'expiration des certificats
expiringCerts, err := cm.CheckCertificateExpiry()
if err != nil {
cm.logger.Error("Failed to check certificate expiry", zap.Error(err))
return
}
// Planifier le renouvellement des certificats expirants
for _, cert := range expiringCerts {
if cert.AutoRenew {
cm.scheduler.ScheduleRenewal(cert)
}
}
}
// CertificateMonitor methods
func (monitor *CertificateMonitor) Start(ctx context.Context) {
monitor.mu.Lock()
defer monitor.mu.Unlock()
if monitor.isRunning {
return
}
monitor.logger.Info("Starting Certificate Monitor")
monitor.isRunning = true
go monitor.monitorCertificates(ctx)
}
func (monitor *CertificateMonitor) Stop() {
monitor.mu.Lock()
defer monitor.mu.Unlock()
monitor.isRunning = false
monitor.logger.Info("Certificate Monitor stopped")
}
func (monitor *CertificateMonitor) monitorCertificates(ctx context.Context) {
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for {
select {
case <-ticker.C:
monitor.checkCertificateHealth()
case <-ctx.Done():
return
}
}
}
func (monitor *CertificateMonitor) checkCertificateHealth() {
monitor.logger.Debug("Checking certificate health")
certs := monitor.manager.ListCertificates()
for domain, cert := range certs {
// Vérifier la validité du certificat
if err := monitor.validateCertificate(cert); err != nil {
monitor.logger.Error("Certificate validation failed",
zap.String("domain", domain),
zap.Error(err))
cert.Status = CertStatusError
}
}
}
func (monitor *CertificateMonitor) validateCertificate(cert *Certificate) error {
// Validation basique du certificat
if cert.Certificate != nil {
now := time.Now()
if now.Before(cert.Certificate.NotBefore) || now.After(cert.Certificate.NotAfter) {
return fmt.Errorf("certificate is not valid for current time")
}
}
return nil
}
// RenewalScheduler methods
func (scheduler *RenewalScheduler) Start(ctx context.Context) {
scheduler.mu.Lock()
defer scheduler.mu.Unlock()
if scheduler.isRunning {
return
}
scheduler.logger.Info("Starting Renewal Scheduler")
scheduler.isRunning = true
go scheduler.processRenewals(ctx)
}
func (scheduler *RenewalScheduler) Stop() {
scheduler.mu.Lock()
defer scheduler.mu.Unlock()
scheduler.isRunning = false
close(scheduler.renewalQueue)
scheduler.logger.Info("Renewal Scheduler stopped")
}
func (scheduler *RenewalScheduler) ScheduleRenewal(cert *Certificate) {
if !scheduler.isRunning {
return
}
select {
case scheduler.renewalQueue <- cert:
scheduler.logger.Info("Certificate renewal scheduled", zap.String("domain", cert.Domain))
default:
scheduler.logger.Warn("Renewal queue is full", zap.String("domain", cert.Domain))
}
}
func (scheduler *RenewalScheduler) processRenewals(ctx context.Context) {
for {
select {
case cert := <-scheduler.renewalQueue:
if cert != nil {
scheduler.renewCertificate(ctx, cert)
}
case <-ctx.Done():
return
}
}
}
func (scheduler *RenewalScheduler) renewCertificate(ctx context.Context, cert *Certificate) {
scheduler.logger.Info("Processing certificate renewal", zap.String("domain", cert.Domain))
_, err := scheduler.manager.RenewCertificate(ctx, cert.Domain)
if err != nil {
scheduler.logger.Error("Failed to renew certificate",
zap.String("domain", cert.Domain),
zap.Error(err))
} else {
scheduler.logger.Info("Certificate renewed successfully", zap.String("domain", cert.Domain))
}
}