#!/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 # ── 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 # 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 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:" 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 # ── 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