fix(firewall): skip the host firewall on Proxmox
Proxmox VE and Proxmox Mail Gateway are Debian, so os_detect classified them as debian and harden-firewall.sh installed the iptables backend on top of pve-firewall. The conflict is not the obvious one. pve-firewall does NOT delete third-party rules: it restores with `iptables-restore -n` (--noflush), only ever flushes chains matching its own patterns (PVEFW-*, tapNiM-*, vethNiM-*, fwbrN-*, GROUP-*), appends `-A INPUT -j PVEFW-INPUT` only when that hook is missing, and never sets a built-in chain's policy. Our rules survive it. We are the ones doing damage: - `-P INPUT DROP` is ours alone, and PVEFW-HOST-IN RETURNs on accept rather than ACCEPTing (it still has to check tap rules), so traffic Proxmox explicitly allowed falls out of its chain onto our DROP -- we silently override the platform's own accepts. - netfilter-persistent restores at boot with a full iptables-restore (no --noflush), wiping PVE's hook along with everything else until the daemon re-appends it ~10s later. - a deny-by-default chain has to enumerate the whole platform to stay usable: 8006, 5405-5412/udp corosync, 60000-60050, 5900-5999, 3128, 22, 111/udp, plus Ceph when hyperconverged -- and `-i lo`, or pveproxy loses pvedaemon on :85. - under the nftables backend (PVE 8.2+) an nft DROP beats an iptables ACCEPT, so our rules would not even be authoritative. So don't manage a firewall there at all: - oslib: is_proxmox() -- matches hosts shipping pve-firewall (VE/PMG), not PBS. - harden-firewall.sh: a third backend, "pve", that deliberately does nothing. apply explains and exits 0, allow/deny refuse loudly rather than fake success for a rule they didn't add, list shows pve-firewall status. It overrides an explicit FW_BACKEND; FW_IGNORE_PVE=1 is the one escape hatch. - harden-ssh.sh / harden-jumphost.sh: skip the firewall and install the standalone INPUT -> sshguard boot hook instead. That jump is safe alongside pve-firewall -- inserted with -I, it sits ahead of the appended PVEFW-INPUT hook and keeps first look at NEW connections. Detection only helps hosts built from here on, so `disable` cleans up one that was hardened earlier: it detects leftovers (and says which signal fired), sets INPUT ACCEPT *before* flushing so it can't drop the SSH session it runs over, deletes /etc/firewall and the engine, disables boot restore and renames the saved rulesets aside rather than persisting the open state, re-adds the sshguard jump, and restarts pve-firewall. `apply` points at it when it spots leftovers. Documented plainly that skipped is NOT protected: Proxmox's firewall is off by default (cluster-wide enable defaults to 0, and the daemon tears its chains down every ~10s while it is), so these hosts have no host firewall until someone enables it -- and the node panel's "Firewall: Yes" is ignored while the datacenter one says No. Also: svc_disable + fw_restore_services/fw_saved_files in oslib (fw_enable_restore now derives from the former), and usage() prints the whole header block instead of a hardcoded line range. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+48
-6
@@ -65,6 +65,22 @@ os_detect() {
|
||||
|
||||
_require_detected() { [[ -n "${OS_FAMILY:-}" ]] || os_detect; }
|
||||
|
||||
# True on a Proxmox host that ships pve-firewall -- Proxmox VE and Proxmox Mail
|
||||
# Gateway. Both are Debian underneath, so os_detect reports debian/systemd and
|
||||
# every other helper here is correct for them; the ONE thing that is not is the
|
||||
# firewall. pve-firewall owns the host ruleset, and while it leaves foreign rules
|
||||
# alone (it restores with --noflush and touches only its own PVEFW-* chains), a
|
||||
# deny-by-default INPUT chain underneath it overrides the accepts it RETURNs on
|
||||
# and cuts the web UI, corosync and migration traffic -- and our boot-time
|
||||
# iptables-restore would wipe its hook outright. harden-firewall.sh therefore
|
||||
# skips these hosts -- see the Proxmox backend there for the full reasoning.
|
||||
#
|
||||
# Proxmox Backup Server ships no firewall of its own and is deliberately NOT
|
||||
# matched: it is a plain Debian host as far as we are concerned.
|
||||
is_proxmox() {
|
||||
[[ -x /usr/sbin/pve-firewall || -x /usr/bin/pveversion || -d /etc/pve/nodes ]]
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Packages
|
||||
# ============================================================================
|
||||
@@ -136,6 +152,14 @@ svc_enable() { # enable at boot
|
||||
esac
|
||||
}
|
||||
|
||||
svc_disable() { # stop it being started at boot (the mirror of svc_enable)
|
||||
_require_detected
|
||||
case "$INIT_SYSTEM" in
|
||||
openrc) rc-update del "$1" default >/dev/null 2>&1 || true ;;
|
||||
systemd) systemctl disable "$1" >/dev/null 2>&1 || true ;;
|
||||
esac
|
||||
}
|
||||
|
||||
svc_start() { _require_detected; [[ "$INIT_SYSTEM" == openrc ]] && rc-service "$1" start || systemctl start "$1"; }
|
||||
svc_restart() { _require_detected; [[ "$INIT_SYSTEM" == openrc ]] && { rc-service "$1" restart || rc-service "$1" start; } || systemctl restart "$1"; }
|
||||
svc_reload() { _require_detected; [[ "$INIT_SYSTEM" == openrc ]] && { rc-service "$1" reload || rc-service "$1" restart; } || { systemctl reload "$1" || systemctl restart "$1"; }; }
|
||||
@@ -543,16 +567,34 @@ fw_save_cmd() {
|
||||
esac
|
||||
}
|
||||
|
||||
# The service(s) that restore the saved ruleset at boot, and the file(s) they
|
||||
# restore from -- the inverse of fw_save_cmd. Undoing the firewall on a host that
|
||||
# should never have had it means disabling the former and clearing the latter
|
||||
# (harden-firewall.sh's Proxmox `disable`), so name them once, here.
|
||||
fw_restore_services() {
|
||||
_require_detected
|
||||
case "$OS_FAMILY" in
|
||||
alpine) echo "iptables ip6tables" ;;
|
||||
debian) echo "netfilter-persistent" ;;
|
||||
rhel) echo "iptables ip6tables" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
fw_saved_files() {
|
||||
_require_detected
|
||||
case "$OS_FAMILY" in
|
||||
alpine) echo "/etc/iptables/rules-save /etc/iptables/rules6-save" ;;
|
||||
debian) echo "/etc/iptables/rules.v4 /etc/iptables/rules.v6" ;;
|
||||
rhel) echo "/etc/sysconfig/iptables /etc/sysconfig/ip6tables" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Enable the family's native boot-time restore service(s). Rules are already
|
||||
# live when this runs, so we only need them re-applied on the NEXT boot --
|
||||
# enable, don't start.
|
||||
fw_enable_restore() {
|
||||
_require_detected
|
||||
case "$OS_FAMILY" in
|
||||
alpine) svc_enable iptables; svc_enable ip6tables ;;
|
||||
debian) svc_enable netfilter-persistent ;;
|
||||
rhel) svc_enable iptables; svc_enable ip6tables ;;
|
||||
esac
|
||||
local s
|
||||
for s in $(fw_restore_services); do svc_enable "$s"; done
|
||||
}
|
||||
|
||||
# Install + enable firewalld (the native RHEL/Alma firewall). harden-firewall.sh
|
||||
|
||||
Reference in New Issue
Block a user