feat: HA-Cluster v1.2.x — Split-Brain, TOTP, Enterprise-FW, Drift-Fix, VIP-Recovery

- keepalived: pg_role='standby' hat Vorrang vor role für BACKUP-Bestimmung
- keepalived-master.sh: gecrasht Dienste beim MASTER-Übergang starten (nicht nur reload)
- confighash: ip_addresses per Interface-Name hashen statt per FK (Cross-Node-Drift-Fix)
- TOTP/2FA: RFC 6238 — Setup-Flow, QR-Code, Admin-Disable; two-step Login
- Firewall-UI: Enterprise-Design — auto-Beschreibung, icon-only Actions, zero-hit Indikator
- fe80-Filter: Link-local IPv6 aus NTP/DNS Listen-Dropdowns entfernen
- VIP-Dashboard, Dual-Path VRRP, GW-Tracking (Migrations 0033/0034)
- Forward Proxy + DNS erweiterte Einstellungen (Migrations 0031/0032)
- unbound-control: edgeguard in unbound-Gruppe via postinst

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-31 18:18:31 +02:00
parent 49899e984c
commit 1d06b28064
55 changed files with 3132 additions and 672 deletions

View File

@@ -12,7 +12,7 @@ Description: EdgeGuard — native Reverse-Proxy / LB / Forward-Proxy / VPN / Fir
PG Streaming Replication + provider Floating-IP for HTTP ingress).
.
This package ships the management API, scheduler and CLI.
Depends: postgresql-16 | postgresql-17, haproxy (>= 2.8), squid, wireguard-tools, unbound, chrony, nftables, certbot, openssl, sudo, adduser, systemd, ca-certificates, ulogd2, ulogd2-json, iputils-ping, traceroute, dnsutils, curl, netcat-openbsd
Depends: postgresql-16 | postgresql-17, haproxy (>= 2.8), squid, wireguard-tools, unbound, chrony, nftables, keepalived, certbot, openssl, sudo, adduser, systemd, ca-certificates, ulogd2, ulogd2-json, iputils-ping, traceroute, dnsutils, curl, netcat-openbsd
Recommends: edgeguard-keydb (>= 6.3.4-edgeguard1), apparmor, fail2ban
Section: admin
Priority: optional

View File

@@ -25,6 +25,12 @@ case "$1" in
if getent group haproxy >/dev/null; then
usermod -a -G haproxy "$EG_USER" || true
fi
# unbound-Gruppe: unbound-control-Socket ist root:unbound srw-rw----
# → edgeguard muss in der Gruppe sein um stats_noreset + flush aufrufen
# zu können (kein sudo nötig, Gruppe reicht).
if getent group unbound >/dev/null; then
usermod -a -G unbound "$EG_USER" || true
fi
# systemd-journal + adm: damit edgeguard-api `journalctl -u …`
# ohne sudo lesen kann — wird für /api/v1/logs gebraucht
# (zentrale Log-Übersicht über alle Services).
@@ -90,9 +96,13 @@ edgeguard ALL=(root) NOPASSWD: /usr/sbin/nft list table inet edgeguard
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl start wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl stop wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl enable wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl disable wg-quick@*.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show all dump
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show *
# WireGuard symlink: /etc/wireguard/ ist root:root 700; edgeguard-api
@@ -139,6 +149,8 @@ edgeguard ALL=(root) NOPASSWD: /usr/bin/systemd-run --unit=edgeguard-restore.ser
# Keepalived reload: VIP-Settings-Änderung triggert keepalived-Reload.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload-or-restart keepalived.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload-or-restart keepalived.service
# VIP-Schwenk-Test: dediziertes Script mit interner Input-Validierung.
edgeguard ALL=(root) NOPASSWD: /usr/lib/edgeguard/vip-cmd.sh
SUDOERS
# ── Distro-Conf-Includes für die per-Service Renderer ─────────
@@ -422,6 +434,7 @@ create_iface() {
local typ="$1" name="$2" parent="$3" vlan_id="$4" mtu="$5" members="$6"
case "$typ" in
vlan)
ip link set "$parent" up 2>/dev/null || true
ip link add link "$parent" name "$name" type vlan id "$vlan_id" || return 1
;;
bridge)
@@ -464,13 +477,31 @@ fi
while IFS='|' read -r typ name parent vlan_id mtu members; do
[ -z "$typ" ] && continue
case "$typ" in '#'*) continue;; esac
if ! ip link show "$name" >/dev/null 2>&1; then
if ip link show "$name" >/dev/null 2>&1; then
# Typ-Änderung: altes Interface löschen und neu erstellen
existing_type=$(ip -d link show "$name" 2>/dev/null | awk '/^[[:space:]]/{print $1; exit}')
case "$typ" in
vlan) expected="vlan" ;;
bridge) expected="bridge_slave" ;; # bridge selbst hat keinen eigenen type-String
bond) expected="bond_slave" ;;
*) expected="" ;;
esac
current_type=$(ip -d link show "$name" 2>/dev/null | grep -oE 'vlan |bridge |bond ' | head -1 | tr -d ' ')
if [ -n "$current_type" ] && [ "$current_type" != "$typ" ]; then
ip link set "$name" down 2>/dev/null || true
ip link del "$name" 2>/dev/null || true
if ! create_iface "$typ" "$name" "$parent" "$vlan_id" "$mtu" "$members"; then
echo "edgeguard-interfaces: failed to recreate $typ $name" >&2
fi
else
[ -n "$mtu" ] && ip link set "$name" mtu "$mtu" 2>/dev/null || true
[ "$typ" = "vlan" ] && [ -n "$parent" ] && ip link set "$parent" up 2>/dev/null || true
ip link set "$name" up 2>/dev/null || true
fi
else
if ! create_iface "$typ" "$name" "$parent" "$vlan_id" "$mtu" "$members"; then
echo "edgeguard-interfaces: failed to create $typ $name" >&2
fi
else
[ -n "$mtu" ] && ip link set "$name" mtu "$mtu" 2>/dev/null || true
ip link set "$name" up 2>/dev/null || true
fi
done < "$CONF"
@@ -574,12 +605,40 @@ IPADDRUNIT
# werden von Keepalived als notify_master / notify_backup / check
# aufgerufen. Kein Auto-Promote — keepalived-master.sh loggt nur.
install -d -m 0755 /usr/lib/edgeguard
for script in keepalived-check.sh keepalived-master.sh keepalived-backup.sh; do
for script in keepalived-check.sh keepalived-master.sh keepalived-backup.sh keepalived-gw-check.sh; do
if [ -f "/usr/lib/edgeguard/${script}" ]; then
chmod 0755 "/usr/lib/edgeguard/${script}"
fi
done
# ── VIP-Schwenk-Script ───────────────────────────────────────
# Führt `ip addr add/del` für VIP-Failover-Tests aus. Läuft via
# sudo (Whitelist in sudoers). Interne Input-Validierung verhindert
# Command-Injection trotz breiter sudoers-Regel.
cat > /usr/lib/edgeguard/vip-cmd.sh <<'VIPCMD'
#!/bin/bash
# vip-cmd.sh {add|del} {address/prefix} {device}
# Wird von edgeguard-api via sudo für VIP-Schwenk-Tests aufgerufen.
# Nach `ip addr add` werden squid/unbound/haproxy reloaded damit sie
# die neu aktive VIP sofort binden.
set -e
action="$1" addrpfx="$2" dev="$3"
case "$action" in
add|del) ;;
*) echo "vip-cmd: ungültige action '$action'" >&2; exit 1 ;;
esac
[[ -z "$addrpfx" || -z "$dev" ]] && { echo "vip-cmd: fehlende Parameter" >&2; exit 1; }
[[ "$addrpfx" =~ ^[0-9a-fA-F.:\/]+$ ]] || { echo "vip-cmd: ungültige Adresse '$addrpfx'" >&2; exit 1; }
[[ "$dev" =~ ^[a-zA-Z0-9._-]+$ ]] || { echo "vip-cmd: ungültiges Device '$dev'" >&2; exit 1; }
ip addr "$action" "$addrpfx" dev "$dev"
if [ "$action" = "add" ]; then
for svc in squid.service unbound.service haproxy.service; do
systemctl is-active --quiet "$svc" && systemctl reload "$svc" 2>/dev/null || true
done
fi
VIPCMD
chmod 0755 /usr/lib/edgeguard/vip-cmd.sh
# ── Self-signed default cert so HAProxy starts cleanly ───────
# HAProxy `bind :443 ssl crt /etc/edgeguard/tls/` needs at least
# one PEM in the directory to come up. Operator runs certbot
@@ -625,6 +684,25 @@ IPADDRUNIT
exit 1
fi
# ALTER PUBLICATION erfordert den PG-Superuser (edgeguard ist nicht
# Owner der Publication). Idempotent — No-Op auf Secondary-Nodes
# (wo die Publication nicht existiert) und wenn die Tabellen schon
# drin sind.
sudo -u postgres psql edgeguard <<'EOSQL' 2>/dev/null || true
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_publication WHERE pubname = 'edgeguard_shared') THEN
IF NOT EXISTS (
SELECT 1 FROM pg_publication_tables
WHERE pubname = 'edgeguard_shared' AND tablename = 'network_interfaces'
) THEN
ALTER PUBLICATION edgeguard_shared ADD TABLE network_interfaces, ip_addresses;
END IF;
END IF;
END;
$$;
EOSQL
# ── Render initial service configs ───────────────────────────
# Writes /etc/edgeguard/haproxy/haproxy.cfg + nftables.d/
# ruleset.nft from the (just-migrated, empty) PG state.
@@ -641,6 +719,19 @@ IPADDRUNIT
echo "postinst: edgeguard-ctl render-config (nftables) failed — aborting" >&2
exit 1
fi
# Keepalived-Config rendern (optional — nur wenn VIPs konfiguriert)
# /etc/keepalived/ wird vom keepalived-Paket als root angelegt;
# edgeguard braucht Schreibrecht damit der Config-Generator den
# atomic-write (tempfile → rename) durchführen kann.
install -d -m 0755 /etc/keepalived
chown "$EG_USER":"$EG_USER" /etc/keepalived
sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl render-config --only=keepalived || true
if [ -f /etc/keepalived/keepalived.conf ]; then
systemctl enable keepalived >/dev/null 2>&1 || true
systemctl is-active --quiet keepalived \
&& systemctl reload keepalived \
|| systemctl start keepalived || true
fi
# ── HAProxy systemd drop-in: read EdgeGuard config ───────────
# Keeps the distro /etc/haproxy/haproxy.cfg untouched (it's a
@@ -665,6 +756,15 @@ IPADDRUNIT
# restart. Bei Erst-Install ist nichts running, dann ist das
# ein normaler Start.
systemctl restart edgeguard-api.service edgeguard-scheduler.service || true
# WireGuard-Sicherung: enabled-aber-inaktive wg-quick@-Interfaces
# nach dem API-Restart starten. Der API-Renderer kann wg-quick@
# kurz flippen; falls er dabei fehlschlägt, stellt dieser Block
# das Interface wieder her ohne manuellen Eingriff.
while IFS= read -r wg_unit; do
[ -n "$wg_unit" ] || continue
systemctl is-active --quiet "$wg_unit" || systemctl start "$wg_unit" 2>/dev/null || true
done < <(systemctl list-unit-files 'wg-quick@*.service' --state=enabled --no-legend 2>/dev/null | awk '{print $1}')
;;
abort-upgrade|abort-remove|abort-deconfigure)