Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 38 additions & 10 deletions cli/lib/nftban/cli/cmd_firewall.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1315,22 +1315,50 @@ _firewall_rebuild_core() {
[[ "$quiet" == "false" ]] && echo " Schema validation: PASSED" || true
fi

# Step 4: Remove rogue tables (keep only NFTBan tables)
[[ "$quiet" == "false" ]] && echo " [4/12] Removing rogue tables..."
# v1.48.0: Include SYNPROXY raw tables in allowed list
# v1.51.1: Include emergency install table (postinst lockout prevention)
local ALLOWED_TABLES_PATTERN="^table (ip|ip6) (nftban|raw)$|^table inet (filter|nftban|nftban_install_emergency)$"
# Step 4: Classify and clean ghost tables (PR26.6 / 6A).
# TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001 — replaces prior
# allowlist-sweep that silently deleted operator-retained tables
# such as `inet ssh_safety`. Default policy is WARN-and-preserve
# for OPERATOR_SAFETY; only EXTERNAL_AUTHORITY_GHOST is deleted.
[[ "$quiet" == "false" ]] && echo " [4/12] Classifying nft tables (preserve operator safety)..."
local _classify_lib="${NFTBAN_LIB_DIR:-/usr/lib/nftban}/core/nftban_table_classify.sh"
if [[ -f "$_classify_lib" ]]; then
# shellcheck source=/dev/null
source "$_classify_lib" 2>/dev/null || true
fi

local ALL_TABLES
ALL_TABLES=$(nft list tables 2>/dev/null || true)

while IFS= read -r table_line; do
[[ -z "$table_line" ]] && continue
if ! echo "$table_line" | grep -qE "$ALLOWED_TABLES_PATTERN"; then
local TABLE_SPEC="${table_line#table }"
if nft delete table "$TABLE_SPEC" 2>/dev/null; then
[[ "$quiet" == "false" ]] && echo " Deleted rogue table: $TABLE_SPEC" || true
fi
local TABLE_SPEC="${table_line#table }"
local _class=""
if declare -f nftban_classify_table_line &>/dev/null; then
_class="$(nftban_classify_table_line "$table_line")"
fi
case "$_class" in
"$TC_EXTERNAL_AUTHORITY_GHOST")
if nft delete table "$TABLE_SPEC" 2>/dev/null; then
[[ "$quiet" == "false" ]] && echo " Deleted external-authority ghost table: $TABLE_SPEC" || true
fi
;;
"$TC_OPERATOR_SAFETY")
# PR26.6 invariant — preserve operator-retained tables.
# Emit warning to stderr so it lands in install logs.
echo "WARNING: preserving non-nftban operator table: $TABLE_SPEC (TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001)" >&2
;;
"$TC_NFTBAN_OWNED"|"$TC_KERNEL_DEFAULT")
# nftban-owned: flushed in step 5 below.
# kernel default (ip raw / ip6 raw): preserve silently.
: ;;
*)
# Classifier unavailable (lib missing) — preserve, do
# not silently delete. Better to leave a foreign table
# than to wipe operator state.
echo "WARNING: table classifier unavailable; preserving: $TABLE_SPEC" >&2
;;
esac
done <<< "$ALL_TABLES"

# Step 5: Flush + load (safe — we validated above)
Expand Down
128 changes: 128 additions & 0 deletions cli/lib/nftban/core/nftban_table_classify.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# =============================================================================
# NFTBan v1.100 Amendment 4 - nft Table Classifier (PR26.6 / 6A)
# =============================================================================
# SPDX-License-Identifier: MPL-2.0
# meta:name="nftban_table_classify"
# meta:type="lib"
# meta:version="1.100.0"
# meta:owner="Antonios Voulvoulis <contact@nftban.com>"
# meta:created_date="2026-04-30"
# meta:description="Classify nft tables before destructive cleanup — TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001"
# meta:inventory.files="cli/lib/nftban/core/nftban_table_classify.sh"
# meta:inventory.binaries=""
# meta:inventory.env_vars=""
# meta:inventory.config_files=""
# meta:inventory.systemd_units=""
# meta:inventory.network=""
# meta:inventory.privileges="none"
#
# Invariant TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001:
# During takeover and rebuild, nftban may disable external firewall
# authority through reversible lifecycle operations, but must not
# destructively delete non-nftban-owned authority or operator-safety
# assets.
#
# Replaces the prior allowlist-sweep pattern that silently called
# nft delete table <every-non-allowlisted-table>
# which on dns2 (2026-04-30) wiped operator-retained inet ssh_safety.
#
# Classes:
# NFTBAN_OWNED — flush/delete authorized
# EXTERNAL_AUTHORITY_GHOST — delete authorized (CSF/firewalld iptables-nft compat)
# KERNEL_DEFAULT — preserve silently (ip raw, ip6 raw)
# OPERATOR_SAFETY — preserve, emit warning (default policy in PR26.6)
# =============================================================================

set -Eeuo pipefail

[[ -n "${_NFTBAN_TABLE_CLASSIFY_LOADED:-}" ]] && return 0
_NFTBAN_TABLE_CLASSIFY_LOADED=1

# shellcheck disable=SC2034
readonly TC_NFTBAN_OWNED="NFTBAN_OWNED"
# shellcheck disable=SC2034
readonly TC_EXTERNAL_AUTHORITY_GHOST="EXTERNAL_AUTHORITY_GHOST"
# shellcheck disable=SC2034
readonly TC_KERNEL_DEFAULT="KERNEL_DEFAULT"
# shellcheck disable=SC2034
readonly TC_OPERATOR_SAFETY="OPERATOR_SAFETY"

# nftban_classify_table — classify a single nft table by family + name.
#
# Input (positional):
# $1 family — one of: ip, ip6, inet, arp, bridge, netdev
# $2 name — table name
# Output:
# prints classification token to stdout (one of TC_* values)
# Exit code: always 0
#
# Pinned table sets are kept in this single function so
# install-side, rebuild-side, and autoheal-side all agree.
nftban_classify_table() {
local family="$1"
local name="$2"
local spec="${family} ${name}"

case "$spec" in
# NFTBan-owned tables
"ip nftban"|"ip6 nftban"|"inet nftban"|"inet nftban_install_emergency")
echo "$TC_NFTBAN_OWNED"
return 0
;;
# Kernel default empty tables (always present on EL9+, harmless)
"ip raw"|"ip6 raw")
echo "$TC_KERNEL_DEFAULT"
return 0
;;
# External firewall ghost tables — created by iptables-nft compat
# layer (CSF/lfd, firewalld, fail2ban, docker). Delete authorized
# during takeover-driven rebuild.
"ip filter"|"ip6 filter"|\
"ip nat"|"ip6 nat"|\
"ip mangle"|"ip6 mangle"|\
"ip security"|"ip6 security"|\
"inet firewalld"|"inet filter")
echo "$TC_EXTERNAL_AUTHORITY_GHOST"
return 0
;;
*)
# Anything else — including operator-retained tables such as
# `inet ssh_safety` — is OPERATOR_SAFETY. PR26.6 default policy
# is WARN-and-preserve.
echo "$TC_OPERATOR_SAFETY"
return 0
;;
esac
}

# nftban_classify_table_line — accept "table <family> <name>" line as
# emitted by `nft list tables` and route through nftban_classify_table.
nftban_classify_table_line() {
local line="$1"
# Strip leading "table "
local rest="${line#table }"
# Split on first whitespace
local family="${rest%% *}"
local name="${rest#* }"
if [[ -z "$family" || -z "$name" || "$family" == "$name" ]]; then
echo "$TC_OPERATOR_SAFETY"
return 0
fi
nftban_classify_table "$family" "$name"
}

# nftban_table_should_delete_for_takeover — true (rc 0) if the table
# may be destructively deleted during takeover-driven rebuild cleanup.
#
# NFTBAN_OWNED → false (the rebuild flushes nftban tables itself, does
# not nft-delete them — preserving set state references).
# EXTERNAL_AUTHORITY_GHOST → true.
# KERNEL_DEFAULT, OPERATOR_SAFETY → false.
nftban_table_should_delete_for_takeover() {
local family="$1"
local name="$2"
local class
class="$(nftban_classify_table "$family" "$name")"
[[ "$class" == "$TC_EXTERNAL_AUTHORITY_GHOST" ]]
}
73 changes: 43 additions & 30 deletions cli/lib/nftban/helpers/autoheal.sh
Original file line number Diff line number Diff line change
Expand Up @@ -439,46 +439,59 @@ else
fi

# =============================================================================
# 9. Remove rogue nftables tables (v1.17.6)
# =============================================================================
# iptables-nft, docker, fail2ban can create rogue tables like "ip raw", "ip filter"
# These interfere with NFTBan and cause schema corruption
log_info "Checking for rogue nftables tables..."

# Allowed tables (v1.18.0: ONLY ip/ip6 nftban - NO inet tables!)
# CVE-2025-NFTBAN-001: inet filter at priority 0 bypasses NFTBan protection
ALLOWED_TABLES_PATTERN="^table (ip|ip6) nftban$"
# 9. Classify and clean ghost nftables tables (PR26.6 / 6A)
# =============================================================================
# TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001 — replaces prior
# allowlist-sweep that silently deleted operator-retained tables such
# as `inet ssh_safety`. Default policy is WARN-and-preserve for
# OPERATOR_SAFETY; only EXTERNAL_AUTHORITY_GHOST is deleted.
log_info "Classifying nft tables (preserve operator safety)..."

_NFTBAN_TABLE_CLASSIFY_LIB="${NFTBAN_LIB_DIR:-/usr/lib/nftban}/core/nftban_table_classify.sh"
if [[ -f "$_NFTBAN_TABLE_CLASSIFY_LIB" ]]; then
# shellcheck source=/dev/null
source "$_NFTBAN_TABLE_CLASSIFY_LIB" 2>/dev/null || true
fi

# Get all tables
ALL_TABLES=$(nft list tables 2>/dev/null || true)

# Find and remove rogue tables
ROGUE_COUNT=0
GHOST_COUNT=0
SAFETY_COUNT=0
while IFS= read -r table_line; do
[[ -z "$table_line" ]] && continue

# Check if table is allowed
if ! echo "$table_line" | grep -qE "$ALLOWED_TABLES_PATTERN"; then
ROGUE_COUNT=$((ROGUE_COUNT + 1))
log_warn "Detected rogue table: $table_line"

# Extract table family and name (e.g., "table ip raw" -> "ip raw")
TABLE_SPEC="${table_line#table }"

# Delete the rogue table
# v1.19.20 FIX
if nft delete table "$TABLE_SPEC" 2>/dev/null; then
log_info "✅ Deleted rogue table: $TABLE_SPEC"
else
log_error "Failed to delete rogue table: $TABLE_SPEC"
fi
TABLE_SPEC="${table_line#table }"
_class=""
if declare -f nftban_classify_table_line &>/dev/null; then
_class="$(nftban_classify_table_line "$table_line")"
fi
case "$_class" in
"$TC_EXTERNAL_AUTHORITY_GHOST")
log_warn "Detected external-authority ghost table: $table_line"
if nft delete table "$TABLE_SPEC" 2>/dev/null; then
log_info "✅ Deleted external-authority ghost table: $TABLE_SPEC"
GHOST_COUNT=$((GHOST_COUNT + 1))
else
log_error "Failed to delete ghost table: $TABLE_SPEC"
fi
;;
"$TC_OPERATOR_SAFETY")
# PR26.6 invariant — preserve operator-retained tables.
log_warn "Preserving non-nftban operator table: $TABLE_SPEC (TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001)"
SAFETY_COUNT=$((SAFETY_COUNT + 1))
;;
"$TC_NFTBAN_OWNED"|"$TC_KERNEL_DEFAULT")
: ;;
*)
log_warn "Table classifier unavailable; preserving: $TABLE_SPEC"
;;
esac
done <<< "$ALL_TABLES"

if [[ $ROGUE_COUNT -eq 0 ]]; then
log_info "✅ No rogue tables detected - schema clean"
if [[ $GHOST_COUNT -eq 0 && $SAFETY_COUNT -eq 0 ]]; then
log_info "✅ No ghost tables, no operator-safety tables — schema clean"
else
log_info "Removed $ROGUE_COUNT rogue table(s)"
log_info "Cleaned $GHOST_COUNT ghost table(s); preserved $SAFETY_COUNT operator-safety table(s)"
fi

# =============================================================================
Expand Down
96 changes: 96 additions & 0 deletions cli/lib/nftban/tests/test_table_classifier.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# =============================================================================
# NFTBan PR26.6 / 6C — nft Table Classifier Fixture Test
# =============================================================================
# SPDX-License-Identifier: MPL-2.0
# meta:name="test_table_classifier"
# meta:type="test"
# meta:owner="Antonios Voulvoulis <contact@nftban.com>"
# meta:created_date="2026-04-30"
# meta:description="Locks TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001 — operator-safety tables (e.g. inet ssh_safety) survive rebuild classification"
# meta:inventory.files="cli/lib/nftban/tests/test_table_classifier.sh"
# meta:inventory.binaries=""
# meta:inventory.env_vars=""
# meta:inventory.config_files=""
# meta:inventory.systemd_units=""
# meta:inventory.network=""
# meta:inventory.privileges="none"
# =============================================================================
set -Eeuo pipefail

LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/core/nftban_table_classify.sh"
if [[ ! -f "$LIB" ]]; then
echo "FAIL: classifier lib not found at $LIB" >&2
exit 1
fi
# shellcheck source=/dev/null
source "$LIB"

FAIL=0
expect() {
local got="$1" want="$2" label="$3"
if [[ "$got" != "$want" ]]; then
echo "FAIL: $label — got '$got', want '$want'" >&2
FAIL=1
else
echo "PASS: $label ($got)"
fi
}

# Class 1: NFTBAN_OWNED
expect "$(nftban_classify_table ip nftban)" "$TC_NFTBAN_OWNED" "ip nftban → NFTBAN_OWNED"
expect "$(nftban_classify_table ip6 nftban)" "$TC_NFTBAN_OWNED" "ip6 nftban → NFTBAN_OWNED"
expect "$(nftban_classify_table inet nftban_install_emergency)" "$TC_NFTBAN_OWNED" "inet nftban_install_emergency → NFTBAN_OWNED"

# Class 2: KERNEL_DEFAULT (ip raw / ip6 raw — preserve silently)
expect "$(nftban_classify_table ip raw)" "$TC_KERNEL_DEFAULT" "ip raw → KERNEL_DEFAULT"
expect "$(nftban_classify_table ip6 raw)" "$TC_KERNEL_DEFAULT" "ip6 raw → KERNEL_DEFAULT"

# Class 3: EXTERNAL_AUTHORITY_GHOST (CSF/firewalld iptables-nft compat)
for spec in "ip filter" "ip6 filter" "ip nat" "ip6 nat" "ip mangle" "ip6 mangle" "ip security" "ip6 security" "inet firewalld" "inet filter"; do
fam="${spec%% *}"; nm="${spec#* }"
expect "$(nftban_classify_table "$fam" "$nm")" "$TC_EXTERNAL_AUTHORITY_GHOST" "$spec → EXTERNAL_AUTHORITY_GHOST"
done

# Class 4: OPERATOR_SAFETY (everything else — must include ssh_safety, dns2 evidence)
expect "$(nftban_classify_table inet ssh_safety)" "$TC_OPERATOR_SAFETY" "inet ssh_safety → OPERATOR_SAFETY (PR26.6 root cause)"
expect "$(nftban_classify_table inet test_operator_safety)" "$TC_OPERATOR_SAFETY" "inet test_operator_safety → OPERATOR_SAFETY"
expect "$(nftban_classify_table ip custom_ops)" "$TC_OPERATOR_SAFETY" "ip custom_ops → OPERATOR_SAFETY"
expect "$(nftban_classify_table bridge filter)" "$TC_OPERATOR_SAFETY" "bridge filter → OPERATOR_SAFETY"
expect "$(nftban_classify_table netdev ingress)" "$TC_OPERATOR_SAFETY" "netdev ingress → OPERATOR_SAFETY"

# Line-form classifier (nft list tables output shape)
expect "$(nftban_classify_table_line 'table inet ssh_safety')" "$TC_OPERATOR_SAFETY" "line: table inet ssh_safety → OPERATOR_SAFETY"
expect "$(nftban_classify_table_line 'table ip nftban')" "$TC_NFTBAN_OWNED" "line: table ip nftban → NFTBAN_OWNED"
expect "$(nftban_classify_table_line 'table ip filter')" "$TC_EXTERNAL_AUTHORITY_GHOST" "line: table ip filter → EXTERNAL_AUTHORITY_GHOST"
expect "$(nftban_classify_table_line 'table ip raw')" "$TC_KERNEL_DEFAULT" "line: table ip raw → KERNEL_DEFAULT"

# Takeover delete predicate
if nftban_table_should_delete_for_takeover ip filter; then
echo "PASS: takeover deletes ip filter"
else
echo "FAIL: takeover should delete ip filter (EXTERNAL_AUTHORITY_GHOST)" >&2; FAIL=1
fi
if nftban_table_should_delete_for_takeover inet ssh_safety; then
echo "FAIL: takeover MUST NOT delete inet ssh_safety (OPERATOR_SAFETY)" >&2; FAIL=1
else
echo "PASS: takeover preserves inet ssh_safety (PR26.6 invariant)"
fi
if nftban_table_should_delete_for_takeover ip nftban; then
echo "FAIL: takeover should not nft-delete ip nftban (NFTBAN_OWNED — flushed elsewhere)" >&2; FAIL=1
else
echo "PASS: takeover does not nft-delete ip nftban (flush-only path)"
fi
if nftban_table_should_delete_for_takeover ip raw; then
echo "FAIL: takeover should not delete ip raw (KERNEL_DEFAULT)" >&2; FAIL=1
else
echo "PASS: takeover preserves ip raw (KERNEL_DEFAULT)"
fi

if [[ "$FAIL" -ne 0 ]]; then
echo ""
echo "RESULT: FAIL"
exit 1
fi
echo ""
echo "RESULT: PASS — TAKEOVER-PRESERVES-NON-NFTBAN-AUTHORITY-001 classifier locked"
Loading
Loading