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

As released in v0.1.0 beta · pre-release. Current state.

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