Description
The default policy of a firewall applies to packets that match no rule. This control requires
the policy for incoming and forwarded packets to drop or reject them, while the SSH ports used by
sshd stay allowed so that administrators keep access:
- with ufw:
Default: deny (incoming)anddeny,rejectordisabled (routed)inufw status verbose, and a rule allowing each SSH port; - with nftables (when
nftables.serviceis enabled and ufw is not active): a base chain on theinputhook and one on theforwardhook withpolicy drop, and anacceptrule for each SSH port; - with iptables-persistent (when
netfilter-persistent.serviceis enabled and neither ufw nor nftables is):INPUTandFORWARDpoliciesDROPfor iptables and, with IPv6, ip6tables, and anACCEPTrule for each SSH port.
ufw on Ubuntu ships DEFAULT_INPUT_POLICY="DROP" and DEFAULT_FORWARD_POLICY="DROP", so once
it is enabled (CB-UBU-NET-005) only the SSH rule matters. The automatic fix only handles ufw: it
adds an allow rule for each SSH port that has none, then sets the incoming and routed defaults
to deny. nftables and iptables rulesets are written by hand. The SSH rule checks look for the
port number in an accept rule; complex rulesets (sets, maps, jumps, user chains) may need a
manual review. The SSH ports come from sshd -T, which fails on 24.04 and later until a first
connection has started ssh.service (missing /run/sshd): the checks then report an error,
and the fix creates the directory first.
Rationale
Default deny turns the firewall into an allow list: a service started by mistake, or a new one installed by a package, is not reachable until someone decides it should be. Dropping forwarded packets keeps the host from relaying traffic between networks. Checking that SSH is allowed avoids a policy that locks administrators out at the next reload.
Impact
Incoming connections to services without an allow rule are dropped. Containers and virtual machines that rely on forwarding through the host (Docker, libvirt) manage their own forward rules; check them after the change.
Audit
- ufw:
ufw status verboseshowsdeny (incoming)anddeny,rejectordisabled (routed), andufw statuslists anALLOWorLIMITrule for each port ofsshd -T | grep '^port '. - nftables:
nft list rulesethas a chain withhook inputandpolicy drop, a chain withhook forwardandpolicy drop, and a rule acceptingtcp dportfor each SSH port. - iptables-persistent:
iptables -Sandip6tables -Sshow-P INPUT DROPand-P FORWARD DROP, andiptables -S INPUThas anACCEPTrule for--dportof each SSH port.
Automated: every check must pass. Scanners report each check by its id.
ufw-defaults
ufw default policies for incoming and routed traffic.
script: |
ufw status 2>/dev/null | grep -q '^Status: active' || { echo 'ufw is not active'; exit 2; }
d=$(ufw status verbose 2>/dev/null | sed -n 's/^Default: //p')
echo "$d"
printf '%s' "$d" | grep -Eq '(deny|reject) \(incoming\)' &&
printf '%s' "$d" | grep -Eq '(deny|reject|disabled) \(routed\)'
ufw-ssh
ufw rule allowing each SSH port.
script: |
s=$(ufw status 2>/dev/null)
printf '%s\n' "$s" | grep -q '^Status: active' || { echo 'ufw is not active'; exit 2; }
ports=$(PATH="$PATH:/usr/sbin" sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }')
[ -n "$ports" ] || exit 3
rc=0
for p in $ports; do
if printf '%s\n' "$s" | grep -Eq "^($p(/tcp)?|OpenSSH)[[:space:]]+(ALLOW|LIMIT)"; then
echo "SSH port $p allowed"
else
echo "SSH port $p not allowed"; rc=1
fi
done
exit "$rc"
nft-defaults
nftables input and forward base chains with policy drop.
script: |
[ "$(systemctl is-enabled nftables.service 2>/dev/null)" = enabled ] || { echo 'nftables.service is not enabled'; exit 2; }
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
echo 'ufw manages the firewall'; exit 2
fi
r=$(nft list ruleset 2>/dev/null) || exit 3
rc=0
for h in input forward; do
if printf '%s\n' "$r" | grep -Eq "hook $h .*policy drop"; then echo "$h: policy drop"; else echo "$h: no chain with policy drop"; rc=1; fi
done
exit "$rc"
nft-ssh
nftables accept rule for each SSH port.
script: |
[ "$(systemctl is-enabled nftables.service 2>/dev/null)" = enabled ] || { echo 'nftables.service is not enabled'; exit 2; }
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
echo 'ufw manages the firewall'; exit 2
fi
r=$(nft list ruleset 2>/dev/null) || exit 3
ports=$(PATH="$PATH:/usr/sbin" sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }')
[ -n "$ports" ] || exit 3
rc=0
for p in $ports; do
if printf '%s\n' "$r" | grep -E 'tcp dport .*accept' | grep -Eq "(^|[^0-9])$p([^0-9]|$)"; then
echo "SSH port $p accepted"
else
echo "SSH port $p not accepted"; rc=1
fi
done
exit "$rc"
iptables-defaults
iptables and ip6tables INPUT and FORWARD policies with iptables-persistent.
script: |
[ "$(systemctl is-enabled netfilter-persistent.service 2>/dev/null)" = enabled ] || { echo 'netfilter-persistent.service is not enabled'; exit 2; }
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
echo 'ufw manages the firewall'; exit 2
fi
[ "$(systemctl is-enabled nftables.service 2>/dev/null)" = enabled ] && { echo 'nftables manages the firewall'; exit 2; }
PATH="$PATH:/usr/sbin"
rc=0
for t in iptables ip6tables; do
[ "$t" = ip6tables ] && [ ! -d /proc/sys/net/ipv6 ] && continue
r=$($t -S 2>/dev/null) || exit 3
for c in INPUT FORWARD; do
if printf '%s\n' "$r" | grep -Eq "^-P $c (DROP|REJECT)$"; then echo "$t $c: DROP"; else echo "$t $c: not DROP"; rc=1; fi
done
done
exit "$rc"
iptables-ssh
iptables accept rule for each SSH port with iptables-persistent.
script: |
[ "$(systemctl is-enabled netfilter-persistent.service 2>/dev/null)" = enabled ] || { echo 'netfilter-persistent.service is not enabled'; exit 2; }
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -q '^Status: active'; then
echo 'ufw manages the firewall'; exit 2
fi
[ "$(systemctl is-enabled nftables.service 2>/dev/null)" = enabled ] && { echo 'nftables manages the firewall'; exit 2; }
PATH="$PATH:/usr/sbin"
r=$(iptables -S INPUT 2>/dev/null) || exit 3
ports=$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }')
[ -n "$ports" ] || exit 3
rc=0
for p in $ports; do
if printf '%s\n' "$r" | grep -E -- '-j ACCEPT' | grep -Eq -- "--dports? ([0-9,:]*[,:])?$p([,:][0-9,:]*)?( |$)"; then
echo "SSH port $p accepted"
else
echo "SSH port $p not accepted"; rc=1
fi
done
exit "$rc"
Remediation
- ufw: allow SSH first, then set the defaults:
ufw allow 22/tcp ufw default deny incoming ufw default deny routed - nftables: write
/etc/nftables.confwith, at least:
Check it withtable inet filter { chain input { type filter hook input priority filter; policy drop; ct state established,related accept iif lo accept meta l4proto { icmp, ipv6-icmp } accept tcp dport 22 accept } chain forward { type filter hook forward priority filter; policy drop; } }nft -c -f /etc/nftables.conf, then runsystemctl reload nftablesfrom a console, or with a scheduled rollback, in case the SSH rule is wrong. - iptables-persistent: add
iptables -A INPUT -p tcp --dport 22 -j ACCEPT(with the loopback and-m conntrack --ctstate ESTABLISHED,RELATEDrules) beforeiptables -P INPUT DROPandiptables -P FORWARD DROP, do the same with ip6tables, then save withnetfilter-persistent save.
| Restart | none |
| Disruption | high |
| Lockout risk | yes: can cut off administrative access. Off by default in generated scripts and playbooks, opt in through a tailoring file. |
Rollback
ufw: ufw default allow incoming and ufw default allow routed (or restore the backed up
/etc/default/ufw and run ufw reload). nftables: restore the previous /etc/nftables.conf
and reload.
ufw-policy
# CB-UBU-NET-006: with ufw active, allow each SSH port that has no rule, then deny incoming and routed by default.
PATH="$PATH:/usr/sbin"
s=$(ufw status 2>/dev/null)
printf '%s\n' "$s" | grep -q '^Status: active' || exit 0
# sshd -T needs /run/sshd, which only exists once ssh.service has run (ssh.socket on 24.04 and later).
if command -v sshd >/dev/null 2>&1; then
[ -d /run/sshd ] || mkdir -p -m 0755 /run/sshd
ports=$(sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }')
if [ -z "$ports" ]; then
echo 'cannot read the SSH ports with sshd -T; ufw defaults left unchanged' >&2
exit 1
fi
fi
for f in /etc/default/ufw /etc/ufw/user.rules /etc/ufw/user6.rules; do cb_backup "$f"; done
for p in $ports; do
printf '%s\n' "$s" | grep -Eq "^($p(/tcp)?|OpenSSH)[[:space:]]+(ALLOW|LIMIT)" && continue
ufw allow "$p/tcp" comment 'SSH (CIRSEC Baselines)' >/dev/null || exit 1
done
ufw default deny incoming >/dev/null || exit 1
ufw default deny routed >/dev/null
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
- ufw(8), Ubuntu manual pages Canonical, vendor-doc, accessed 2026-10-19
- nft(8), Ubuntu manual pages Canonical, vendor-doc, accessed 2026-10-19
- iptables(8), Ubuntu manual pages Canonical, vendor-doc, accessed 2026-10-19
- Ubuntu Server documentation - Firewalls Canonical, vendor-doc, accessed 2026-10-19
- NIST SP 800-41 Rev. 1 - Guidelines on Firewalls and Firewall Policy NIST, guide, accessed 2026-10-19
Mappings
| Framework | ID | Relation | Note |
|---|---|---|---|
| NIST SP 800-53 Rev. 55.1.1 | SC-7(5) | equivalent |
How this control relates to the other framework's: equivalent, stricter, looser, partial or related.
Threats mitigated
Related controls
- requires
CB-UBU-NET-005Run exactly one host firewall
People and lifecycle
| Authors |
|
| Verified | 2026-10-19 by Sn0wAlice on 20.04, Proxmox VM, Ubuntu 20.04 cloud image, kernel 5.4.0-216-generic, BIOS boot. ufw defaults and SSH rule pass once ufw is enabled; nftables (drop with and without SSH rule) and iptables-persistent on legacy iptables (drop, SSH rule, multiport rule, missing rule) checks tested; SSH by key works after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 22.04, Proxmox VM, Ubuntu 22.04 cloud image, kernel 5.15.0-198-generic, BIOS boot. Not applicable while no firewall is active; ufw defaults and SSH rule pass after NET-005; SSH by key works after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 24.04, Proxmox VM, Ubuntu 24.04 cloud image, kernel 6.8.0-147-generic, BIOS boot. Not applicable while no firewall is active; ufw defaults and SSH rule pass after NET-005; SSH by key works after reboot. |
| Verified | 2026-10-19 by Sn0wAlice on 26.04, Proxmox VM, Ubuntu 26.04 cloud image, kernel 7.0.0-38-generic, BIOS boot. ufw defaults and SSH rule pass after NET-005; nftables (drop with and without SSH rule) and iptables-persistent (drop, SSH rule, multiport rule, missing rule) checks tested; SSH by key works after reboot. |
| Introduced in | v0.1.0 |
| Created, updated | 2026-10-19, 2026-10-19 |
| Translations | fr |