Files
Debian ea7c356455 feat(cluster): Phase 3 Foundation — node.conf + ha_nodes-Drift + UI
Code-Vorbereitung für Multi-Node, ohne dass eine zweite Box nötig ist.
Single-Node-Mode bleibt der Default; alles existiert und wird sichtbar,
sobald ein 2. Knoten joined (Phase 3.2 später).

Migration 0020:
  ha_nodes += version (edgeguard-api-Version)
              config_hash (drift-Detection-Hash)
              mgmt_ip (Management-IP, niemals VIP)
              status (online|offline|joining|leaving|unknown)

internal/cluster/local_config.go:
  /etc/edgeguard/node.conf — INI-style, node-lokale Identität:
  NODE_ID, HOSTNAME, MGMT_IP, ROLE, PEER_HOSTS. NIEMALS zwischen
  Cluster-Peers replizieren. LoadLocalConfig / SaveLocalConfig /
  EnsureLocalConfig (auto-Generierung beim ersten Boot).
  MgmtIP-Default = firstNonLoopbackIPv4(); Operator kann
  überschreiben (mehrere Interfaces).

internal/cluster/store.go:
  - HANode-Model um die 4 neuen Felder erweitert
  - UpsertSelf nimmt jetzt mgmt_ip/version/config_hash/status, COALESCE
    erhält werte wenn der Caller sie nicht setzt
  - EnsureSelfRegistered-Signatur: + role + version-Argument

internal/handlers/cluster.go:
  GET /api/v1/cluster/status — strukturierter Endpoint:
    {local_id, local_node, peers[], mode, health, drift_found, updated_at}
  GET /api/v1/cluster/nodes bleibt für Tools.

UI (pages/Cluster):
  - Header zeigt Mode-Tag (Single-Node / Cluster) + Health-Tag (OK /
    degraded / split-brain)
  - Self-Card: Descriptions mit FQDN, Node-ID, Status, Role, Version,
    MGMT-IP, API-URL, Config-Hash
  - Peers-Tabelle nur wenn vorhanden, mit "drift"-Marker pro Row
  - Drift-Alert-Banner wenn ein Peer einen anderen config_hash hat
  - Single-Node-Mode Hinweis-Alert ("cluster-join kommt in 3.2")

postinst: leeres /etc/edgeguard/node.conf wird angelegt (chown
edgeguard); API auto-befüllt beim ersten boot.

main.go ruft EnsureLocalConfig + EnsureSelfRegistered mit version.

Verifiziert auf der Box (1.0.70):
  - /etc/edgeguard/node.conf hat NODE_ID, HOSTNAME, MGMT_IP=89.163.205.6,
    ROLE=primary
  - ha_nodes-Row: status=online, version=1.0.70, mgmt_ip=89.163.205.6

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 08:27:00 +02:00

476 lines
23 KiB
Bash
Executable File

#!/bin/bash
# postinst for edgeguard-api — creates system user, filesystem layout,
# initialises PostgreSQL (role + db + migrations), enables systemd
# units. Each step idempotent; safe to re-run on every upgrade.
set -e
export LC_ALL=C
export LANG=C
EG_USER="edgeguard"
EG_HOME="/var/lib/edgeguard"
case "$1" in
configure)
# ── System user ──────────────────────────────────────────────
if ! getent passwd "$EG_USER" >/dev/null; then
adduser --system --group --home "$EG_HOME" \
--shell /usr/sbin/nologin --no-create-home \
--gecos "EdgeGuard daemon" "$EG_USER"
fi
# haproxy admin.sock liegt mit mode 0660 root:haproxy. Damit
# edgeguard-api das Backend-Stat-CSV lesen kann (Dashboard),
# in die haproxy-group adden — best-effort, nicht failen wenn
# das Paket noch nicht installiert ist.
if getent group haproxy >/dev/null; then
usermod -a -G haproxy "$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).
if getent group systemd-journal >/dev/null; then
usermod -a -G systemd-journal "$EG_USER" || true
fi
if getent group adm >/dev/null; then
usermod -a -G adm "$EG_USER" || true
fi
# ── Directories ──────────────────────────────────────────────
# /etc/edgeguard und Service-Subdirs müssen für die Service-User
# (squid, unbound, haproxy laufen NICHT als edgeguard) traversier-
# bzw lesbar sein. 0755 statt 0750 — kein Geheimnis ist hier
# gespeichert, alles sind Renderer-Outputs.
for d in /etc/edgeguard /etc/edgeguard/haproxy /etc/edgeguard/squid \
/etc/edgeguard/wireguard /etc/edgeguard/unbound \
/etc/edgeguard/nftables.d; do
install -d -m 0755 -o "$EG_USER" -g "$EG_USER" "$d"
done
# Sensitive Verzeichnisse bleiben 0750 (TLS-Keys, ACME-State).
for d in /etc/edgeguard/tls /var/lib/edgeguard /var/log/edgeguard \
/var/lib/edgeguard/acme; do
install -d -m 0750 -o "$EG_USER" -g "$EG_USER" "$d"
done
# ACME-Account-Dir 0700 — hält den lego-Account-Schlüssel,
# gehört nur edgeguard.
install -d -m 0700 -o "$EG_USER" -g "$EG_USER" /var/lib/edgeguard/acme-account
# Backup-Verzeichnis: edgeguard-owned damit der scheduler /
# API-Prozess schreiben kann. Mode 0750 — Backups enthalten
# einen pg_dump der edgeguard-DB + license_key, also nicht
# world-readable.
install -d -m 0750 -o "$EG_USER" -g "$EG_USER" /var/backups/edgeguard
# node.conf — node-lokale Cluster-Identität (NIEMALS in Sync
# zwischen Peers). API auto-befüllt das beim ersten Boot mit
# NODE_ID + Hostname + erster non-loopback-IPv4. Operator
# darf danach editieren (z.B. MGMT_IP korrigieren wenn Box
# mehrere Interfaces hat).
if [ ! -f /etc/edgeguard/node.conf ]; then
: > /etc/edgeguard/node.conf
chown "$EG_USER":"$EG_USER" /etc/edgeguard/node.conf
chmod 0644 /etc/edgeguard/node.conf
fi
# ── sudoers: HAProxy reload + (later) systemd-networkd reload
# Damit edgeguard-api nach einer SSL- oder Netzwerk-Mutation
# selbst reloaden kann ohne root zu sein. NOPASSWD ist auf
# genau dieses Kommando beschränkt.
cat > /etc/sudoers.d/edgeguard <<'SUDOERS'
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload haproxy.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload haproxy.service
edgeguard ALL=(root) NOPASSWD: /usr/sbin/nft -f /etc/edgeguard/nftables.d/ruleset.nft
edgeguard ALL=(root) NOPASSWD: /usr/sbin/nft list tables
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: /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: /usr/bin/wg show all dump
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg show *
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload squid.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload squid.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload unbound.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload unbound.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart unbound.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart unbound.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload chrony.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload chrony.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart chrony.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart chrony.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update -qq
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update
# Backup-Pfad: pg_dump als postgres-User. Whitelist exakt mit
# --clean --if-exists --no-owner --no-acl + dem festen DB-Namen.
edgeguard ALL=(postgres) NOPASSWD: /usr/bin/pg_dump --clean --if-exists --no-owner --no-acl edgeguard
# Static-Routes: API ruft `sudo systemctl restart edgeguard-routes.service`
# nach jedem Mutate, damit das apply-Skript die neue routes.conf anwendet.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart edgeguard-routes.service
# Self-Upgrade-Pfad (handlers/system.go → /system/upgrade). Whitelist
# nur die exakte Unit-Form, damit edgeguard NICHT beliebige systemd-
# Units anlegen darf.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reset-failed edgeguard-upgrade.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemd-run --unit=edgeguard-upgrade.service --description=EdgeGuard self-upgrade --collect bash /var/lib/edgeguard/upgrade.sh
# Backup-Restore: gleiche Pattern wie Upgrade — Skript landet immer
# unter /var/lib/edgeguard/restore.sh, Unit-Form ist fix.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reset-failed edgeguard-restore.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemd-run --unit=edgeguard-restore.service --description=EdgeGuard self-restore --collect bash /var/lib/edgeguard/restore.sh
SUDOERS
# ── Distro-Conf-Includes für die per-Service Renderer ─────────
# Squid + Unbound lesen ihre Distro-Default-Conf, die wir per
# Symlink/Drop-in auf unsere managed conf zeigen lassen müssen.
# Renderer können das nicht selbst (kein Schreibrecht in /etc/squid
# bzw. /etc/unbound/unbound.conf.d/), daher hier einmalig.
# Plus: Squid + Unbound laufen als eigene User (squid/unbound),
# nicht edgeguard. Damit sie unsere Conf lesen können, müssen
# die Conf-Dirs world-readable sein (Configs ohne Secrets).
install -d -m 0755 -o "$EG_USER" -g "$EG_USER" /etc/edgeguard/squid /etc/edgeguard/unbound
# Squid: ersetzt die Distro-Datei durch Symlink (Backup .distro-bak)
if [ -f /etc/squid/squid.conf ] && [ ! -L /etc/squid/squid.conf ]; then
mv /etc/squid/squid.conf /etc/squid/squid.conf.distro-bak
fi
ln -sfn /etc/edgeguard/squid/squid.conf /etc/squid/squid.conf
# Unbound: Drop-in im conf.d-Verzeichnis. Wir schreiben direkt
# rein (statt /etc/edgeguard/unbound/...) weil das AppArmor-
# Profil unbound nur /etc/unbound erlaubt. Datei gehört dem
# edgeguard-User damit der Renderer sie überschreiben kann.
install -d /etc/unbound/unbound.conf.d
# Vorgänger-Symlink (aus früheren Versionen) wegräumen.
if [ -L /etc/unbound/unbound.conf.d/edgeguard.conf ]; then
rm /etc/unbound/unbound.conf.d/edgeguard.conf
fi
if [ ! -f /etc/unbound/unbound.conf.d/edgeguard.conf ]; then
: > /etc/unbound/unbound.conf.d/edgeguard.conf
fi
chown "$EG_USER":"$EG_USER" /etc/unbound/unbound.conf.d/edgeguard.conf
chmod 0644 /etc/unbound/unbound.conf.d/edgeguard.conf
# Chrony: gleicher Pattern wie Unbound — Drop-in im conf.d, der
# vom distro-default chrony.conf included wird. Datei gehört
# edgeguard damit der Renderer sie überschreiben kann.
install -d /etc/chrony/conf.d
if [ -L /etc/chrony/conf.d/edgeguard.conf ]; then
rm /etc/chrony/conf.d/edgeguard.conf
fi
if [ ! -f /etc/chrony/conf.d/edgeguard.conf ]; then
: > /etc/chrony/conf.d/edgeguard.conf
fi
chown "$EG_USER":"$EG_USER" /etc/chrony/conf.d/edgeguard.conf
chmod 0644 /etc/chrony/conf.d/edgeguard.conf
chmod 0440 /etc/sudoers.d/edgeguard
# ── Sysctl-Profil für Edge-Gateway (NAT + HAProxy + Forwarding) ──
# Voraussetzung für NAT/DNAT/Masquerade + sinnvolle Defaults
# für eine high-throughput Forwarding-Box. Edit nicht von Hand
# — Re-install vom Package überschreibt die Datei. Eigene
# Tweaks gehören in eine Datei mit höherer Nummer als 99.
rm -f /etc/sysctl.d/99-edgeguard-forward.conf # Vorgänger
cat > /etc/sysctl.d/99-edgeguard.conf <<'SYSCTL'
# ── Managed by edgeguard ────────────────────────────────────────────
# Lade-Reihenfolge: 99-* überschreibt distro-Defaults. Eigene
# Operator-Tweaks: /etc/sysctl.d/99-zzz-local.conf (lexikografisch
# später) — nicht in DIESE Datei!
# ─── Forwarding (NAT/DNAT/Masquerade) ───────────────────────────────
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
net.ipv4.conf.all.send_redirects = 0
net.ipv4.conf.default.send_redirects = 0
net.ipv4.conf.all.accept_redirects = 0
net.ipv6.conf.all.accept_redirects = 0
net.ipv4.conf.all.accept_source_route = 0
net.ipv6.conf.all.accept_source_route = 0
# ─── Reverse-Path-Filter (anti-spoof, loose-Modus für asymmetrisches
# Routing wie Multi-WAN / WireGuard split) ─────────────────────
net.ipv4.conf.all.rp_filter = 2
net.ipv4.conf.default.rp_filter = 2
# ─── Conntrack — Edge-Box trackt viele parallele Sessions ─────────
net.netfilter.nf_conntrack_max = 524288
net.netfilter.nf_conntrack_tcp_timeout_established = 86400
net.netfilter.nf_conntrack_tcp_timeout_time_wait = 30
net.netfilter.nf_conntrack_tcp_timeout_close_wait = 30
net.netfilter.nf_conntrack_buckets = 131072
# ─── TCP/IP-Stack-Tuning für HAProxy + viele Backends ─────────────
net.core.somaxconn = 65535
net.core.netdev_max_backlog = 16384
net.core.rmem_max = 16777216
net.core.wmem_max = 16777216
net.core.rmem_default = 262144
net.core.wmem_default = 262144
net.ipv4.tcp_rmem = 4096 87380 16777216
net.ipv4.tcp_wmem = 4096 65536 16777216
net.ipv4.tcp_max_syn_backlog = 65535
net.ipv4.tcp_fin_timeout = 15
net.ipv4.tcp_tw_reuse = 1
net.ipv4.tcp_keepalive_time = 300
net.ipv4.tcp_keepalive_probes = 5
net.ipv4.tcp_keepalive_intvl = 30
net.ipv4.tcp_mtu_probing = 1
net.ipv4.tcp_slow_start_after_idle = 0
net.ipv4.tcp_no_metrics_save = 1
net.ipv4.ip_local_port_range = 10240 65535
# ─── Modern congestion control + queueing (BBR + fq) ──────────────
# Wenn der Kernel BBR nicht hat, fällt Linux still auf cubic zurück.
net.ipv4.tcp_congestion_control = bbr
net.core.default_qdisc = fq
# ─── Anti-DoS / Hardening ─────────────────────────────────────────
net.ipv4.tcp_syncookies = 1
net.ipv4.icmp_echo_ignore_broadcasts = 1
net.ipv4.icmp_ignore_bogus_error_responses = 1
net.ipv4.conf.all.log_martians = 1
kernel.kptr_restrict = 2
kernel.dmesg_restrict = 1
# ─── Memory ───────────────────────────────────────────────────────
vm.swappiness = 10
vm.dirty_ratio = 20
vm.dirty_background_ratio = 5
SYSCTL
sysctl --system >/dev/null 2>&1 || true
# ── Firewall-Logging via ulogd2 (NFLOG group 0) ──────────────
# nft-Renderer emittiert `log prefix "edgeguard:<rule-id>" group 0`
# für jede Rule mit log=true. ulogd2 subscribed auf netlink-group
# 0 und schreibt JSON-Lines nach /var/log/edgeguard/firewall.jsonl.
# Das UI tailt das File für Live-Log + Historie.
install -d -m 0755 /var/log/edgeguard
install -d -m 0755 -o "$EG_USER" -g "$EG_USER" /var/log/edgeguard
if [ ! -f /var/log/edgeguard/firewall.jsonl ]; then
: > /var/log/edgeguard/firewall.jsonl
fi
# ulogd2 läuft als root (eigener Daemon); File muss von ihm
# schreibbar UND von edgeguard-API lesbar sein.
chown root:"$EG_USER" /var/log/edgeguard/firewall.jsonl
chmod 0640 /var/log/edgeguard/firewall.jsonl
cat > /etc/ulogd.conf <<'ULOGD'
# Managed by edgeguard — re-installation overwrites this file.
# NFLOG group 0 → JSON-Lines pro Paket nach /var/log/edgeguard/firewall.jsonl
# Format-Felder: oob.time.sec, oob.prefix (rule-id), src_ip, dst_ip,
# src_port, dst_port, ip.protocol, raw.pktlen, oob.in/oob.out (iface).
[global]
logfile="/var/log/ulogd.log"
loglevel=5
plugin="/usr/lib/x86_64-linux-gnu/ulogd/ulogd_inppkt_NFLOG.so"
plugin="/usr/lib/x86_64-linux-gnu/ulogd/ulogd_filter_IFINDEX.so"
plugin="/usr/lib/x86_64-linux-gnu/ulogd/ulogd_filter_IP2STR.so"
plugin="/usr/lib/x86_64-linux-gnu/ulogd/ulogd_filter_HWHDR.so"
plugin="/usr/lib/x86_64-linux-gnu/ulogd/ulogd_raw2packet_BASE.so"
plugin="/usr/lib/x86_64-linux-gnu/ulogd/ulogd_output_JSON.so"
stack=fw1:NFLOG,base1:BASE,ifi1:IFINDEX,ip2str1:IP2STR,mac2str1:HWHDR,json1:JSON
[fw1]
group=0
# qthreshold=1 + qtimeout=1: Kernel batched sonst Pakete bis 1024 oder
# bis 1s. Mit 1/1 → jedes Paket SOFORT raus, kein Sammeln. Wichtig
# damit die Live-Log-UI in real-time fließt statt in Bursts.
qthreshold=1
qtimeout=1
[json1]
sync=1
file="/var/log/edgeguard/firewall.jsonl"
timestamp=1
boolean_label=1
ULOGD
chmod 0644 /etc/ulogd.conf
# Logrotate-Profil für firewall.jsonl — Default: daily, 14 days
# comprimiert, copytruncate damit ulogd kein Reopen-Signal braucht.
cat > /etc/logrotate.d/edgeguard-firewall <<'LOGROTATE'
/var/log/edgeguard/firewall.jsonl {
daily
rotate 14
missingok
notifempty
compress
delaycompress
copytruncate
create 0640 root edgeguard
}
LOGROTATE
chmod 0644 /etc/logrotate.d/edgeguard-firewall
# ulogd2 enablen + restarten (idempotent). Wenn das Paket nicht
# da ist (Dependency-Konflikt o.ä.), nur warnen — die Firewall
# läuft auch ohne Logger.
if systemctl list-unit-files ulogd2.service >/dev/null 2>&1; then
systemctl enable ulogd2.service >/dev/null 2>&1 || true
systemctl restart ulogd2.service || \
echo "postinst: ulogd2.service restart failed (firewall logs disabled until fixed)" >&2
else
echo "postinst: ulogd2.service not installed — install ulogd2 to enable firewall log" >&2
fi
# ── Static-routes apply-script + systemd unit ────────────────
# Verwaltet aus /etc/edgeguard/routes.conf. `proto edgeguard`
# markiert die Routen damit das flush keine fremden Routen
# tötet (kernel/dhcp/static manuell gesetzt).
cat > /usr/sbin/edgeguard-apply-routes <<'APPLYROUTES'
#!/bin/bash
# Managed by edgeguard — DO NOT EDIT.
# Reads /etc/edgeguard/routes.conf (pipe-format) and applies via ip(8).
set -e
CONF=/etc/edgeguard/routes.conf
# Existierende edgeguard-Routen weg, bevor wir neue setzen. Andere
# Quellen (kernel/dhcp/manuell ohne proto) bleiben intakt.
ip route flush proto 250 2>/dev/null || true
[ -f "$CONF" ] || exit 0
while IFS='|' read -r dest gw dev metric table; do
[ -z "$dest" ] && continue
case "$dest" in '#'*) continue;; esac
args=("$dest")
[ -n "$gw" ] && args+=("via" "$gw")
[ -n "$dev" ] && args+=("dev" "$dev")
[ -n "$metric" ] && args+=("metric" "$metric")
if [ -n "$table" ] && [ "$table" != "main" ]; then
args+=("table" "$table")
fi
args+=("proto" "250")
if ! ip route add "${args[@]}"; then
echo "edgeguard-routes: failed to add: ip route add ${args[*]}" >&2
# weitermachen — eine fehlende Route soll nicht alle anderen
# blockieren.
fi
done < "$CONF"
APPLYROUTES
chmod 0755 /usr/sbin/edgeguard-apply-routes
chown root:root /usr/sbin/edgeguard-apply-routes
# rt_protos-Eintrag für `proto edgeguard` (Symbolname statt
# numerisch). Debian 13 hat /etc/iproute2 nicht als Default,
# also conf.d-Pattern: /etc/iproute2/rt_protos.d/edgeguard.conf
# überlagert die /usr/share/iproute2/rt_protos.
install -d /etc/iproute2/rt_protos.d
echo "250 edgeguard" > /etc/iproute2/rt_protos.d/edgeguard.conf
chmod 0644 /etc/iproute2/rt_protos.d/edgeguard.conf
cat > /etc/systemd/system/edgeguard-routes.service <<'ROUTESUNIT'
[Unit]
Description=EdgeGuard static-routes apply
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/edgeguard-apply-routes
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
ROUTESUNIT
systemctl daemon-reload
systemctl enable edgeguard-routes.service >/dev/null 2>&1 || true
# Initialer Apply — leere /etc/edgeguard/routes.conf ist ok
# (Skript exitet einfach ohne irgendwas zu tun).
systemctl start edgeguard-routes.service 2>/dev/null || true
# Initialer Stub damit `cat` im Skript nicht klagt
if [ ! -f /etc/edgeguard/routes.conf ]; then
: > /etc/edgeguard/routes.conf
chown "$EG_USER":"$EG_USER" /etc/edgeguard/routes.conf
fi
# ── 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
# later; until then, browsers see an unverified cert which is
# the expected first-boot UX.
DEFAULT_PEM="/etc/edgeguard/tls/_default.pem"
if [ ! -f "$DEFAULT_PEM" ]; then
HOSTNAME_FQDN="$(hostname -f 2>/dev/null || hostname)"
TMP_KEY="$(mktemp)"
TMP_CRT="$(mktemp)"
openssl req -x509 -nodes -newkey rsa:2048 \
-keyout "$TMP_KEY" -out "$TMP_CRT" \
-days 3650 \
-subj "/CN=$HOSTNAME_FQDN" \
-addext "subjectAltName = DNS:$HOSTNAME_FQDN,DNS:localhost" \
>/dev/null 2>&1
cat "$TMP_CRT" "$TMP_KEY" > "$DEFAULT_PEM"
chown "$EG_USER:$EG_USER" "$DEFAULT_PEM"
chmod 0640 "$DEFAULT_PEM"
rm -f "$TMP_KEY" "$TMP_CRT"
fi
# ── Pre-flight: validate embedded migration set ──────────────
# Catches duplicate version prefixes BEFORE we touch the DB,
# so a broken upgrade can't half-apply migrations and leave
# the cluster wedged (mail-gateway 2026-05-08 incident).
if ! /usr/bin/edgeguard-ctl migrate check; then
echo "postinst: embedded migrations failed validation — aborting" >&2
exit 1
fi
# ── PostgreSQL: ensure role + database exist ─────────────────
# Requires postgresql-16 (or -17) running locally — guaranteed
# by Depends. Idempotent — re-runs on upgrade are no-ops.
if ! /usr/bin/edgeguard-ctl initdb; then
echo "postinst: edgeguard-ctl initdb failed — aborting" >&2
exit 1
fi
# ── Apply pending schema migrations ──────────────────────────
if ! sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl migrate up; then
echo "postinst: edgeguard-ctl migrate up failed — aborting" >&2
exit 1
fi
# ── Render initial service configs ───────────────────────────
# Writes /etc/edgeguard/haproxy/haproxy.cfg + nftables.d/
# ruleset.nft from the (just-migrated, empty) PG state.
#
# haproxy bekommt --no-reload (drop-in unten zeigt erst danach
# auf unsere cfg; wir restarten explizit); nftables muss aber
# aktiv reloadet werden, sonst läuft das Kernel-Set bei Template-
# Änderungen (z.B. neue anti-lockout-Ports) hinterher.
if ! sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl render-config --only=haproxy --no-reload; then
echo "postinst: edgeguard-ctl render-config (haproxy) failed — aborting" >&2
exit 1
fi
if ! sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl render-config --only=nftables; then
echo "postinst: edgeguard-ctl render-config (nftables) failed — aborting" >&2
exit 1
fi
# ── HAProxy systemd drop-in: read EdgeGuard config ───────────
# Keeps the distro /etc/haproxy/haproxy.cfg untouched (it's a
# conffile of the haproxy package). Drop-in is reversible by
# removing the file + daemon-reload.
install -d /etc/systemd/system/haproxy.service.d
if [ -f /etc/edgeguard/systemd/haproxy-edgeguard.conf ]; then
install -m 0644 /etc/edgeguard/systemd/haproxy-edgeguard.conf \
/etc/systemd/system/haproxy.service.d/edgeguard.conf
fi
# ── systemd: pick up new units + restart haproxy with our cfg
systemctl daemon-reload
systemctl restart haproxy.service || true
systemctl enable --now edgeguard-api.service edgeguard-scheduler.service || true
;;
abort-upgrade|abort-remove|abort-deconfigure)
;;
*)
echo "postinst called with unknown argument \`$1'" >&2
exit 1
;;
esac
#DEBHELPER#
exit 0