Ubuntu · Network

Drop incoming and forwarded traffic by default, with SSH allowed

When it fails: The host firewall accepts incoming or forwarded traffic that no rule allows. A firewall whose default policy accepts traffic only blocks what someone thought of in advance.

CB-UBU-NET-006highactiveautomatedautomated fix, lockout riskprofile core

As released in v0.1.0 (beta). Current state.

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) and deny, reject or disabled (routed) in ufw status verbose, and a rule allowing each SSH port;
  • with nftables (when nftables.service is enabled and ufw is not active): a base chain on the input hook and one on the forward hook with policy drop, and an accept rule for each SSH port;
  • with iptables-persistent (when netfilter-persistent.service is enabled and neither ufw nor nftables is): INPUT and FORWARD policies DROP for iptables and, with IPv6, ip6tables, and an ACCEPT rule 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 verbose shows deny (incoming) and deny, reject or disabled (routed), and ufw status lists an ALLOW or LIMIT rule for each port of sshd -T | grep '^port '.
  • nftables: nft list ruleset has a chain with hook input and policy drop, a chain with hook forward and policy drop, and a rule accepting tcp dport for each SSH port.
  • iptables-persistent: iptables -S and ip6tables -S show -P INPUT DROP and -P FORWARD DROP, and iptables -S INPUT has an ACCEPT rule for --dport of 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.conf with, at least:
    table 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;
      }
    }
    
    Check it with nft -c -f /etc/nftables.conf, then run systemctl reload nftables from 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,RELATED rules) before iptables -P INPUT DROP and iptables -P FORWARD DROP, do the same with ip6tables, then save with netfilter-persistent save.
Restartnone
Disruptionhigh
Lockout riskyes: 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

Mappings

FrameworkIDRelationNote
NIST SP 800-53 Rev. 55.1.1SC-7(5)equivalent

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

Threats mitigated

People and lifecycle

Authors
Verified2026-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.
Verified2026-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.
Verified2026-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.
Verified2026-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 inv0.1.0
Created, updated2026-10-19, 2026-10-19
Translationsfr