Handbook sections

Hardening the host firewall

Last reviewed

A host firewall is the last gate in front of every listening service. Default-deny inbound so only the ports you chose are ever reachable, and allow SSH *before* you enable it.

The threat: every reachable port is an open door

Every daemon that calls listen() opens a door on the network, and many open more doors than you think: a database that should answer only on 127.0.0.1 ends up bound to 0.0.0.0, a debug endpoint stays up after a deploy, a package pulls in a service you never wanted. Without a packet filter, all of them are exposed to whatever can route to the host: the office LAN, a cloud VPC, the open Internet. Attackers don't guess; they mass-scan the entire IPv4 space for open ports in minutes, fingerprint the service, and fire the matching exploit. And the moment one machine falls, a flat network with no host firewalls turns a single foothold into trivial lateral movement to every neighbour.

Why harden it

The size of your attack surface is exactly the set of reachable ports, and you want that set as small and as deliberate as possible. A host firewall lets you assert, at the kernel, this machine accepts inbound traffic on these ports and nothing else, independently of how each service happens to be configured. It is defense in depth: even if a daemon binds every interface, or a new port appears after an update, the firewall keeps the network silent until you explicitly open it. It is also your safety net against the gap between installed and actually filtering: a firewall package present but stopped protects nothing.

Defense principles applied

  • Default-deny: set the inbound policy to drop/deny and allow-list only the ports a role needs (22/tcp for SSH, 443/tcp for a web server). Everything unlisted is refused.
  • Attack-surface reduction: fewer reachable ports means fewer services an attacker can even talk to.
  • Defense in depth: the firewall backs up service config; it does not replace sshd hardening, it contains it.
  • One backend, clearly owned: run a single filter (nftables, firewalld, or ufw), never two stacked layers with conflicting rulesets. All three drive nftables in the kernel underneath.
  • Fail safe, stay reachable: a default-deny policy that forgets 22/tcp cuts your own SSH; allow management access first and keep an out-of-band console.

What Pavois audits: a filter actually running

Pavois audits the effective runtime state, not a package list or a config file. Its firewall-present rule loops over ufw, nftables and firewalld and runs systemctl is-active --quiet <service>; it is compliant only if at least one packet filter is actually running. A firewall installed but stopped, masked, or failed to start is correctly flagged, exactly the gap a file-based scan (OVAL/oscap) misses. The rule carries the real mappings ANSSI BP-028 R50, CIS 4.x, NIST 3.1.3, PCI-DSS 1.2.1 and STIG, and the harden plan can install and enable a backend (default firewalld).

How do you set it without locking yourself out?

The order matters: allow management access first, enable the policy last.

# UFW (Debian/Ubuntu)
ufw allow OpenSSH        # or: ufw allow 22/tcp  -- BEFORE enabling
ufw default deny incoming
ufw default allow outgoing
ufw enable               # only now

# firewalld (RHEL)
firewall-cmd --permanent --add-service=ssh
firewall-cmd --permanent --set-target=DROP   # on the active zone
firewall-cmd --reload

The firewalld trap: runtime vs permanent. A plain firewall-cmd --add-service=... changes only the runtime ruleset and is lost on --reload or reboot. Use --permanent then --reload (or --runtime-to-permanent) so the rule actually persists. This is the effective-config gap: the live ruleset and the saved one can differ.

Verify

Read the effective ruleset, not just the config:

ufw status verbose                  # UFW: policy + allowed ports
firewall-cmd --list-all             # firewalld: the active zone's services/ports
nft list ruleset                    # the real kernel ruleset, whatever the frontend
ss -tulpen                          # what is actually listening (cross-check)

Coverage and gaps

Pavois confirms a packet filter is running, the honest baseline. It does not yet audit the content of the ruleset (default-deny target, per-zone services, rich rules), which is on the roadmap; today you author and verify the allow/deny rules yourself with the commands above. The handbook is explicit about this so the control is not mistaken for full ruleset assurance.

FAQ

Why does Pavois only check that a firewall runs? Because a present-but-stopped filter is the most common real failure, and it is the honest, portable baseline across ufw/firewalld/nftables. Full ruleset/zone auditing is roadmap.

ufw, firewalld or nftables? Pick one. ufw is simplest (Debian/Ubuntu), firewalld is zone-based (RHEL), nftables is the lowest-level and what the others drive. Running two at once produces conflicting rules.

My firewalld rule vanished after a reload. It was runtime-only. Re-add it with --permanent then --reload, or firewall-cmd --runtime-to-permanent.