351 lines
12 KiB
Go
351 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"certigo-addon-backend/providers"
|
|
)
|
|
|
|
// StartRenewalScheduler startet den Scheduler für automatische Zertifikatserneuerungen
|
|
// Der Scheduler prüft regelmäßig die renewal_queue auf fällige Erneuerungen
|
|
func StartRenewalScheduler() {
|
|
go func() {
|
|
ticker := time.NewTicker(5 * time.Minute) // Prüfe alle 5 Minuten
|
|
defer ticker.Stop()
|
|
|
|
log.Println("Renewal Scheduler gestartet - prüfe alle 5 Minuten auf fällige Erneuerungen")
|
|
|
|
// Führe sofort eine Prüfung durch beim Start
|
|
processRenewalQueue()
|
|
|
|
// Dann in regelmäßigen Intervallen
|
|
for range ticker.C {
|
|
processRenewalQueue()
|
|
}
|
|
}()
|
|
}
|
|
|
|
// processRenewalQueue prüft die renewal_queue auf fällige Erneuerungen und führt sie aus
|
|
func processRenewalQueue() {
|
|
// Verwende kürzeren Context für die Queue-Abfrage, um Datenbankblockierungen zu vermeiden
|
|
queryCtx, queryCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer queryCancel()
|
|
|
|
now := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
|
|
// Hole alle fälligen Queue-Einträge (scheduled_at <= now und status = 'pending')
|
|
rows, err := db.QueryContext(queryCtx, `
|
|
SELECT id, certificate_id, fqdn_id, space_id, scheduled_at
|
|
FROM renewal_queue
|
|
WHERE status = 'pending' AND scheduled_at <= ?
|
|
ORDER BY scheduled_at ASC
|
|
LIMIT 10
|
|
`, now)
|
|
|
|
if err != nil {
|
|
log.Printf("Fehler beim Abfragen der renewal_queue: %v", err)
|
|
return
|
|
}
|
|
defer rows.Close()
|
|
|
|
var processedCount int
|
|
for rows.Next() {
|
|
var queueID, certID, fqdnID, spaceID, scheduledAt string
|
|
if err := rows.Scan(&queueID, &certID, &fqdnID, &spaceID, &scheduledAt); err != nil {
|
|
log.Printf("Fehler beim Scannen der Queue-Zeile: %v", err)
|
|
continue
|
|
}
|
|
|
|
// Markiere als in Bearbeitung (mit angemessenem Timeout)
|
|
// Verwende Retry-Logik für Datenbankoperationen, um Blockierungen zu handhaben
|
|
var updateErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
updateCtx, updateCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, updateErr = db.ExecContext(updateCtx, `
|
|
UPDATE renewal_queue
|
|
SET status = 'processing', processed_at = ?
|
|
WHERE id = ?
|
|
`, time.Now().UTC().Format("2006-01-02 15:04:05"), queueID)
|
|
updateCancel()
|
|
if updateErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
log.Printf("Warnung: Fehler beim Aktualisieren des Queue-Status (Versuch %d/3), versuche erneut: %v", retry+1, updateErr)
|
|
time.Sleep(time.Second * time.Duration(retry+1)) // Exponential backoff
|
|
}
|
|
}
|
|
if updateErr != nil {
|
|
log.Printf("Fehler beim Aktualisieren des Queue-Status nach 3 Versuchen: %v", updateErr)
|
|
continue
|
|
}
|
|
|
|
// Führe Erneuerung durch (in separater Goroutine, um Datenbankblockierungen zu vermeiden)
|
|
go func(qID, cID, fID, sID string) {
|
|
if err := processCertificateRenewal(cID, fID, sID, qID); err != nil {
|
|
log.Printf("Fehler bei der Zertifikatserneuerung (Queue ID: %s): %v", qID, err)
|
|
// Markiere als fehlgeschlagen (mit Retry-Logik)
|
|
var updateErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
errorCtx, errorCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, updateErr = db.ExecContext(errorCtx, `
|
|
UPDATE renewal_queue
|
|
SET status = 'failed', error_message = ?
|
|
WHERE id = ?
|
|
`, err.Error(), qID)
|
|
errorCancel()
|
|
if updateErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
time.Sleep(time.Second * time.Duration(retry+1))
|
|
}
|
|
}
|
|
if updateErr != nil {
|
|
log.Printf("Fehler beim Aktualisieren des Fehlerstatus nach 3 Versuchen: %v", updateErr)
|
|
}
|
|
return
|
|
}
|
|
|
|
// Markiere als erfolgreich (mit Retry-Logik)
|
|
var successErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
successCtx, successCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, successErr = db.ExecContext(successCtx, `
|
|
UPDATE renewal_queue
|
|
SET status = 'completed'
|
|
WHERE id = ?
|
|
`, qID)
|
|
successCancel()
|
|
if successErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
time.Sleep(time.Second * time.Duration(retry+1))
|
|
}
|
|
}
|
|
if successErr != nil {
|
|
log.Printf("Fehler beim Markieren als erfolgreich nach 3 Versuchen: %v", successErr)
|
|
} else {
|
|
log.Printf("Zertifikatserneuerung erfolgreich verarbeitet (Queue ID: %s, Cert ID: %s, FQDN: %s)", qID, cID, fID)
|
|
}
|
|
}(queueID, certID, fqdnID, spaceID)
|
|
|
|
processedCount++
|
|
}
|
|
|
|
if processedCount > 0 {
|
|
log.Printf("Renewal Queue: %d Erneuerungen gestartet", processedCount)
|
|
}
|
|
}
|
|
|
|
// processCertificateRenewal führt die tatsächliche Zertifikatserneuerung durch
|
|
func processCertificateRenewal(certID, fqdnID, spaceID, queueID string) error {
|
|
// Lade FQDN-Daten (mit angemessenem Timeout)
|
|
var fqdn FQDN
|
|
var acmeProviderID, acmeUsername, acmePassword, acmeEmail, acmeKeyID sql.NullString
|
|
var renewalEnabled sql.NullInt64
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
|
|
err := db.QueryRowContext(ctx, `
|
|
SELECT id, space_id, fqdn, acme_provider_id, acme_username, acme_password, acme_email, acme_key_id, COALESCE(renewal_enabled, 1)
|
|
FROM fqdns
|
|
WHERE id = ? AND space_id = ?
|
|
`, fqdnID, spaceID).Scan(
|
|
&fqdn.ID, &fqdn.SpaceID, &fqdn.FQDN,
|
|
&acmeProviderID, &acmeUsername, &acmePassword, &acmeEmail, &acmeKeyID, &renewalEnabled,
|
|
)
|
|
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if acmeProviderID.String != "certigo-acmeproxy" || !acmeUsername.Valid || !acmePassword.Valid || !acmeEmail.Valid {
|
|
return fmt.Errorf("fqdn hat keine gültigen ACME-Daten")
|
|
}
|
|
|
|
fqdn.AcmeProviderID = acmeProviderID.String
|
|
fqdn.AcmeUsername = acmeUsername.String
|
|
fqdn.AcmePassword = acmePassword.String
|
|
fqdn.AcmeEmail = acmeEmail.String
|
|
if acmeKeyID.Valid {
|
|
fqdn.AcmeKeyID = acmeKeyID.String
|
|
}
|
|
|
|
// Lade Provider-Konfiguration
|
|
pm := providers.GetManager()
|
|
provider, exists := pm.GetProvider("certigo-acmeproxy")
|
|
if !exists || provider == nil {
|
|
return fmt.Errorf("acme provider nicht gefunden")
|
|
}
|
|
|
|
config, err := pm.GetProviderConfig("certigo-acmeproxy")
|
|
if err != nil {
|
|
return fmt.Errorf("Fehler beim Laden der Provider-Konfiguration: %v", err)
|
|
}
|
|
|
|
acmeProxyProvider, ok := provider.(*providers.CertigoACMEProxyProvider)
|
|
if !ok {
|
|
return fmt.Errorf("Ungültiger Provider-Typ")
|
|
}
|
|
|
|
// Erstelle Update- und Cleanup-Funktionen (mit Retry-Logik)
|
|
updateTokenFunc := func(token string) error {
|
|
var updateErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
updateCtx, updateCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, updateErr = db.ExecContext(updateCtx, "UPDATE fqdns SET acme_challenge_token = ? WHERE id = ?", token, fqdnID)
|
|
updateCancel()
|
|
if updateErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
time.Sleep(time.Second * time.Duration(retry+1))
|
|
}
|
|
}
|
|
if updateErr != nil {
|
|
return fmt.Errorf("fehler beim Speichern des Challenge-Tokens: %v", updateErr)
|
|
}
|
|
return acmeProxyProvider.UpdateChallengeToken(fqdn.AcmeUsername, fqdn.AcmePassword, token, config.Settings)
|
|
}
|
|
|
|
cleanupTokenFunc := func() error {
|
|
var cleanupErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
cleanupCtx, cleanupCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, cleanupErr = db.ExecContext(cleanupCtx, "UPDATE fqdns SET acme_challenge_token = NULL WHERE id = ?", fqdnID)
|
|
cleanupCancel()
|
|
if cleanupErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
time.Sleep(time.Second * time.Duration(retry+1))
|
|
}
|
|
}
|
|
return cleanupErr
|
|
}
|
|
|
|
// Status-Callback (optional, für Logging)
|
|
statusCallback := func(status string) {
|
|
log.Printf("[Renewal Queue %s] Status: %s", queueID, status)
|
|
}
|
|
|
|
// Generiere TraceID
|
|
traceID := generateTraceID()
|
|
log.Printf("Starte automatische Zertifikatserneuerung (Queue ID: %s, TraceID: %s, FQDN: %s)", queueID, traceID, fqdn.FQDN)
|
|
|
|
// Erstelle ACME-Client-Kontext
|
|
// Standardmäßig verwenden wir Let's Encrypt Staging, aber in Zukunft könnte dies aus der FQDN-Konfiguration kommen
|
|
acmeProviderIDStr := "letsencrypt-staging" // TODO: Aus FQDN-Konfiguration lesen
|
|
acmeCtx, err := NewACMEClientContext(acmeProviderIDStr)
|
|
if err != nil {
|
|
return fmt.Errorf("fehler beim Initialisieren des ACME-Providers: %v", err)
|
|
}
|
|
|
|
// Prüfe ob der KeyID zum aktuellen Provider passt
|
|
// Wenn der FQDN noch den alten Provider (certigo-acmeproxy) hat, aber wir jetzt einen direkten ACME-Provider verwenden,
|
|
// muss der KeyID ignoriert werden, da er zu einem anderen Provider gehört
|
|
keyIDToUse := fqdn.AcmeKeyID
|
|
if fqdn.AcmeProviderID == "certigo-acmeproxy" && acmeProviderIDStr != "certigo-acmeproxy" {
|
|
// Provider hat sich geändert - KeyID ist nicht mehr gültig
|
|
log.Printf("Provider hat sich geändert (%s -> %s), erstelle neuen Account", fqdn.AcmeProviderID, acmeProviderIDStr)
|
|
keyIDToUse = "" // Erzwinge neue Account-Erstellung
|
|
}
|
|
|
|
// Beantrage neues Zertifikat
|
|
baseFqdn := strings.TrimPrefix(fqdn.FQDN, "*.")
|
|
result, err := RequestCertificate(acmeCtx, baseFqdn, fqdn.AcmeEmail, fqdnID, keyIDToUse, traceID, updateTokenFunc, cleanupTokenFunc, statusCallback)
|
|
if err != nil {
|
|
return fmt.Errorf("fehler beim Beantragen des neuen Zertifikats: %v", err)
|
|
}
|
|
|
|
// Speichere neues Zertifikat
|
|
if result.Certificate != "" && result.PrivateKey != "" {
|
|
newCertID := uuid.New().String()
|
|
certificateID := result.OrderURL
|
|
if certificateID == "" {
|
|
certificateID = newCertID
|
|
}
|
|
|
|
createdAt := time.Now().UTC().Format("2006-01-02 15:04:05")
|
|
expiresAt, isIntermediate, parseErr := ParseCertificate(result.Certificate)
|
|
var expiresAtStr string
|
|
var isIntermediateInt int
|
|
if parseErr == nil {
|
|
expiresAtStr = expiresAt.UTC().Format("2006-01-02 15:04:05")
|
|
if isIntermediate {
|
|
isIntermediateInt = 1
|
|
}
|
|
}
|
|
|
|
// Verwende separaten Context für INSERT mit Retry-Logik
|
|
var insertErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
insertCtx, insertCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, insertErr = db.ExecContext(insertCtx, `
|
|
INSERT INTO certificates (id, fqdn_id, space_id, csr_id, certificate_id, provider_id, certificate_pem, private_key_pem, status, expires_at, is_intermediate, created_at)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
`, newCertID, fqdnID, spaceID, nil, certificateID, "certigo-acmeproxy", result.Certificate, result.PrivateKey, "issued", expiresAtStr, isIntermediateInt, createdAt)
|
|
insertCancel()
|
|
if insertErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
time.Sleep(time.Second * time.Duration(retry+1))
|
|
}
|
|
}
|
|
if insertErr != nil {
|
|
return fmt.Errorf("fehler beim Speichern des neuen Zertifikats: %v", insertErr)
|
|
}
|
|
|
|
// Markiere altes Zertifikat (optional, falls gewünscht)
|
|
// Hier könnten wir z.B. den Status auf 'replaced' setzen
|
|
|
|
// Verarbeite RenewalInfo für das neue Zertifikat (falls aktiviert)
|
|
renewalEnabledValue := true
|
|
if renewalEnabled.Valid {
|
|
renewalEnabledValue = renewalEnabled.Int64 == 1
|
|
}
|
|
|
|
if renewalEnabledValue {
|
|
go func() {
|
|
if err := ProcessRenewalInfoForCertificate(result.Certificate, newCertID, fqdnID, spaceID, true); err != nil {
|
|
log.Printf("Fehler beim Verarbeiten der RenewalInfo für erneuertes Zertifikat (wird ignoriert): %v", err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
log.Printf("Automatische Zertifikatserneuerung erfolgreich abgeschlossen (Queue ID: %s, Neues Cert ID: %s)", queueID, newCertID)
|
|
}
|
|
|
|
// Update KeyID falls geändert (mit Retry-Logik)
|
|
if result.KeyID != "" && result.KeyID != fqdn.AcmeKeyID {
|
|
var keyIDErr error
|
|
for retry := 0; retry < 3; retry++ {
|
|
keyIDCtx, keyIDCancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
_, keyIDErr = db.ExecContext(keyIDCtx, "UPDATE fqdns SET acme_key_id = ? WHERE id = ?", result.KeyID, fqdnID)
|
|
keyIDCancel()
|
|
if keyIDErr == nil {
|
|
break
|
|
}
|
|
if retry < 2 {
|
|
time.Sleep(time.Second * time.Duration(retry+1))
|
|
}
|
|
}
|
|
if keyIDErr != nil {
|
|
log.Printf("Warnung: Fehler beim Speichern der neuen KeyID nach 3 Versuchen: %v", keyIDErr)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|