Stub raus, vollständig implementiert:
* Migration 0014: dns_settings (single-row) + dns_zones.forward_to.
Default-Settings sind sinnvoll für die typische LAN-Resolver-Rolle
(1.1.1.1 + 9.9.9.9 upstream, localnet allow, DNSSEC + qname-min on).
* internal/services/dns: CRUD-Repo für zones, records, settings.
* internal/handlers/dns.go: REST /api/v1/dns/zones, /records, /settings
mit Auto-Reload nach jeder Mutation.
* internal/unbound/unbound.cfg.tpl + unbound.go: Renderer schreibt
/etc/unbound/unbound.conf.d/edgeguard.conf direkt (kein Symlink-
Dance, weil AppArmor unbound nur /etc/unbound erlaubt). Local-zones
authoritativ aus dns_records; forward-zones per stub-zone; default-
forwarders catchen alles sonst.
* main.go: dnsRepo + unbound-Reloader injiziert.
* render.go: unbound.New() bekommt Pool.
* postinst:
- Conf-Datei /etc/unbound/unbound.conf.d/edgeguard.conf wird als
edgeguard:edgeguard 0644 angelegt damit Renderer schreiben kann.
- /etc/edgeguard + Service-Subdirs auf 0755 (Squid + Unbound laufen
NICHT als edgeguard, brauchen Read-Traversal).
- Sudoers: systemctl reload unbound.service whitelisted.
* Template: chroot:"" (Conf liegt außerhalb /var/lib/unbound default-
chroot), DNSSEC-Trust-Anchor NICHT setzen (Distro hat schon
root-auto-trust-anchor-file.conf — sonst doppelter Anchor → start
failure).
* Frontend /dns: PageHeader + zwei Tabs (Zones + Resolver-Settings).
Zones-Tab mit Drawer für Records (CRUD pro Zone, A/AAAA/CNAME/TXT/
MX/SRV/NS/PTR/CAA). Sidebar-Eintrag unter Network.
* i18n DE/EN für dns.* Block.
Verified end-to-end: render → unbound restart → dig @127.0.0.1
example.com → 104.20.23.154 / 172.66.147.243.
Version 1.0.34 (mehrere Iterationen wegen AppArmor + chroot + perms).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
86 lines
2.5 KiB
Go
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.34"
|
|
|
|
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)
|
|
}
|