Debian GNU/Linux · Network

Run exactly one host firewall

When it fails: No host firewall is active, or several firewall tools compete for the same rules. Without a host firewall every listening service is reachable from the network; with several, rules overwrite each other in ways nobody planned.

CB-DEB-NET-005highactiveautomatedautomated fix, lockout riskprofile core

Description

The Linux kernel filters packets with netfilter. On Debian, its rules are managed by one of these tools, each started at boot by its own service:

  • ufw, a simple front end, active when ufw status shows Status: active;
  • nftables, rules loaded from /etc/nftables.conf by nftables.service;
  • firewalld;
  • iptables-persistent, rules restored by netfilter-persistent.service.

This control requires exactly one of them to be enabled. Its default policy is covered by CB-DEB-NET-006.

Debian 13 favours nftables and the CIS Benchmarks favour ufw; both are accepted. ufw on Debian writes its rules through iptables-nft, so nft list ruleset also shows them: that does not count as a second firewall as long as nftables.service is not enabled. Docker and libvirt add their own rules beside the host firewall; they are not counted.

Rationale

A host firewall limits the services reachable from the network to the ones that are meant to be, even when a package starts a new listener or a service binds to all addresses by mistake. Two firewall tools flush or reorder each other's rules on reload, which leaves the host either wide open or cut off after a restart.

Impact

The automatic fix installs and enables ufw when no firewall is active. It allows the SSH ports first, but ufw then drops every other incoming connection: services such as web or database servers become unreachable until they are allowed with ufw allow <port>/<protocol>. It is disabled by default; list the listening services with ss -tulpn and allow them before enabling it.

Audit

  1. ufw status (if installed): Status: active counts as one.
  2. systemctl is-enabled nftables.service firewalld.service netfilter-persistent.service: each enabled counts as one. Exactly one must be counted.

Automated: every check must pass. Scanners report each check by its id.

single-firewall

Firewall tools enabled at boot.

script: |
  active=
  if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
    active="$active ufw"
  fi
  for u in nftables firewalld netfilter-persistent; do
    [ "$(systemctl is-enabled "$u.service" 2>/dev/null)" = enabled ] && active="$active $u"
  done
  echo "active firewall tools:${active:- none}"
  set -- $active
  [ "$#" -eq 1 ]

Remediation

  • No firewall: install ufw, allow SSH and the other services that must stay reachable, then enable it:
    apt install ufw
    ufw allow 22/tcp
    ufw enable
    
    Or write /etc/nftables.conf (see CB-DEB-NET-006) and run systemctl enable --now nftables.
  • Several tools: keep one, then disable the others, for example ufw disable, systemctl disable --now nftables or apt purge iptables-persistent.
Restartnone
Disruptionhigh
Lockout riskyes: can cut off administrative access. Off by default in generated scripts and playbooks, opt in through a tailoring file.

Rollback

Run ufw disable. Installed packages can stay.

enable-ufw

# CB-DEB-NET-005: when no firewall is active, install and enable ufw, allowing SSH first.
# Does nothing when exactly one firewall tool is active; fails when several are (choice left to a human).
PATH="$PATH:/usr/sbin"
active=
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
  active="$active ufw"
fi
for u in nftables firewalld netfilter-persistent; do
  [ "$(systemctl is-enabled "$u.service" 2>/dev/null)" = enabled ] && active="$active $u"
done
# shellcheck disable=SC2086 # word splitting is the point
set -- $active
[ "$#" -eq 1 ] && exit 0
if [ "$#" -gt 1 ]; then
  echo "several firewall tools are enabled:$active; keep one by hand" >&2
  exit 1
fi
if ! command -v ufw >/dev/null 2>&1; then
  DEBIAN_FRONTEND=noninteractive apt-get install -y -q ufw >/dev/null || exit 1
fi
for f in /etc/ufw/ufw.conf /etc/ufw/user.rules /etc/ufw/user6.rules; do cb_backup "$f"; done
# Allow every port sshd listens on before turning the firewall on.
for p in $(sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }'); do
  ufw allow "$p/tcp" comment 'SSH (CIRSEC Baselines)' >/dev/null || exit 1
done
ufw --force enable

Handwritten code reads parameters from CB_<NAME> environment variables. Each release turns these fixes into remediate.sh and an Ansible role per profile: dry run by default, a fix runs only when its check fails.

References

Mappings

FrameworkIDRelationNote
NIST SP 800-53 Rev. 55.1.1SC-7partial

How this control relates to the other framework's: equivalent, stricter, looser, partial or related.

Threats mitigated

  • related CB-DEB-NET-006 Drop incoming and forwarded traffic by default, with SSH allowed

People and lifecycle

Authors
Verified2026-10-18 by Sn0wAlice on 12, Proxmox VM 921, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64. No firewall reported, fix installs ufw, allows the SSH port, enables it; SSH by key still accepted locally and port 22 reachable from the lab host, other ports blocked; ufw plus nftables.service reported and fix refuses; kept after reboot.
Verified2026-10-18 by Sn0wAlice on 13, Proxmox VM 931, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64. No firewall reported, fix installs ufw, allows the SSH port, enables it; SSH by key still accepted locally and port 22 reachable from the lab host, other ports blocked; ufw plus nftables.service reported and fix refuses; kept after reboot.
Introduced inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr