Handbook sections▾
Foundations
Understanding the threats to a Linux hostWhy we hardenThe defense principles behind every ruleEffective configuration: the truth no file holdsThe standards Pavois maps toSOCLE control ID modelEvidence & exportsHow the A-E grade is computedWhat a PASS proves: the qualified verdictTrust model for the evidence bundleIs SOCLE just your own norm? (governance & the circularity question)What Pavois covers, and what it doesn'tDomains
Hardening SSHHardening PAMMandatory Access Control (SELinux / AppArmor)Hardening the host firewallHardening sudoFile Permissions & OwnershipMount & filesystem hardeningHardening kernel modulesHardening the kernel & network with sysctlAudit logging with auditdHardening logging with journald and rsyslogHardening systemd servicesPackage hygieneHardening the bootloader (GRUB)Synchronizing timeLogin banners and MOTDControlling cron and at accessHardening the GNOME desktop (dconf)Tooling
Undo a hardening run: restore pointsOperating Pavois: privileges, air-gap, timing, exceptionsRun Pavois in CI (GitHub Actions)Feature status: delivered, partial, roadmapGovernance: licence, versioning, provenance, securityHardening 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/denyand allow-list only the ports a role needs (22/tcpfor SSH,443/tcpfor 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
sshdhardening, it contains it. - One backend, clearly owned: run a single filter (
nftables,firewalld, orufw), 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/tcpcuts 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 --reloadThe 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.