Files
edgeguard-native/cmd/edgeguard-scheduler/main.go
Debian b1eb940d09 fix(firewall+wg): Operator-Rule landete als Comment + wg-quick las falsche Conf
Zwei show-stopper beim Cutover .101 → .6 entdeckt + behoben:

1. nft-Template-Bug: {{- if ...}}-Whitespace-Trimmer nach der
   '# rule N' Kommentarzeile schluckte den Newline → die ganze
   Operator-Rule landete als Teil des # Kommentars. nft akzeptierte
   die Datei (legaler Comment) und der Operator sah keine Wirkung.
   Fix: Body auf eigener Zeile via {{""}}-Padding, Trimmer raus.

2. wg-Renderer schrieb /etc/edgeguard/wireguard/<iface>.conf, aber
   wg-quick@<iface>.service liest /etc/wireguard/<iface>.conf
   (Distro-Default). Die zwei Files driftet auseinander — beim
   Restart sah wg-quick die alte AllowedIPs. Fix: Renderer legt
   einen Symlink /etc/wireguard/<iface>.conf → /etc/edgeguard/...
   beim Render an (idempotent, ersetzt vorhandene Real-Files).

Beide Fixes waren voraussetzung für den .101 → .6 Cutover, der
jetzt sauber läuft: VIP .100 lebt auf .6, Unify Home dial't durch
zu wg7 (handshake), 10.0.10.x via wg7-Tunnel reachable.

Version 1.0.18.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:43:39 +02:00

86 lines
2.5 KiB
Go

// edgeguard-scheduler runs background jobs that don't belong on the
// API request path:
//
// - ACME cert renewal (every 6h, re-issues anything < 30d to expiry)
//
// Future jobs (cluster heartbeat, backup, audit-log retention)
// hang off the same Tick loop. Stays single-process — no leader
// election yet (Phase 3).
package main
import (
"context"
"log/slog"
"os"
"time"
"git.netcell-it.de/projekte/edgeguard-native/internal/database"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/acme"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/certrenewer"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/setup"
"git.netcell-it.de/projekte/edgeguard-native/internal/services/tlscerts"
)
var version = "1.0.18"
const (
// renewTickInterval — how often we re-evaluate expiring certs.
// 6h is enough: LE renewal window is 30 days; missing one tick
// makes no difference. Hourly would log too much.
renewTickInterval = 6 * time.Hour
// certDir matches handlers.NewTLSCertsHandler default — HAProxy
// reads from this directory.
certDir = "/etc/edgeguard/tls"
)
func main() {
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{Level: slog.LevelInfo})))
slog.Info("edgeguard-scheduler starting", "version", version)
ctx := context.Background()
pool, err := database.Open(ctx, database.ConnStringFromEnv())
if err != nil {
slog.Error("scheduler: DB open failed — sleeping forever", "error", err)
select {}
}
defer pool.Close()
tlsRepo := tlscerts.New(pool)
setupStore := setup.NewStore(setup.DefaultDir)
st, _ := setupStore.Load()
var renewer *certrenewer.Service
if st != nil && st.ACMEEmail != "" {
issuer := acme.New(st.ACMEEmail)
renewer = certrenewer.New(tlsRepo, issuer, certDir, 30*24*time.Hour)
slog.Info("scheduler: ACME renewer enabled",
"email", st.ACMEEmail, "tick", renewTickInterval, "threshold", "30d")
} else {
slog.Warn("scheduler: setup.acme_email empty — ACME renewal disabled until setup wizard ran")
}
if renewer != nil {
runRenewer(ctx, renewer)
}
tick := time.NewTicker(renewTickInterval)
defer tick.Stop()
for range tick.C {
if renewer != nil {
runRenewer(ctx, renewer)
}
}
}
func runRenewer(ctx context.Context, r *certrenewer.Service) {
res, err := r.Run(ctx)
if err != nil {
slog.Error("scheduler: renewer run failed", "error", err)
return
}
slog.Info("scheduler: renewer pass complete",
"checked", res.Checked, "renewed", res.Renewed,
"failed", res.Failed, "skipped", res.Skipped)
}