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:
@@ -101,7 +101,7 @@ deployments/<name>/ # one folder per stack
|
||||
| [`setup-host.sh`](scripts/setup-host.sh) | Set hostname per the naming schema (derives FQDN + Node ID) and render the shared MOTD with auto-computed border spacing. |
|
||||
| [`harden-ssh.sh`](scripts/harden-ssh.sh) | SSH hardening: post-quantum hybrid KEX, fresh Ed25519 host keys, key-only auth, external SFTP subsystem, sshguard. |
|
||||
| [`harden-jumphost.sh`](scripts/harden-jumphost.sh) | Bastion hardening on top of `harden-ssh`: `ssh-admins` (shell) vs `ssh-jumpers` (ProxyJump-only) with a PermitOpen allow-list. |
|
||||
| [`harden-firewall.sh`](scripts/harden-firewall.sh) | Deny-by-default host firewall: **iptables** on Alpine/Debian, **firewalld** on Alma/RHEL (set `FW_BACKEND` to override). Loopback, established, ICMP, SSH (configurable port) + registered ports; persisted natively (no boot hook). Same `allow`/`deny`/`list`/`disable` sub-commands on both. |
|
||||
| [`harden-firewall.sh`](scripts/harden-firewall.sh) | Deny-by-default host firewall: **iptables** on Alpine/Debian, **firewalld** on Alma/RHEL (set `FW_BACKEND` to override), **skipped on Proxmox** (`pve-firewall` owns the ruleset). Loopback, established, ICMP, SSH (configurable port) + registered ports; persisted natively (no boot hook). Same `allow`/`deny`/`list`/`disable` sub-commands on both. |
|
||||
| [`sshuser.sh`](scripts/sshuser.sh) | Add/edit/remove SSH users on a hardened jump host (Gum TUI or CLI flags). Installed standalone as `sshuser`. |
|
||||
| [`ntfy-ssh-login.sh`](scripts/ntfy-ssh-login.sh) | `pam_exec` hook that posts SSH logins to ntfy (user, source IP, key used, best-effort jump target), gated by group. Config: [`ssh-notify.conf.example`](scripts/ssh-notify.conf.example). |
|
||||
| [`auto-update.sh`](scripts/auto-update.sh) | Daily unattended package updates; reports (doesn't auto-jump) a new Alpine branch; reboot detection; ntfy summary. `install`/`run`/`uninstall`. |
|
||||
@@ -179,6 +179,8 @@ services with Alpine-specific wiring, so it isn't part of the tri-distro set.
|
||||
deny-by-default; we strip the stock `ssh`/`cockpit` services, open SSH +
|
||||
registered ports, and let sshguard block via the `sshguard-firewalld` backend
|
||||
(no `INPUT → sshguard` jump needed).
|
||||
- **Proxmox → nothing. The host is skipped** — and left with no host firewall
|
||||
until you enable Proxmox's own. See below.
|
||||
|
||||
`OUTPUT`/egress stays open and `FORWARD` is left untouched, so Docker container
|
||||
networking is unaffected. The harden scripts and `cloud-init/base.yml` /
|
||||
@@ -212,6 +214,86 @@ networking is unaffected. The harden scripts and `cloud-init/base.yml` /
|
||||
established-connection accept is added before the policy flips to `DROP`, and
|
||||
firewalld reloads preserve established connections.
|
||||
|
||||
### Proxmox hosts are skipped
|
||||
|
||||
Proxmox VE and Proxmox Mail Gateway are Debian underneath, so everything else in
|
||||
this repo treats them as Debian — but they already ship a firewall, and
|
||||
`pve-firewall` owns the host ruleset. The conflict is not the one you'd expect:
|
||||
`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 would survive it fine. The damage runs the
|
||||
other way — **we break Proxmox**:
|
||||
|
||||
- `-P INPUT DROP` is ours alone, and `PVEFW-HOST-IN` **returns** on accept rather
|
||||
than accepting (it still has to check the tap rules), so traffic Proxmox
|
||||
explicitly allowed falls out of its chain and lands on our `DROP`. We silently
|
||||
override the platform's own accepts;
|
||||
- the persistence layer is worse: `netfilter-persistent` restores at boot with a
|
||||
full `iptables-restore` (no `--noflush`), wiping PVE's `-j PVEFW-INPUT` hook
|
||||
along with everything else until the daemon re-appends it on its next ~10s pass;
|
||||
- a deny-by-default chain has to enumerate the whole platform to stay usable:
|
||||
`8006/tcp` web UI, `5405-5412/udp` corosync (5405 + knet link number, up to 8
|
||||
links — the bare `5405` is the pre-6.x multicast-era number), `60000-60050/tcp`
|
||||
migration, `5900-5999/tcp` VNC, `3128/tcp` SPICE, `22/tcp` SSH, `111/udp`
|
||||
rpcbind with NFS storage, plus Ceph's `6789`/`3300`/`6800-7300` when
|
||||
hyperconverged. Miss one and you lose the GUI or the cluster; miss `-i lo` and
|
||||
`pveproxy` can't reach `pvedaemon` on `127.0.0.1:85`, breaking the API locally;
|
||||
- under the opt-in **nftables** backend (PVE 8.2+), `proxmox-firewall` registers
|
||||
its own nft input hook, and an nft `DROP` beats an iptables `ACCEPT` — our
|
||||
rules wouldn't even be authoritative.
|
||||
|
||||
So `harden-firewall.sh` detects Proxmox (`is_proxmox` in `oslib.sh`: it looks
|
||||
for `pve-firewall`/`pveversion`/`/etc/pve/nodes`) and does nothing — `apply`
|
||||
explains and exits cleanly, `allow`/`deny` refuse loudly rather than pretend,
|
||||
and `list` shows `pve-firewall status`. `harden-ssh.sh` / `harden-jumphost.sh`
|
||||
skip the firewall too and install only the `INPUT → sshguard` boot hook, so
|
||||
brute-force protection still works while the Proxmox firewall is off. Manage the
|
||||
host firewall where Proxmox expects it — *Datacenter → Firewall* and
|
||||
*Node → Firewall*, or the `.fw` files directly:
|
||||
|
||||
```
|
||||
[RULES]
|
||||
IN ACCEPT -p tcp -dport 443
|
||||
```
|
||||
|
||||
`FW_IGNORE_PVE=1` forces our firewall on anyway. It will fight `pve-firewall`
|
||||
and can lock you out of the GUI and the cluster; Proxmox Backup Server ships no
|
||||
firewall of its own and is *not* detected, so it hardens as a normal Debian host.
|
||||
|
||||
> **Skipped is not the same as protected.** Proxmox's firewall is **off by
|
||||
> default** — the cluster-wide `enable` in `cluster.fw` defaults to `0`, and while
|
||||
> it is, the `pve-firewall` daemon actively tears its chains down every ~10 seconds,
|
||||
> leaving `INPUT` at policy `ACCEPT` with no rules. Until you enable it at
|
||||
> *Datacenter → Firewall → Options*, a Proxmox host has **no host firewall at all**
|
||||
> and `8006`, `22`, `3128` and `111` are open on every interface. Don't be reassured
|
||||
> by the *node* panel reading `Firewall: Yes` — that setting is ignored while the
|
||||
> datacenter one reads `No`. On these hosts, do your filtering in Proxmox or upstream
|
||||
> of it.
|
||||
|
||||
The `INPUT → sshguard` jump the harden scripts install *is* safe alongside
|
||||
`pve-firewall`: it is inserted with `-I`, so it sits ahead of the appended
|
||||
`PVEFW-INPUT` hook and keeps getting first look at new connections either way.
|
||||
|
||||
**Already hardened a Proxmox host?** Detection only helps hosts set up from now
|
||||
on, so `disable` cleans up one that already has our firewall:
|
||||
|
||||
```sh
|
||||
bash scripts/harden-firewall.sh disable
|
||||
```
|
||||
|
||||
On Proxmox that sub-command checks for leftovers (`/etc/firewall`, the engine, or
|
||||
a `DROP` policy) and, if it finds them, sets `INPUT` back to `ACCEPT` and flushes
|
||||
it — policy first, so it never drops the SSH session you are running it over —
|
||||
deletes `/etc/firewall` and `/usr/local/sbin/firewall-apply`, **disables** the
|
||||
boot-restore service and renames the saved rulesets to `*.bak-harden-firewall`
|
||||
(saving the open state, as the normal `disable` does, would snapshot
|
||||
`pve-firewall`'s own `PVEFW-*` chains and restore that stale copy at the next
|
||||
boot), re-inserts the `INPUT → sshguard` jump, and restarts `pve-firewall`. On a
|
||||
host that was correctly skipped it finds nothing and says so. `apply` points you
|
||||
at it when it spots leftovers.
|
||||
|
||||
## SSH login notifications
|
||||
|
||||
The harden scripts can install a `pam_exec` hook
|
||||
|
||||
+2
-1
@@ -29,7 +29,8 @@ runcmd:
|
||||
DATACENTER="Globally Everywhere"
|
||||
SSH_PORT=22
|
||||
ALLOWED_IP= # optional: whitelist your client IP in sshguard
|
||||
ENABLE_FIREWALL=1 # deny-by-default host firewall (0 to skip)
|
||||
ENABLE_FIREWALL=1 # deny-by-default host firewall (0 to skip;
|
||||
# always skipped on Proxmox -- pve-firewall owns it)
|
||||
OPEN_PORTS="" # extra inbound ports, e.g. "80/tcp 443/tcp"
|
||||
# ==================
|
||||
|
||||
|
||||
@@ -25,7 +25,8 @@ runcmd:
|
||||
DATACENTER="Globally Everywhere"
|
||||
SSH_PORT=22
|
||||
ALLOWED_IP= # optional: whitelist your client IP
|
||||
ENABLE_FIREWALL=1 # deny-by-default host firewall (0 to skip)
|
||||
ENABLE_FIREWALL=1 # deny-by-default host firewall (0 to skip;
|
||||
# always skipped on Proxmox -- pve-firewall owns it)
|
||||
JUMP_TARGETS="10.0.0.5:22 10.0.0.6:22" # hosts jumpers may ProxyJump to
|
||||
# Optional login notifications (pam_exec -> ntfy). Leave NTFY_URL empty to
|
||||
# skip. NTFY_REGION defaults to the region segment of this host's FQDN.
|
||||
|
||||
+179
-2
@@ -11,8 +11,13 @@
|
||||
# rhel (Alma) firewalld -- the native RHEL firewall. The default zone is
|
||||
# deny-by-default; we open SSH + registered ports and let
|
||||
# sshguard block via the sshguard-firewalld backend.
|
||||
# proxmox none -- SKIPPED. Proxmox VE/PMG are Debian, but pve-firewall
|
||||
# already owns the host ruleset; see the Proxmox section below.
|
||||
# On a host hardened BEFORE this skip existed, 'disable' removes
|
||||
# our ruleset, config and boot restore, and hands it back.
|
||||
# OUTPUT/egress stays open; FORWARD is left untouched so Docker networking is
|
||||
# unaffected. The allow/deny/list/disable subcommands work on both backends.
|
||||
# unaffected. The allow/deny/list/disable subcommands work on either managed
|
||||
# backend; on Proxmox they explain themselves and change nothing.
|
||||
#
|
||||
# All distro-specific operations go through scripts/oslib.sh. The OS-specific
|
||||
# surface here is exactly three things, all in oslib: which packages provide
|
||||
@@ -44,6 +49,7 @@
|
||||
# OPEN_PORTS="80/tcp 443/tcp" bash harden-firewall.sh # open extra ports at install
|
||||
# FW_SSH_SOURCE=10.0.0.0/8 bash harden-firewall.sh # restrict SSH to a source CIDR
|
||||
# FW_ALLOW_PING=0 bash harden-firewall.sh # drop ICMP echo (ping)
|
||||
# FW_IGNORE_PVE=1 bash harden-firewall.sh # harden a Proxmox host anyway
|
||||
#
|
||||
# bash harden-firewall.sh allow 443/tcp 51820/udp # register + apply
|
||||
# bash harden-firewall.sh allow web # preset: 80/tcp + 443/tcp
|
||||
@@ -96,6 +102,14 @@ if [[ -z "$FW_BACKEND" ]]; then
|
||||
[[ "$OS_FAMILY" == rhel ]] && FW_BACKEND=firewalld || FW_BACKEND=iptables
|
||||
fi
|
||||
|
||||
# Proxmox VE / Proxmox Mail Gateway: pve-firewall owns the host ruleset, so we
|
||||
# do not manage a firewall here at all. This overrides an explicit FW_BACKEND
|
||||
# too -- FW_IGNORE_PVE=1 is the single, deliberate escape hatch.
|
||||
: "${FW_IGNORE_PVE:=0}"
|
||||
if is_proxmox && [[ "$FW_IGNORE_PVE" != "1" ]]; then
|
||||
FW_BACKEND=pve
|
||||
fi
|
||||
|
||||
# ============================================================================
|
||||
# Spec parsing: PORT[/PROTO][@CIDR] and named presets -> "PORT/PROTO[ CIDR]" lines
|
||||
# ============================================================================
|
||||
@@ -373,10 +387,167 @@ disable_firewalld() {
|
||||
warn "Re-harden: 'bash $0 apply' | turn firewalld off: 'systemctl disable --now firewalld'"
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Proxmox backend (VE / Mail Gateway) -- deliberately does nothing.
|
||||
#
|
||||
# These are Debian hosts, so the iptables backend would happily install itself
|
||||
# here. It must not -- though not for the reason you would guess. pve-firewall
|
||||
# does NOT delete foreign rules: it applies every ruleset 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 exact hook is missing, and never sets
|
||||
# a built-in chain's policy at all. Our rules would survive it. The damage runs
|
||||
# the other way -- we break Proxmox:
|
||||
#
|
||||
# * `-P INPUT DROP` is ours alone, and PVEFW-HOST-IN RETURNs on accept rather
|
||||
# than ACCEPTing ("we use RETURN because we need to check also tap rules"),
|
||||
# so traffic Proxmox explicitly allowed falls out of its chain and lands on
|
||||
# our DROP. We silently override the platform's own accepts.
|
||||
# * the persistence layer is worse. netfilter-persistent restores at boot with
|
||||
# a full iptables-restore (no --noflush), wiping PVE's -j PVEFW-INPUT hook
|
||||
# along with everything else, until the daemon re-appends it on its next
|
||||
# ~10s cycle.
|
||||
# * a deny-by-default chain has to enumerate the whole platform to stay usable:
|
||||
# 8006/tcp web UI, 5405-5412/udp corosync (5405 + knet link number, up to 8
|
||||
# links), 60000-60050/tcp migration, 5900-5999/tcp VNC, 3128/tcp SPICE,
|
||||
# 22/tcp SSH, 111/udp rpcbind with NFS storage, and Ceph's 6789 / 3300 /
|
||||
# 6800-7300 when hyperconverged. Miss one and you lose the GUI or the cluster.
|
||||
# Miss `-i lo` and pveproxy can no longer reach pvedaemon on 127.0.0.1:85,
|
||||
# which breaks the API locally, not just remotely.
|
||||
# * under the opt-in nftables backend (PVE 8.2+) proxmox-firewall registers its
|
||||
# own nft input hook, and an nft DROP beats an iptables ACCEPT -- so our rules
|
||||
# would not even be authoritative.
|
||||
#
|
||||
# So: skip, and leave the host to the firewall the platform ships. Be clear-eyed
|
||||
# that this is NOT the same as protected: that firewall is off until someone
|
||||
# turns it on (see _pve_explain).
|
||||
# ============================================================================
|
||||
PVE_CLUSTER_FW=/etc/pve/firewall/cluster.fw
|
||||
PVE_HOST_FW=/etc/pve/local/host.fw # == /etc/pve/nodes/<node>/host.fw
|
||||
|
||||
# The shared explanation. Printed by every subcommand so the reason is never a
|
||||
# mystery, whichever entry point the operator came in through.
|
||||
_pve_explain() {
|
||||
warn "Proxmox detected -- pve-firewall owns the host ruleset; not touching it."
|
||||
warn " Manage the firewall natively instead:"
|
||||
warn " Datacenter -> Firewall -> ${PVE_CLUSTER_FW}"
|
||||
warn " Node -> Firewall -> ${PVE_HOST_FW}"
|
||||
warn " pve-firewall status | compile | localnet"
|
||||
warn " Host rules go under a [RULES] section -- a line before one is discarded:"
|
||||
warn " [RULES]"
|
||||
warn " IN ACCEPT -p tcp -dport 443"
|
||||
warn " HEADS UP: Proxmox's firewall is DISABLED by default (cluster-wide 'enable'"
|
||||
warn " defaults to 0, and pve-firewall tears its chains down every ~10s while it is)."
|
||||
warn " Until you enable it at Datacenter -> Firewall -> Options this host has NO"
|
||||
warn " host firewall -- 8006, 22, 3128 and 111 are open on every interface. The node"
|
||||
warn " panel reading 'Firewall: Yes' is ignored while the datacenter one reads 'No'."
|
||||
}
|
||||
|
||||
# Did an EARLIER harden-firewall.sh run (from before this skip existed) leave our
|
||||
# firewall on this host? Config, engine, or a DROP policy only we would have set.
|
||||
_pve_leftovers() {
|
||||
PVE_LEFTOVER_WHY=""
|
||||
if [[ -e "$APPLY" || -d "$FW_DIR" ]]; then
|
||||
PVE_LEFTOVER_WHY="our engine/config is still installed"
|
||||
return 0
|
||||
fi
|
||||
# pve-firewall never sets the INPUT policy itself (its own default lives inside
|
||||
# PVEFW-INPUT), so a DROP policy on a Proxmox host was set by something else --
|
||||
# us, or an admin. Say which signal fired rather than assuming.
|
||||
if iptables -S INPUT 2>/dev/null | grep -q '^-P INPUT DROP'; then
|
||||
PVE_LEFTOVER_WHY="INPUT policy is DROP (not something pve-firewall sets)"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
apply_pve() {
|
||||
log "Detected OS: ${OS_ID} (family ${OS_FAMILY}) on Proxmox -- firewall backend: none (skipped)"
|
||||
_pve_explain
|
||||
if _pve_leftovers; then
|
||||
warn " NOTE: this host carries a deny-by-default ruleset (${PVE_LEFTOVER_WHY})."
|
||||
warn " Remove them with: bash $0 disable"
|
||||
fi
|
||||
warn " Override (fights pve-firewall, can lock you out): FW_IGNORE_PVE=1 bash $0 apply"
|
||||
}
|
||||
|
||||
# allow/deny must NOT report success when nothing was opened -- fail loudly and
|
||||
# say where the rule really belongs.
|
||||
allow_deny_pve() {
|
||||
local verb="$1"; shift
|
||||
_pve_explain
|
||||
die "Refusing to $verb $* on a Proxmox host: add it to ${PVE_HOST_FW} (or the GUI) instead."
|
||||
}
|
||||
|
||||
list_pve() {
|
||||
echo "Proxmox host -- firewall managed by pve-firewall (harden-firewall.sh is a no-op here)."
|
||||
echo " cluster: ${PVE_CLUSTER_FW}"
|
||||
echo " host: ${PVE_HOST_FW}"
|
||||
echo
|
||||
echo "pve-firewall status:"
|
||||
if command -v pve-firewall >/dev/null 2>&1; then
|
||||
pve-firewall status 2>&1 | sed 's/^/ /' || true
|
||||
else
|
||||
echo " (pve-firewall not installed)"
|
||||
fi
|
||||
}
|
||||
|
||||
# On a host we skipped there is nothing to unlock -- but a host hardened BEFORE the
|
||||
# skip existed still carries our rules, and `disable` is exactly what you reach for
|
||||
# then, so do the real cleanup rather than claiming there is nothing to do.
|
||||
disable_pve() {
|
||||
if ! _pve_leftovers; then
|
||||
_pve_explain
|
||||
warn "Nothing to disable: this host was skipped, so no rules of ours are in place."
|
||||
warn " If Proxmox's own firewall is locking you out: pve-firewall stop"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "Proxmox host carrying a deny-by-default ruleset (${PVE_LEFTOVER_WHY}) -- removing it."
|
||||
local ipt s f
|
||||
# Policy BEFORE flush: flushing a chain whose policy is still DROP kills the SSH
|
||||
# session this is running over.
|
||||
for ipt in iptables ip6tables; do
|
||||
command -v "$ipt" >/dev/null 2>&1 || continue
|
||||
"$ipt" -P INPUT ACCEPT || true
|
||||
"$ipt" -F INPUT || true
|
||||
done
|
||||
|
||||
# Deliberately NOT fw_save_cmd here, unlike the normal disable path: on Proxmox
|
||||
# that snapshot captures pve-firewall's own PVEFW-* chains and restores the stale
|
||||
# copy at the next boot, before pve-firewall starts. Kill the restore instead.
|
||||
for s in $(fw_restore_services); do svc_disable "$s"; done
|
||||
for f in $(fw_saved_files); do
|
||||
[[ -f "$f" ]] || continue
|
||||
mv -f "$f" "$f.bak-harden-firewall"
|
||||
log "Saved ruleset moved aside: $f -> $f.bak-harden-firewall"
|
||||
done
|
||||
rm -rf "$APPLY" "$FW_DIR"
|
||||
log "Removed ${APPLY}, ${FW_DIR}, and the boot-time restore."
|
||||
|
||||
# The flush took the INPUT -> sshguard jump with it; put it back so brute-force
|
||||
# protection keeps working until harden-ssh.sh installs its boot hook.
|
||||
if command -v sshguard >/dev/null 2>&1; then
|
||||
for ipt in iptables ip6tables; do
|
||||
command -v "$ipt" >/dev/null 2>&1 || continue
|
||||
"$ipt" -N sshguard 2>/dev/null || true
|
||||
"$ipt" -C INPUT -p tcp --dport "$SSH_PORT" -j sshguard 2>/dev/null || "$ipt" -I INPUT -p tcp --dport "$SSH_PORT" -j sshguard || true
|
||||
done
|
||||
warn "Re-added the INPUT -> sshguard jump on ${SSH_PORT}; re-run harden-ssh.sh for its boot hook."
|
||||
fi
|
||||
|
||||
# Hand the ruleset back to pve-firewall.
|
||||
if command -v pve-firewall >/dev/null 2>&1; then
|
||||
pve-firewall restart >/dev/null 2>&1 || warn "pve-firewall restart failed -- check 'pve-firewall status'."
|
||||
fi
|
||||
_pve_explain
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
# Subcommands
|
||||
# ============================================================================
|
||||
cmd_apply() {
|
||||
if [[ "$FW_BACKEND" == pve ]]; then apply_pve; return; fi
|
||||
if [[ "$FW_BACKEND" == firewalld ]]; then apply_firewalld; return; fi
|
||||
|
||||
log "Detected OS: ${OS_ID} (family ${OS_FAMILY}, init ${INIT_SYSTEM}) -- firewall backend: iptables"
|
||||
@@ -407,6 +578,7 @@ cmd_apply() {
|
||||
|
||||
cmd_allow() {
|
||||
[[ "$#" -ge 1 ]] || die "usage: allow <port[/proto][@cidr]|web|http|https>..."
|
||||
[[ "$FW_BACKEND" == pve ]] && allow_deny_pve allow "$@"
|
||||
[[ "$FW_BACKEND" == firewalld ]] && { allow_firewalld "$@"; return; }
|
||||
ensure_installed
|
||||
local lines file="$PORTS_DIR/manual.rule"
|
||||
@@ -424,6 +596,7 @@ cmd_allow() {
|
||||
|
||||
cmd_deny() {
|
||||
[[ "$#" -ge 1 ]] || die "usage: deny <port[/proto][@cidr]|web|http|https>..."
|
||||
[[ "$FW_BACKEND" == pve ]] && allow_deny_pve deny "$@"
|
||||
[[ "$FW_BACKEND" == firewalld ]] && { deny_firewalld "$@"; return; }
|
||||
ensure_installed
|
||||
local lines l file tmp
|
||||
@@ -448,6 +621,7 @@ cmd_deny() {
|
||||
}
|
||||
|
||||
cmd_list() {
|
||||
[[ "$FW_BACKEND" == pve ]] && { list_pve; return; }
|
||||
[[ "$FW_BACKEND" == firewalld ]] && { list_firewalld; return; }
|
||||
echo "Registered ports ($PORTS_DIR):"
|
||||
if ls "$PORTS_DIR"/*.rule >/dev/null 2>&1; then
|
||||
@@ -466,6 +640,7 @@ cmd_list() {
|
||||
# Recovery escape hatch: open the policy and flush our rules, then persist that
|
||||
# open state so a reboot does not re-DROP. The sshguard chain is left intact.
|
||||
cmd_disable() {
|
||||
[[ "$FW_BACKEND" == pve ]] && { disable_pve; return; }
|
||||
[[ "$FW_BACKEND" == firewalld ]] && { disable_firewalld; return; }
|
||||
local ipt
|
||||
for ipt in iptables ip6tables; do
|
||||
@@ -479,7 +654,9 @@ cmd_disable() {
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,50p' "$0" | sed 's/^#\{0,1\} \{0,1\}//'
|
||||
# The whole header comment block, however long it grows: line 2 until the
|
||||
# first non-comment line.
|
||||
awk 'NR == 1 { next } !/^#/ { exit } { print }' "$0" | sed 's/^#\{0,1\} \{0,1\}//'
|
||||
}
|
||||
|
||||
# ============================================================================
|
||||
|
||||
@@ -307,6 +307,20 @@ EOF
|
||||
# standalone boot hook. Otherwise fall back to the minimal init-agnostic boot
|
||||
# hook that just (re)inserts the jump at every boot.
|
||||
: "${ENABLE_FIREWALL:=1}"
|
||||
# Proxmox (VE/PMG) is Debian underneath, but pve-firewall already owns the host
|
||||
# ruleset -- harden-firewall.sh skips those hosts, so fall through to the boot
|
||||
# hook and keep sshguard's jump. That jump is safe there: pve-firewall restores
|
||||
# with --noflush and only ever flushes its own PVEFW-* chains, and the hook it
|
||||
# adds to INPUT is APPENDED -- so a jump inserted with -I sits ahead of it and
|
||||
# keeps getting first look at NEW connections, firewall enabled or not.
|
||||
# FW_IGNORE_PVE=1 forces our firewall anyway.
|
||||
if [[ "$ENABLE_FIREWALL" == "1" && "${FW_IGNORE_PVE:-0}" != "1" ]] && is_proxmox; then
|
||||
warn "Proxmox detected -- pve-firewall owns the host firewall; installing only the sshguard jump hook."
|
||||
# A host hardened before the skip existed still has a live DROP chain that only
|
||||
# accepts the OLD port -- changing SSH_PORT here would lock you out of it.
|
||||
[[ -x /usr/local/sbin/firewall-apply || -d /etc/firewall ]] && warn " This host still has an older harden-firewall.sh ruleset. Run 'harden-firewall.sh disable' FIRST, especially if you are changing SSH_PORT."
|
||||
ENABLE_FIREWALL=0
|
||||
fi
|
||||
if [[ "$ENABLE_FIREWALL" == "1" && -f "$SCRIPT_DIR/harden-firewall.sh" ]]; then
|
||||
log "Installing host firewall (deny-by-default INPUT; carries the sshguard jump)..."
|
||||
SSH_PORT="$SSH_PORT" OPEN_PORTS="${OPEN_PORTS:-}" \
|
||||
|
||||
@@ -285,6 +285,20 @@ EOF
|
||||
# standalone boot hook. Otherwise fall back to the minimal init-agnostic boot
|
||||
# hook that just (re)inserts the jump at every boot.
|
||||
: "${ENABLE_FIREWALL:=1}"
|
||||
# Proxmox (VE/PMG) is Debian underneath, but pve-firewall already owns the host
|
||||
# ruleset -- harden-firewall.sh skips those hosts, so fall through to the boot
|
||||
# hook and keep sshguard's jump. That jump is safe there: pve-firewall restores
|
||||
# with --noflush and only ever flushes its own PVEFW-* chains, and the hook it
|
||||
# adds to INPUT is APPENDED -- so a jump inserted with -I sits ahead of it and
|
||||
# keeps getting first look at NEW connections, firewall enabled or not.
|
||||
# FW_IGNORE_PVE=1 forces our firewall anyway.
|
||||
if [[ "$ENABLE_FIREWALL" == "1" && "${FW_IGNORE_PVE:-0}" != "1" ]] && is_proxmox; then
|
||||
warn "Proxmox detected -- pve-firewall owns the host firewall; installing only the sshguard jump hook."
|
||||
# A host hardened before the skip existed still has a live DROP chain that only
|
||||
# accepts the OLD port -- changing SSH_PORT here would lock you out of it.
|
||||
[[ -x /usr/local/sbin/firewall-apply || -d /etc/firewall ]] && warn " This host still has an older harden-firewall.sh ruleset. Run 'harden-firewall.sh disable' FIRST, especially if you are changing SSH_PORT."
|
||||
ENABLE_FIREWALL=0
|
||||
fi
|
||||
if [[ "$ENABLE_FIREWALL" == "1" && -f "$SCRIPT_DIR/harden-firewall.sh" ]]; then
|
||||
log "Installing host firewall (deny-by-default INPUT; carries the sshguard jump)..."
|
||||
SSH_PORT="$SSH_PORT" OPEN_PORTS="${OPEN_PORTS:-}" \
|
||||
|
||||
+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