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
17 changes: 16 additions & 1 deletion .github/workflows/ci-install-canonization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,11 @@ jobs:
echo '{"schema_version":"1.0","entries":[]}' | sudo tee /var/lib/nftban/update-history.json >/dev/null
before_hist=$(sudo sha256sum /var/lib/nftban/update-history.json | awk '{print $1}')

# PR-P2-3 (G3-KS-SNAPSHOT): capture kernel nft tables + firewall-
# adjacent service states BEFORE the refuse-dry-run invocation
# so we can hard-assert they are byte-identical afterward.
before_ks=$(bash scripts/ci-snapshot-kernel-service.sh)

set +e
out=$(sudo ./bin/nftban-installer --mode=install --dry-run \
--state-dir=/var/lib/nftban/state 2>&1)
Expand All @@ -129,7 +134,17 @@ jobs:
echo "::error::G3-IN-REFUSE-DRY-RUN FAIL: history file changed on a refused run"
exit 1
fi
echo "G3-IN-REFUSE-DRY-RUN PASS — install dry-run refused cleanly, no pollution"

# PR-P2-3 (G3-KS-SNAPSHOT): kernel + service state must be
# byte-identical after a refused run. Hard assertion — no
# `|| true` on the diff check.
after_ks=$(bash scripts/ci-snapshot-kernel-service.sh)
if [[ "$before_ks" != "$after_ks" ]]; then
echo "::error::G3-KS-SNAPSHOT FAIL: kernel or service state changed during install --dry-run refusal"
diff <(echo "$before_ks") <(echo "$after_ks") || true
exit 1
fi
echo "G3-IN-REFUSE-DRY-RUN PASS — install dry-run refused cleanly, no pollution, kernel+service unchanged"

# ------------------------------------------------------------------
# G3-IN-FLAG-COMBOS — reject operator-error flag combinations.
Expand Down
16 changes: 15 additions & 1 deletion .github/workflows/ci-uninstall-canonization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,13 @@ jobs:
# Stop Condition violation.
before_varlib=$(find /var/lib/nftban -type f 2>/dev/null | sort | xargs -r sha256sum 2>/dev/null | sort || true)
before_etcnftban=$(find /etc/nftban -type f 2>/dev/null | sort | xargs -r sha256sum 2>/dev/null | sort || true)
# PR-P2-3 (G3-KS-SNAPSHOT): capture kernel nft tables +
# firewall-adjacent service states BEFORE the dry-run so we
# can hard-assert they remain byte-identical afterward.
# Filesystem snapshot alone (above) misses a regression that
# mutates kernel tables or service states without touching
# tracked files — exactly the class of drift this gate closes.
before_ks=$(bash scripts/ci-snapshot-kernel-service.sh)
set +e
sudo ./bin/nftban-installer --mode=uninstall --dry-run \
--state-dir=/var/lib/nftban/state 2>&1 | tee /tmp/uninstall-dryrun.out
Expand All @@ -219,6 +226,7 @@ jobs:
fi
after_varlib=$(find /var/lib/nftban -type f 2>/dev/null | sort | xargs -r sha256sum 2>/dev/null | sort || true)
after_etcnftban=$(find /etc/nftban -type f 2>/dev/null | sort | xargs -r sha256sum 2>/dev/null | sort || true)
after_ks=$(bash scripts/ci-snapshot-kernel-service.sh)
if [[ "$before_varlib" != "$after_varlib" ]]; then
echo "::error::PR-22 Stop Condition violated — uninstall dry-run modified /var/lib/nftban/"
diff <(echo "$before_varlib") <(echo "$after_varlib") || true
Expand All @@ -229,7 +237,13 @@ jobs:
diff <(echo "$before_etcnftban") <(echo "$after_etcnftban") || true
exit 1
fi
echo "G3-UN-NO-WRITES PASS — /var/lib/nftban and /etc/nftban unchanged after dry-run"
# PR-P2-3 hard assertion — no `|| true` on the diff check.
if [[ "$before_ks" != "$after_ks" ]]; then
echo "::error::G3-KS-SNAPSHOT FAIL: kernel or service state changed during uninstall --dry-run"
diff <(echo "$before_ks") <(echo "$after_ks") || true
exit 1
fi
echo "G3-UN-NO-WRITES PASS — /var/lib/nftban, /etc/nftban, kernel, and service state all unchanged after dry-run"
# Mandatory contract-language elements — each one corresponds
# to a row PR-22 promised to render.
for needle in \
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/ci-update-canonization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,10 @@ jobs:
before=$(find /var/lib/nftban /etc/nftban -type f -printf '%p %s\n' 2>/dev/null | sort)
before_hist=$(sudo sha256sum /var/lib/nftban/update-history.json | awk '{print $1}')
before_state=$(sudo test -f /var/lib/nftban/state/install_state && sudo sha256sum /var/lib/nftban/state/install_state | awk '{print $1}' || echo "missing")
# PR-P2-3 (G3-KS-SNAPSHOT): capture kernel nft tables + firewall-
# adjacent service states for a hard before/after assertion
# around the update dry-run.
before_ks=$(bash scripts/ci-snapshot-kernel-service.sh)

set +e
sudo ./bin/nftban-installer --mode=upgrade --dry-run \
Expand Down Expand Up @@ -212,6 +216,18 @@ jobs:
exit 1
fi

# PR-P2-3 (G3-KS-SNAPSHOT): kernel + service state must be
# byte-identical. The pre-PR-P2-3 gate snapshot covered only
# filesystem truth; a future regression that mutates nftables
# tables or service states without touching tracked files
# would have slipped past.
after_ks=$(bash scripts/ci-snapshot-kernel-service.sh)
if [[ "$before_ks" != "$after_ks" ]]; then
echo "::error::G3-KS-SNAPSHOT FAIL: kernel or service state changed during update --dry-run"
diff <(echo "$before_ks") <(echo "$after_ks") || true
exit 1
fi

# Exit code sanity: 0 (committed) if preflight passes, 1 (degraded)
# if preflight fails. Never 2/3/4 for a well-formed run on a
# host that doesn't have a real daemon.
Expand Down
2 changes: 1 addition & 1 deletion internal/installer/uninstall/contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -281,12 +281,12 @@ discipline.
| # | PR | Merge commit | Purpose |
|---|---|---|---|
| 1 | Prior-authority record hardening | PR #484 / `3b834033` | Added `recorded_at`, `installer_version`, explicit `active_at_install=false` handling to `prior.go`; 5-state classification |
| 2 | External-firewall detection unification | PR #486 / `49d98fc1` | `internal/installer/extfw` canonical detector; Option A CSF config-file signal shared across install/update/uninstall; multi-active → `Ambiguous` (no silent collapse); cross-caller consistency test locked as regression guard |

### Behavioral / semantic blockers (code contract changes)

| # | PR | Scope | Blocking because |
|---|---|---|---|
| 2 | External-firewall detection unification | One shared `DetectExternalAuthority` function + one precedence order (ufw → firewalld → iptables → csf) used by install-side `authority/classify.go`, uninstall-side `uninstall/authority.go`, and any future consumer | Detection drift between modules will cause install/uninstall/restore to disagree about what external authority exists |
| 6 | Payload integrity minimum checks | Minimum-size + required-header/token check for `/etc/nftban/nftban.conf` and `/etc/nftban/nftables.conf`; wire into existing `payload.VerifyInventory` | Presence-only validation lets a truncated-or-empty critical config pass |

### Assurance / gate blockers (CI and scope-lock enforcement)
Expand Down
80 changes: 80 additions & 0 deletions scripts/ci-snapshot-kernel-service.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# =============================================================================
# NFTBan v1.100 PR-P2-3 — CI kernel/service snapshot helper
# =============================================================================
# SPDX-License-Identifier: MPL-2.0
# meta:name="ci-snapshot-kernel-service"
# meta:type="script"
# meta:version="1.100.0"
# meta:owner="Antonios Voulvoulis <contact@nftban.com>"
# meta:created_date="2026-04-20"
# meta:description="Emit stable before/after snapshot of nft tables + firewall-adjacent service states"
# meta:inventory.files="scripts/ci-snapshot-kernel-service.sh"
# meta:inventory.binaries=""
# meta:inventory.env_vars=""
# meta:inventory.config_files=""
# meta:inventory.systemd_units="nftband.service, ufw.service, firewalld.service, csf.service, lfd.service, iptables.service"
# meta:inventory.network=""
# meta:inventory.privileges="root"
# =============================================================================
#
# Prints a deterministic, line-oriented snapshot of:
#
# 1. Kernel nftables tables (`nft list tables`, sorted)
# 2. Firewall-adjacent systemd unit states (nftband + every external
# firewall unit the lifecycle may interact with)
#
# Used by CI gates to assert that dry-run paths leave kernel and
# service state unchanged. The caller captures the output twice
# (before + after the dry-run) and fails CI if the two snapshots
# differ.
#
# Degrades gracefully on container environments that lack nft or
# systemctl — both sides of the comparison emit the same placeholder,
# so diff remains empty for environments that cannot probe.
#
# Contract (PR-P2-3, frozen 2026-04-20):
# - Output is stable (sorted) and purely from read-only probes.
# - Never invokes nft / systemctl with mutation verbs.
# - Never writes to the filesystem.
# - Exit code 0 always; the CALLER decides whether differences fail.
#
# =============================================================================
set -Eeuo pipefail

# PR-P2-3 monitored-units: every unit that is either owned by nftban or
# represents an external firewall the lifecycle code touches. Kept in
# lockstep with internal/installer/extfw/detect.go so the CI gate and
# the production detector agree on "what counts as a firewall service."
UNITS=(
nftband.service
ufw.service
firewalld.service
csf.service
lfd.service
iptables.service
)

echo "## kernel-nft-tables"
if command -v nft >/dev/null 2>&1; then
# Redirect stderr so a missing kernel module doesn't pollute the
# snapshot with different messages across before/after invocations.
sudo nft list tables 2>/dev/null | sort || echo "nft:exec_failed"
else
echo "nft:not_installed"
fi

echo "## service-states"
if command -v systemctl >/dev/null 2>&1 && systemctl --version >/dev/null 2>&1; then
for u in "${UNITS[@]}"; do
# Always emit "unit=state" for every monitored unit — even
# inactive/missing — so both sides of the before/after diff
# produce the same lines unless state actually changes.
# `is-active` exits non-zero for inactive; we capture the
# string and swallow the exit code intentionally.
state=$(systemctl is-active "$u" 2>&1 || true)
echo "$u=$state"
done
else
echo "systemctl:not_available"
fi
Loading