feat(firewall): drive firewalld on Alma/RHEL with full CLI parity
A fresh Alma box has firewalld active, and the iptables-based harden-firewall.sh refused to run there (caught by harden-ssh's '|| warn', so the host firewall was silently skipped). Use firewalld natively on the rhel family instead of fighting it. - harden-firewall.sh: family-aware backend. On rhel, apply/allow/deny/list/disable drive firewall-cmd (deny-by-default zone, SSH + registered ports, ping policy, source-restricted rich rules); Alpine/Debian keep the iptables engine unchanged. FW_BACKEND=iptables|firewalld overrides. - oslib: install_firewalld(); sshguard_backend() prefers sshg-fw-firewalld on rhel so brute-force blocks land in firewalld (no INPUT->sshguard jump needed). - Deployments already fall through to a firewall-cmd branch when the iptables engine is absent, so they need no changes. - README + script header document the per-family backend. harden-ssh / harden-jumphost are unchanged -- they call harden-firewall.sh apply and read sshguard_backend(), so the switch happens underneath them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -92,7 +92,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 iptables baseline: loopback, established, ICMP, SSH (configurable port) + registered ports. Persisted **natively** (no boot hook). Deployments add ports via `/etc/firewall/ports.d` + `firewall-apply`. Sub-commands: `allow`/`deny`/`list`/`disable`. |
|
||||
| [`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. |
|
||||
| [`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`. |
|
||||
@@ -152,22 +152,29 @@ services with Alpine-specific wiring, so it isn't part of the tri-distro set.
|
||||
## Host firewall
|
||||
|
||||
[`scripts/harden-firewall.sh`](scripts/harden-firewall.sh) installs a
|
||||
**deny-by-default** iptables baseline: `INPUT` drops everything except loopback,
|
||||
established/related, ICMP, and SSH on the configured port — plus any ports a
|
||||
deployment registers. `OUTPUT` stays open and `FORWARD` is left untouched, so
|
||||
Docker container networking is unaffected. The harden scripts and
|
||||
`cloud-init/base.yml` / `jumphost.yml` install it automatically
|
||||
(`ENABLE_FIREWALL=1` by default; set `0` to fall back to the minimal
|
||||
sshguard-only jump).
|
||||
**deny-by-default** baseline, with the backend chosen per family (override with
|
||||
`FW_BACKEND=iptables|firewalld`):
|
||||
|
||||
- **Alpine / Debian → iptables.** `INPUT` drops everything except loopback,
|
||||
established/related, ICMP, and SSH on the configured port — plus any ports a
|
||||
deployment registers.
|
||||
- **Alma / RHEL → firewalld** (its native firewall). The default zone is already
|
||||
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).
|
||||
|
||||
`OUTPUT`/egress stays open and `FORWARD` is left untouched, so Docker container
|
||||
networking is unaffected. The harden scripts and `cloud-init/base.yml` /
|
||||
`jumphost.yml` install it automatically (`ENABLE_FIREWALL=1` by default).
|
||||
|
||||
- **Configurable SSH port** — read live from `sshd_config`, so a bastion on
|
||||
`2222` is firewalled correctly with no extra flags. Restrict the source with
|
||||
`FW_SSH_SOURCE=<cidr>`; drop ping with `FW_ALLOW_PING=0`.
|
||||
- **Native persistence, no boot hook** — rules are saved and restored by the
|
||||
distro's own package: `iptables` + `ip6tables` (Alpine/OpenRC),
|
||||
`iptables-persistent` (Debian), or `iptables-services` (Alma). The saved
|
||||
ruleset carries the `INPUT → sshguard` jump, so brute-force protection
|
||||
survives reboot without a custom hook.
|
||||
- **Native persistence, no boot hook** — on iptables hosts the ruleset is saved
|
||||
and restored by the distro's own package: `iptables` + `ip6tables`
|
||||
(Alpine/OpenRC) or `iptables-persistent` (Debian); the saved ruleset carries
|
||||
the `INPUT → sshguard` jump. On firewalld hosts every change is `--permanent`,
|
||||
so it persists across reboot natively and sshguard manages its own blocks.
|
||||
- **Scripted additions** — deployments drop a rule file and re-apply:
|
||||
```sh
|
||||
printf '80/tcp\n443/tcp\n' > /etc/firewall/ports.d/mystack.rule
|
||||
@@ -179,10 +186,12 @@ sshguard-only jump).
|
||||
reach the host through nat/`FORWARD` and **bypass `INPUT`**, so the firewall
|
||||
neither blocks nor needs to open them; the per-stack rule files are
|
||||
belt-and-braces for any host-bound bind and self-documentation.
|
||||
- **Recovery** — `harden-firewall.sh disable` flushes the rules and sets `INPUT`
|
||||
back to `ACCEPT` (persisted), should a rule ever lock you out. A re-apply never
|
||||
drops the live SSH session: the established-connection accept is added before
|
||||
the policy flips to `DROP`.
|
||||
- **Recovery** — `harden-firewall.sh disable` un-locks you: on iptables it
|
||||
flushes the rules and sets `INPUT` back to `ACCEPT` (persisted); on firewalld it
|
||||
re-opens SSH (the `ssh` service + the configured port) and leaves firewalld
|
||||
running. A re-apply never drops the live SSH session — on iptables the
|
||||
established-connection accept is added before the policy flips to `DROP`, and
|
||||
firewalld reloads preserve established connections.
|
||||
|
||||
## SSH login notifications
|
||||
|
||||
|
||||
Reference in New Issue
Block a user