#!/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
        # 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
        # freeradius: edgeguard muss die gerenderten clients.conf/authorize
        # ins freeradius-lesbare Verzeichnis schreiben (setgid-Dir unten).
        if getent group freeradius >/dev/null; then
            usermod -a -G freeradius "$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/kea \
                 /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

        # Cluster-TLS-Dir 0700 — Cluster-CA-Key + peer.key sind sehr
        # sensitiv (CA-Compromise = vollständiger Cluster-Bruch). Wird
        # beim API-Boot mit Material gefüllt; postinst legt nur den
        # Dir an damit der API-User schreiben kann.
        install -d -m 0700 -o "$EG_USER" -g "$EG_USER" /var/lib/edgeguard/cluster-tls

        # 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: /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 Live-Reload ohne Tunnel-Abbruch: wg syncconf + wg-quick strip
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg syncconf *
edgeguard ALL=(root) NOPASSWD: /usr/bin/wg-quick strip *
# WireGuard symlink: /etc/wireguard/ ist root:root 700; edgeguard-api
# legt Symlinks an damit wg-quick@<iface> die Configs findet.
edgeguard ALL=(root) NOPASSWD: /bin/ln -sf /etc/edgeguard/wireguard/* /etc/wireguard/*
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
# DHCP (Kea): Service-Lifecycle an dhcp_settings.enabled gekoppelt.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl stop kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl enable kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable kea-dhcp4-server.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl disable kea-dhcp4-server.service
# RADIUS (FreeRADIUS): Service-Lifecycle an radius_settings.enabled gekoppelt.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart freeradius.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl restart freeradius.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop freeradius.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl stop freeradius.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable freeradius.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl enable freeradius.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable freeradius.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl disable freeradius.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update -qq
edgeguard ALL=(root) NOPASSWD: /usr/bin/apt-get update
# Auto-Update-Toggle (Settings → Automatische Updates) schreibt/entfernt
# exakt diese Conf-Datei. Restrict auf den genauen Pfad damit edgeguard
# NICHT beliebig in /etc/apt/apt.conf.d schreiben darf.
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/apt.conf.d/52edgeguard-auto-updates
edgeguard ALL=(root) NOPASSWD: /bin/rm -f /etc/apt/apt.conf.d/52edgeguard-auto-updates
# Update-Kanal-Switch (Settings → Update-Kanal) schreibt exakt diese
# sources.list-Zeile. Gleiches Restrict-Pattern wie oben.
edgeguard ALL=(root) NOPASSWD: /usr/bin/tee /etc/apt/sources.list.d/edgeguard.list
# 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
# Network-Interfaces: API ruft `sudo systemctl restart edgeguard-interfaces.service`
# nach jedem Mutate (Create/Update/Delete) von VLAN/bridge/bond-Interfaces.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart edgeguard-interfaces.service
# IP-Addresses: API ruft `sudo systemctl restart edgeguard-ipaddresses.service`
# nach jedem Mutate (Create/Update/Delete).
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart edgeguard-ipaddresses.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
# Replication-Repair: Resync der Logical-Replication-Subscription vom
# Primary (Config-Drift-Banner → "Resync erzwingen"). Skript landet immer
# unter /var/lib/edgeguard/repair-replication.sh, Unit-Form ist fix.
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reset-failed edgeguard-repair-replication.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemd-run --unit=edgeguard-repair-replication.service --description=EdgeGuard replication repair --collect bash /var/lib/edgeguard/repair-replication.sh
# 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
# CrowdSec management
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli decisions list -o json
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli decisions add *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli decisions delete *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli alerts list *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli alerts delete *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli bouncers list -o json
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli bouncers delete *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli machines list -o json
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli machines delete *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli collections list -o json
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli collections install *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli collections remove *
edgeguard ALL=(root) NOPASSWD: /usr/bin/cscli version
# WAF agent service control
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start edgeguard-waf.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop edgeguard-waf.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable edgeguard-waf.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable edgeguard-waf.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl restart edgeguard-waf.service
# CrowdSec service toggle (start/stop/enable/disable) + reload (Whitelist-Render)
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl reload crowdsec.service
edgeguard ALL=(root) NOPASSWD: /bin/systemctl reload crowdsec.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start crowdsec.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop crowdsec.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable crowdsec.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable crowdsec.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl start crowdsec-firewall-bouncer.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl stop crowdsec-firewall-bouncer.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl enable crowdsec-firewall-bouncer.service
edgeguard ALL=(root) NOPASSWD: /usr/bin/systemctl disable crowdsec-firewall-bouncer.service
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

        # Kea DHCPv4: managed config unter /etc/edgeguard/kea, vom Distro-
        # Pfad symlinked (Distro-Original wird gesichert). Der Dienst läuft
        # NUR, wenn der Operator DHCP aktiviert (Default AUS) — kein
        # rogue DHCP-Server nach Install. Der Renderer enabled+restartet
        # kea-dhcp4-server, sobald dhcp_settings.enabled=true gesetzt wird.
        install -d /etc/kea
        if [ -f /etc/kea/kea-dhcp4.conf ] && [ ! -L /etc/kea/kea-dhcp4.conf ]; then
            mv /etc/kea/kea-dhcp4.conf /etc/kea/kea-dhcp4.conf.distro-bak
        fi
        ln -sfn /etc/edgeguard/kea/kea-dhcp4.conf /etc/kea/kea-dhcp4.conf
        systemctl disable --now kea-dhcp4-server.service 2>/dev/null || true

        # FreeRADIUS: managed clients.conf + authorize unter /etc/edgeguard/
        # freeradius (setgid → Dateien erben Gruppe freeradius, 0640 → der
        # freeradius-Dienst kann lesen). Symlinks von den Distro-Pfaden.
        # Dienst default AUS (kein RADIUS bis Operator aktiviert).
        if getent group freeradius >/dev/null; then
            install -d -m 2750 -o "$EG_USER" -g freeradius /etc/edgeguard/freeradius
        else
            install -d -m 0750 -o "$EG_USER" -g "$EG_USER" /etc/edgeguard/freeradius
        fi
        if [ -d /etc/freeradius/3.0 ]; then
            if [ -e /etc/freeradius/3.0/clients.conf ] && [ ! -L /etc/freeradius/3.0/clients.conf ]; then
                mv /etc/freeradius/3.0/clients.conf /etc/freeradius/3.0/clients.conf.distro-bak
            fi
            ln -sfn /etc/edgeguard/freeradius/clients.conf /etc/freeradius/3.0/clients.conf
            if [ -e /etc/freeradius/3.0/mods-config/files/authorize ] && [ ! -L /etc/freeradius/3.0/mods-config/files/authorize ]; then
                mv /etc/freeradius/3.0/mods-config/files/authorize /etc/freeradius/3.0/mods-config/files/authorize.distro-bak
            fi
            install -d /etc/freeradius/3.0/mods-config/files
            ln -sfn /etc/edgeguard/freeradius/authorize /etc/freeradius/3.0/mods-config/files/authorize
        fi
        systemctl disable --now freeradius.service 2>/dev/null || true

        chmod 0440 /etc/sudoers.d/edgeguard

        # ── Migration: alte "main"-Komponente → "stable" (Kanal-Modell) ──
        # Vor diesem Release schrieb der Installer immer Komponente "main"
        # in die apt-Quelle. Ab jetzt publiziert publish.sh/release.sh nur
        # noch nach stable/testing — ohne diese Migration würden
        # Bestandsnodes stumm keine neuen Updates mehr sehen (main bleibt
        # auf dem letzten main-Stand stehen). Idempotent.
        #
        # netcell-edgeguard.list: NOCH ältere Installer-Generation vor dem
        # Rename auf edgeguard.list (install.sh räumt sie nur bei einem
        # FRISCHEN Install auf, nie bei einem Upgrade — auf Bestandsnodes,
        # die nie neu installiert wurden, ist sie ggf. noch die aktive
        # Datei). Erst konsolidieren, dann main→stable.
        EG_SOURCES_LIST=/etc/apt/sources.list.d/edgeguard.list
        EG_LEGACY_LIST=/etc/apt/sources.list.d/netcell-edgeguard.list
        if [ -f "$EG_LEGACY_LIST" ] && [ ! -f "$EG_SOURCES_LIST" ]; then
            mv "$EG_LEGACY_LIST" "$EG_SOURCES_LIST"
        fi
        if [ -f "$EG_SOURCES_LIST" ] && grep -q ' trixie main$' "$EG_SOURCES_LIST"; then
            sed -i 's/ trixie main$/ trixie stable/' "$EG_SOURCES_LIST"
            apt-get update -qq || true
        fi

        # ── 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
# log_martians AUS (nicht der rp_filter-Drop selbst!): Ein VRRP-Backup-Node
# sieht auf geteilten L2-VLANs dauerhaft Broadcast/Multicast für Gateway-/
# VIP-Adressen, die er im Backup-Zustand nicht besitzt — rp_filter=2 verwirft
# das korrekt, aber log_martians=1 flutet dmesg/journal damit (>100k Zeilen/
# Woche beobachtet). Kernel-OR-Semantik: conf.all=1 überschreibt jeden
# Interface-spezifischen Wert, daher nur hier zentral abschaltbar. Security-
# Sichtbarkeit läuft ohnehin über nftables-NFLOG/ulogd2 + CrowdSec, nicht dmesg.
net.ipv4.conf.all.log_martians           = 0
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

        # ── Network-Interfaces apply-script + systemd unit ───────────
        # Verwaltet aus /etc/edgeguard/interfaces.conf. VLAN/bridge/bond-
        # Interfaces die der Operator anlegt werden sofort via
        # `systemctl restart edgeguard-interfaces.service` erstellt.
        # Ethernet + WireGuard werden vom OS bzw. wg-quick verwaltet,
        # nicht von diesem Skript. Diff-Ansatz analog ip-addresses.
        cat > /usr/sbin/edgeguard-apply-interfaces <<'APPLYIFACES'
#!/bin/bash
# Managed by edgeguard — DO NOT EDIT.
CONF=/etc/edgeguard/interfaces.conf
APPLIED=/var/lib/edgeguard/interfaces-applied.conf

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)
            ip link add "$name" type bridge || return 1
            if [ -n "$members" ]; then
                echo "$members" | tr ',' '\n' | while read -r m; do
                    [ -n "$m" ] && ip link set "$m" master "$name" 2>/dev/null || true
                done
            fi
            ;;
        bond)
            ip link add "$name" type bond || return 1
            if [ -n "$members" ]; then
                echo "$members" | tr ',' '\n' | while read -r m; do
                    [ -n "$m" ] && { ip link set "$m" down 2>/dev/null; ip link set "$m" master "$name" 2>/dev/null || true; }
                done
            fi
            ;;
        *) return 0 ;;
    esac
    [ -n "$mtu" ] && ip link set "$name" mtu "$mtu" 2>/dev/null || true
    ip link set "$name" up 2>/dev/null || true
}

# Entferne Interfaces die im letzten Apply waren, aber jetzt nicht mehr
if [ -f "$APPLIED" ]; then
    while IFS='|' read -r typ name rest; do
        [ -z "$typ" ] && continue
        case "$typ" in '#'*) continue;; esac
        if ! awk -F'|' -v n="$name" 'NR>2 && $2==n {found=1} END{exit !found}' "$CONF" 2>/dev/null; then
            ip link set "$name" down 2>/dev/null || true
            ip link del "$name" 2>/dev/null || true
        fi
    done < "$APPLIED"
fi

[ -f "$CONF" ] || { : > "$APPLIED"; exit 0; }

# Erstelle/aktualisiere Interfaces aus Config
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
        # 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
    fi
done < "$CONF"

cp "$CONF" "$APPLIED"
APPLYIFACES
        chmod 0755 /usr/sbin/edgeguard-apply-interfaces
        chown root:root /usr/sbin/edgeguard-apply-interfaces

        cat > /etc/systemd/system/edgeguard-interfaces.service <<'IFACESUNIT'
[Unit]
Description=EdgeGuard managed network interfaces apply
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=oneshot
ExecStart=/usr/sbin/edgeguard-apply-interfaces
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
IFACESUNIT
        systemctl daemon-reload
        systemctl enable edgeguard-interfaces.service >/dev/null 2>&1 || true
        # Initialer Apply — leere /etc/edgeguard/interfaces.conf ist ok
        systemctl start edgeguard-interfaces.service 2>/dev/null || true

        if [ ! -f /etc/edgeguard/interfaces.conf ]; then
            : > /etc/edgeguard/interfaces.conf
            chown "$EG_USER":"$EG_USER" /etc/edgeguard/interfaces.conf
        fi

        # ── IP-Addresses apply-script + systemd unit ─────────────────
        # Verwaltet aus /etc/edgeguard/ip-addresses.conf. Adressen die
        # der Operator über die GUI hinzufügt/entfernt werden sofort via
        # `systemctl restart edgeguard-ipaddresses.service` angewendet.
        # Diff-Ansatz: /var/lib/edgeguard/ip-addresses-applied.conf hält
        # den zuletzt angewendeten Stand; alte Adressen werden entfernt,
        # neue hinzugefügt — manuell gebundene Adressen bleiben intakt.
        cat > /usr/sbin/edgeguard-apply-ipaddresses <<'APPLYIP'
#!/bin/bash
# Managed by edgeguard — DO NOT EDIT.
set -e
CONF=/etc/edgeguard/ip-addresses.conf
APPLIED=/var/lib/edgeguard/ip-addresses-applied.conf

# Entferne Adressen die im letzten Apply-Lauf waren, aber jetzt nicht mehr
if [ -f "$APPLIED" ]; then
    while IFS='|' read -r dev addrpfx; do
        [ -z "$dev" ] && continue
        case "$dev" in '#'*) continue;; esac
        # Nur entfernen wenn nicht mehr in der neuen Config
        if ! grep -qxF "${dev}|${addrpfx}" "$CONF" 2>/dev/null; then
            ip addr del "$addrpfx" dev "$dev" 2>/dev/null || true
        fi
    done < "$APPLIED"
fi

[ -f "$CONF" ] || { : > "$APPLIED"; exit 0; }

# Füge neue Adressen hinzu
while IFS='|' read -r dev addrpfx; do
    [ -z "$dev" ] && continue
    case "$dev" in '#'*) continue;; esac
    ip addr add "$addrpfx" dev "$dev" 2>/dev/null || true
done < "$CONF"

# Aktuelle Config als "applied" speichern
cp "$CONF" "$APPLIED"
APPLYIP
        chmod 0755 /usr/sbin/edgeguard-apply-ipaddresses
        chown root:root /usr/sbin/edgeguard-apply-ipaddresses

        cat > /etc/systemd/system/edgeguard-ipaddresses.service <<'IPADDRUNIT'
[Unit]
Description=EdgeGuard managed IP addresses apply
After=network-online.target edgeguard-interfaces.service
Wants=network-online.target
StartLimitIntervalSec=0

[Service]
Type=oneshot
ExecStart=/usr/sbin/edgeguard-apply-ipaddresses
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target
IPADDRUNIT
        systemctl daemon-reload
        systemctl enable edgeguard-ipaddresses.service >/dev/null 2>&1 || true
        systemctl start edgeguard-ipaddresses.service 2>/dev/null || true

        if [ ! -f /etc/edgeguard/ip-addresses.conf ]; then
            : > /etc/edgeguard/ip-addresses.conf
            chown "$EG_USER":"$EG_USER" /etc/edgeguard/ip-addresses.conf
        fi

        # ── Keepalived notify-scripts installieren ───────────────────
        # Die Skripte liegen im Package unter /usr/lib/edgeguard/ und
        # 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 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
        # 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

        # ── Replikation reconcilen (Publication / Grants / Subscription) ──
        # Ersetzt die frühere feste ADD-TABLE-Logik (die network_interfaces/
        # ip_addresses bei JEDEM Upgrade fälschlich wieder in die Publication
        # zog — beide sind node-lokal). edgeguard-ctl erkennt die Rolle selbst
        # und bringt idempotent den Soll-Zustand:
        #   Publisher : fehlende Shared-Tables ADD, node-lokale (localOnlyTables)
        #               DROP, GRANT SELECT für den Replikations-User (deckt per
        #               Migration neu hinzugekommene Tabellen ab — sonst hängt
        #               deren tablesync in 'd').
        #   Subscriber: neue Tabellen leeren + REFRESH PUBLICATION.
        #   Standalone: No-Op.
        # Läuft als root → buildPsqlCmd nutzt `sudo -u postgres psql` (Superuser).
        # Best-effort: ein Reconcile-Fehler darf das Upgrade nie abbrechen.
        /usr/bin/edgeguard-ctl cluster-reconcile-replication 2>&1 || true

        # ── CrowdSec IDS installation ─────────────────────────────────────────
        # Install CrowdSec if not present. We use the official CrowdSec APT repo.
        # Only done on initial install (not on upgrade) to avoid overwriting
        # user configuration.
        if [ "$1" = "configure" ] && [ -z "$2" ]; then
            if ! command -v cscli >/dev/null 2>&1; then
                echo "postinst: installing CrowdSec..."
                # Add CrowdSec repo key
                install -d -m 0755 /etc/apt/keyrings
                curl -s https://packagecloud.io/crowdsec/crowdsec/gpgkey | \
                    gpg --dearmor -o /etc/apt/keyrings/crowdsec_crowdsec-archive-keyring.gpg 2>/dev/null || true
                # Add repo (bookworm compat for Trixie)
                echo "deb [signed-by=/etc/apt/keyrings/crowdsec_crowdsec-archive-keyring.gpg] https://packagecloud.io/crowdsec/crowdsec/debian/ bookworm main" \
                    > /etc/apt/sources.list.d/crowdsec.list
                apt-get update -qq 2>/dev/null || true
                DEBIAN_FRONTEND=noninteractive apt-get install -y -qq crowdsec crowdsec-firewall-bouncer-nftables 2>/dev/null || true

                # Install default collections
                if command -v cscli >/dev/null 2>&1; then
                    cscli collections install crowdsecurity/linux crowdsecurity/haproxy 2>/dev/null || true
                fi
            fi
        fi

        # ── CrowdSec HAProxy-Acquisition auf journald zeigen ──────────
        # EdgeGuards HAProxy loggt nach journald (log /dev/log local0/1) —
        # es gibt KEINE /var/log/haproxy.log. `cscli setup` rät aber auf eine
        # datei-basierte Quelle → CrowdSec liest nichts → die HTTP-/CVE-
        # Szenarien laufen leer (WAF-artiger Web-Schutz tot). Wir setzen die
        # Acquisition deterministisch auf die haproxy.service-Journal-Unit
        # (analog zur sshd-Quelle). Läuft auf JEDEM configure (self-healing
        # auch für Bestandsinstalls). Admin-Custom bleibt unangetastet: nur
        # überschreiben, wenn Datei fehlt / noch die kaputte Datei-Default
        # (/var/log/haproxy.log) enthält / von uns stammt.
        if command -v cscli >/dev/null 2>&1; then
            CS_HAPROXY_ACQUIS="/etc/crowdsec/acquis.d/setup.haproxy.yaml"
            if [ ! -f "$CS_HAPROXY_ACQUIS" ] \
               || grep -q '/var/log/haproxy.log' "$CS_HAPROXY_ACQUIS" 2>/dev/null \
               || grep -q 'Managed by EdgeGuard' "$CS_HAPROXY_ACQUIS" 2>/dev/null; then
                install -d -m 0755 /etc/crowdsec/acquis.d
                cat > "$CS_HAPROXY_ACQUIS" <<'ACQUIS'
# Managed by EdgeGuard. HAProxy loggt nach journald (log /dev/log local0/1),
# es gibt keine /var/log/haproxy.log. journalctl-Quelle analog zur sshd-Acquis.
source: journalctl
journalctl_filter:
  - "_SYSTEMD_UNIT=haproxy.service"
labels:
  type: haproxy
ACQUIS
                # SIGHUP-Reload lädt die Acquisition neu; Restart als Fallback.
                systemctl reload crowdsec 2>/dev/null \
                    || systemctl restart crowdsec 2>/dev/null || true
            fi

            # CrowdSec-Whitelist: eigenen Management-Traffic (Backend
            # api_backend = ausschließlich edgeguard-api: Admin-UI + REST-API +
            # ACME) von ALLEN Scenarios ausnehmen. Die Admin-SPA feuert beim
            # Laden viele /api/-Requests — normal, wurde aber als http-crawl
            # gebannt (Incident 2026-08-03, Admin-IP gesperrt). IP-unabhängig.
            install -d -m 0755 /etc/crowdsec/parsers/s02-enrich
            # Altlast aus dem manuellen Live-Fix (2026-08-03) entfernen — sonst
            # lägen zwei inhaltsgleiche Whitelist-Dateien nebeneinander.
            rm -f /etc/crowdsec/parsers/s02-enrich/netcell-mgmt-whitelist.yaml
            cat > /etc/crowdsec/parsers/s02-enrich/edgeguard-mgmt-whitelist.yaml <<'WL'
name: edgeguard/mgmt-ui-whitelist
description: Management-UI/REST-API/ACME (HAProxy-Backend api_backend) von CrowdSec ausnehmen - Admin-SPA-Traffic ist kein Angriff.
whitelist:
  reason: edgeguard management-ui / api (backend api_backend)
  expression:
    - "evt.Parsed.backend_name == 'api_backend'"
WL
            systemctl reload crowdsec 2>/dev/null \
                || systemctl restart crowdsec 2>/dev/null || true

            # Admin-Hosts-Whitelist: wird vom crowdsec-whitelist-Generator aus
            # domains.crowdsec_trusted gerendert (UI-Schalter "vertrauenswuerdiges
            # Admin-Panel"). Das Verzeichnis ist root-owned → der Generator (laeuft
            # als edgeguard) kann nur eine vorab-chownte Datei ueberschreiben, daher
            # hier leer + edgeguard-owned anlegen, dann aus der DB befuellen.
            install -o "$EG_USER" -g "$EG_USER" -m 0644 /dev/null \
                /etc/crowdsec/parsers/s02-enrich/edgeguard-admin-hosts-whitelist.yaml
            sudo -n -u "$EG_USER" /usr/bin/edgeguard-ctl render-config --only=crowdsec-whitelist 2>/dev/null || true

            # CrowdSec-Simulations-Default: http-crawl-non_statics ist bei modernen
            # SPAs/Apps strukturell FP-anfaellig — ein Seiten-Load/Sync feuert 40+
            # distinkte /api/-URLs, der Leaky-Bucket (capacity=40, leak ~2/s) laeuft
            # in Sekunden ueber → False-Positive-Ban legitimer Nutzer/Kunden. Daher
            # per Default in SIMULATION: alarmiert weiter, bannt aber nicht. Echte
            # Angriffe (ssh-bf, http-cve-*, backdoors, CVE-2021-41773 …) bleiben
            # scharf. Nur bei ERST-Install setzen (Marker) → ein spaeteres manuelles
            # 'cscli simulation disable …' des Operators wird bei Updates NICHT
            # wieder ueberschrieben.
            EG_CROWDSEC_SIM_MARKER=/var/lib/edgeguard/.crowdsec-crawl-sim-applied
            if [ ! -f "$EG_CROWDSEC_SIM_MARKER" ]; then
                cscli simulation enable crowdsecurity/http-crawl-non_statics 2>/dev/null || true
                install -d -m 0755 /var/lib/edgeguard
                : > "$EG_CROWDSEC_SIM_MARKER"
                systemctl reload crowdsec 2>/dev/null || systemctl restart crowdsec 2>/dev/null || true
            fi
        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
        # 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
        # ── keepalived systemd drop-in: Boot-Race-Fix ──────────────────
        # keepalived referenziert VIP-Devices (vlanXXX), die erst von
        # edgeguard-interfaces.service angelegt werden. Ohne Ordering startet
        # keepalived vor den VLANs (beide nur After=network-online.target) →
        # "interface vlanX doesn't exist" → permanenter CONFIG-Crash OHNE
        # Auto-Recovery (keepalived bleibt nach Reboot tot). After=/Wants=
        # wartet auf die Interfaces; Restart=on-failure ist das Sicherheitsnetz.
        install -d /etc/systemd/system/keepalived.service.d
        cat > /etc/systemd/system/keepalived.service.d/10-edgeguard.conf <<'KEEPALIVEDDROPIN'
[Unit]
After=edgeguard-interfaces.service
Wants=edgeguard-interfaces.service
StartLimitIntervalSec=0

[Service]
Restart=on-failure
RestartSec=3
KEEPALIVEDDROPIN
        systemctl daemon-reload
        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
        # 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

        # ── OWASP Core Rule Set (CRS) für edgeguard-waf ─────────────────────
        # Nur beim Erstinstall — kein Überschreiben bei Upgrades damit
        # admins angepasste crs-setup.conf behalten können.
        WAF_CRS_DIR="/usr/share/edgeguard/waf/crs"
        if [ "$1" = "configure" ] && [ -z "$2" ] && [ ! -d "$WAF_CRS_DIR/rules" ]; then
            CRS_VERSION="4.7.0"
            echo "postinst: installing OWASP CRS v${CRS_VERSION}..."
            install -d -m 0755 "$WAF_CRS_DIR/rules"
            CRS_TMP="$(mktemp -d)"
            if curl -sL --max-time 60 \
                "https://github.com/coreruleset/coreruleset/archive/refs/tags/v${CRS_VERSION}.tar.gz" \
                -o "${CRS_TMP}/crs.tar.gz" 2>/dev/null; then
                tar xzf "${CRS_TMP}/crs.tar.gz" -C "${CRS_TMP}" 2>/dev/null || true
                CRS_SRC="${CRS_TMP}/coreruleset-${CRS_VERSION}"
                if [ -f "${CRS_SRC}/crs-setup.conf.example" ]; then
                    install -m 0644 "${CRS_SRC}/crs-setup.conf.example" "${WAF_CRS_DIR}/crs-setup.conf"
                    install -m 0644 "${CRS_SRC}/rules/"* "${WAF_CRS_DIR}/rules/" 2>/dev/null || true
                    chown -R "$EG_USER":"$EG_USER" "$WAF_CRS_DIR"
                    echo "postinst: OWASP CRS v${CRS_VERSION} installed to ${WAF_CRS_DIR}"
                else
                    echo "postinst: CRS download succeeded but layout unexpected — skipping" >&2
                fi
            else
                echo "postinst: CRS download failed (no internet?) — WAF usable without CRS" >&2
            fi
            rm -rf "${CRS_TMP}"
        fi

        # ── CRS App-Exclusion-Plugins (Nextcloud / WordPress / Drupal) ──────
        # Offizielle OWASP-CRS-Plugins (separate Repos coreruleset/<name>-plugin).
        # Ihre plugins/*.conf landen in <crs>/plugins/; der WAF-Renderer bindet
        # je Domain die GEWÄHLTEN ein (waf_configs.crs_plugins). Läuft auf JEDEM
        # configure (self-healing für Bestandsinstalls), aber nur wenn das
        # jeweilige Plugin noch fehlt — admin-Anpassungen bleiben unangetastet.
        if [ -d "$WAF_CRS_DIR/rules" ]; then
            install -d -m 0755 "$WAF_CRS_DIR/plugins"
            for plugin in nextcloud-rule-exclusions wordpress-rule-exclusions drupal-rule-exclusions; do
                if [ -f "$WAF_CRS_DIR/plugins/${plugin}-before.conf" ] \
                   || [ -f "$WAF_CRS_DIR/plugins/${plugin}-config.conf" ]; then
                    continue
                fi
                P_TMP="$(mktemp -d)"
                P_OK=""
                # Plugin-Repos nutzen teils 'main', teils 'master' als Default-
                # Branch. curl -f (statt still einen 404 als Erfolg zu werten).
                for P_BR in main master; do
                    if curl -fsSL --max-time 45 \
                        "https://github.com/coreruleset/${plugin}-plugin/archive/refs/heads/${P_BR}.tar.gz" \
                        -o "${P_TMP}/p.tgz" 2>/dev/null \
                        && tar xzf "${P_TMP}/p.tgz" -C "${P_TMP}" 2>/dev/null; then
                        P_OK=1; break
                    fi
                done
                if [ -n "$P_OK" ] && ls "${P_TMP}"/*/plugins/${plugin}-*.conf >/dev/null 2>&1; then
                    install -m 0644 "${P_TMP}"/*/plugins/${plugin}-*.conf \
                        "$WAF_CRS_DIR/plugins/" 2>/dev/null \
                        && echo "postinst: CRS-Plugin ${plugin} installiert"
                else
                    echo "postinst: CRS-Plugin ${plugin} nicht installiert (Download/Layout)" >&2
                fi
                rm -rf "${P_TMP}"
            done
            chown -R "$EG_USER":"$EG_USER" "$WAF_CRS_DIR/plugins" 2>/dev/null || true
        fi

        # ── systemd: pick up new units + restart haproxy with our cfg
        systemctl daemon-reload
        systemctl restart haproxy.service || true
        systemctl enable edgeguard-api.service edgeguard-scheduler.service edgeguard-waf.service || true
        # restart (NICHT enable --now): das neue Unit-File könnte ein
        # geänderter ReadWritePaths-Block sein (Befund 1.0.87:
        # /var/lib/apt fehlte → apt-get update aus dem Update-Banner-
        # Handler schrieb in read-only Filesystem und failte still).
        # daemon-reload allein lädt die neue Unit nur für NÄCHSTE Starts;
        # der laufende Daemon behält das alte Sandbox-Snapshot bis zum
        # restart. Bei Erst-Install ist nichts running, dann ist das
        # ein normaler Start.
        systemctl restart edgeguard-api.service edgeguard-scheduler.service edgeguard-waf.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)
        ;;

    *)
        echo "postinst called with unknown argument \`$1'" >&2
        exit 1
        ;;
esac

#DEBHELPER#

exit 0
