Debian GNU/Linux · 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-DEB-NET-006highactiveautomatedautomated fix, lockout riskprofile core

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.

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 rulesets are written by hand. The SSH rule check looks for the port number in an accept rule of nft list ruleset; complex rulesets (sets, maps, jumps) may need a manual review.

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.

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"

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.
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-DEB-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
for f in /etc/default/ufw /etc/ufw/user.rules /etc/ufw/user6.rules; do cb_backup "$f"; done
for p in $(sshd -T 2>/dev/null | awk '$1 == "port" { print $2 }'); 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

FrameworkIDRelation
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-18 by Sn0wAlice on 12, Proxmox VM 921, Debian 12 genericcloud image, kernel 6.1.0-54-cloud-amd64. ufw defaults and SSH rule checked before and after the fix; nftables checks tested with accept, drop without SSH rule and drop with SSH rule rulesets; SSH by key works after fix and reboot.
Verified2026-10-18 by Sn0wAlice on 13, Proxmox VM 931, Debian 13 genericcloud image, kernel 6.12.113+deb13-cloud-amd64. ufw defaults and SSH rule checked before and after the fix; nftables checks tested with accept, drop without SSH rule and drop with SSH rule rulesets; SSH by key works after fix and reboot.
Introduced inv0.1.0
Created, updated2026-10-18, 2026-10-18
Translationsfr