release(v1.178.0): BotScan read-authority — cap-scoped collector → nf… #634
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================= | |
| # NFTBan — CI: Update Canonization Gate (G3-UPDATE, PR-16 slice) | |
| # ============================================================================= | |
| # SPDX-License-Identifier: MPL-2.0 | |
| # Purpose: Enforce v1.99 update canonization invariants, starting with the | |
| # PR-16 baseline sub-gates: | |
| # G3-U1 update lifecycle entry | |
| # G3-U2 current → target version detection | |
| # G3-U3 --dry-run correctness (no mutation) | |
| # G3-U4 preflight enforcement | |
| # | |
| # Future sub-gates (G3-U5 .. G3-U17 + P-1/P-2/P-3 + G3-U-REBUILD-PARITY) | |
| # are added incrementally by PR-17 .. PR-21 per the gate spec. | |
| # | |
| # Architecture constraint enforced here (INV-U-001): | |
| # Update is a BOUNDED TRIGGER into the rebuild/lifecycle pipeline — | |
| # not a parallel execution path. | |
| # ============================================================================= | |
| name: Update Canonization Gate | |
| on: | |
| pull_request: | |
| branches: [main, master] | |
| paths: | |
| - 'cmd/nftban-installer/**' | |
| - 'internal/installer/**' | |
| - 'internal/lifecycle/**' | |
| - 'cli/lib/nftban/cli/cmd_update*.sh' | |
| - 'VERSION' | |
| - '.github/workflows/ci-update-canonization.yml' | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| concurrency: | |
| group: ci-update-canonization-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| update-canonization: | |
| name: Update Canonization (${{ matrix.label }}) | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - label: ubuntu-24.04 | |
| runner: ubuntu-24.04 | |
| container: '' | |
| - label: almalinux-9 | |
| runner: ubuntu-24.04 | |
| container: almalinux:9 | |
| runs-on: ${{ matrix.runner }} | |
| container: ${{ matrix.container }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 | |
| - name: Install system dependencies (DEB) | |
| if: matrix.label == 'ubuntu-24.04' | |
| run: | | |
| sudo apt-get update -qq | |
| sudo apt-get install -y nftables jq systemd util-linux coreutils strace | |
| - name: Install system dependencies (RPM) | |
| if: matrix.label == 'almalinux-9' | |
| run: | | |
| dnf -y install epel-release | |
| # coreutils-single is preinstalled on almalinux:9 minimal — omit coreutils. | |
| dnf -y install nftables jq systemd util-linux git tar gzip which procps-ng findutils sudo golang strace | |
| - name: Set up Go (DEB only) | |
| if: matrix.label == 'ubuntu-24.04' | |
| uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.6.0 | |
| with: | |
| # v1.157 PR-C: pin exact patch (matches go.mod `go 1.25.11`). | |
| # GOTOOLCHAIN=local intentionally NOT set here: the RPM matrix legs | |
| # build with dnf-installed Go (not this step), whose patch may differ. | |
| go-version: '1.25.11' | |
| # ------------------------------------------------------------------ | |
| # Unit tests — close G3-U2 (version detection) + G3-U4 (preflight) | |
| # at the function level. This is what makes PR-16 merge-safe: unit | |
| # coverage proves the contract before we touch runtime. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U2 + G3-U4 — unit tests for update.{Preflight,DetectVersions,BuildPlan} | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| go test -v ./internal/installer/update/... | |
| # ------------------------------------------------------------------ | |
| # Build the installer binary so we can exercise the full dry-run | |
| # control flow end-to-end. | |
| # ------------------------------------------------------------------ | |
| - name: Build nftban-installer + nftban-validate | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| mkdir -p bin | |
| go build -trimpath -o bin/nftban-installer ./cmd/nftban-installer/ | |
| go build -trimpath -o bin/nftban-validate ./cmd/nftban-validate/ | |
| test -x bin/nftban-installer | |
| test -x bin/nftban-validate | |
| # ------------------------------------------------------------------ | |
| # Stage a minimal VERSION + ip nftban table so preflight has enough | |
| # host state to evaluate. We do NOT install nftban fully — that's | |
| # out of scope for the update-canonization gate. | |
| # ------------------------------------------------------------------ | |
| - name: Prepare host state for dry-run | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| sudo install -d /usr/lib/nftban/bin /var/lib/nftban/state /etc/nftban /etc/ssh | |
| # Current version on disk. | |
| sudo cp VERSION /usr/lib/nftban/VERSION | |
| # Stage the validator binary at its FHS home so runUpdateApply | |
| # can invoke it (fhs.NftbanValidateBin absolute path — NOT on | |
| # default $PATH). This was caught by the G3-U-REBUILD-PARITY | |
| # gate FC-1 on 2026-04-19 and fixed in the Go apply path. | |
| # Staging here future-proofs against any non-dry-run CI step | |
| # that exercises the real validator invocation. | |
| sudo install -m 0755 bin/nftban-validate /usr/lib/nftban/bin/nftban-validate | |
| # Seed a terminal state marker so the stale-in-progress warning | |
| # does not fire in the happy path. | |
| echo "COMMITTED" | sudo tee /var/lib/nftban/state/install_state >/dev/null | |
| # Simulate a host where nftban is already authoritative: | |
| # - Disable UFW (real install took authority long ago) | |
| # - Flush iptables-nft ghost tables (filter/nat/mangle) | |
| # - Create ip nftban so authority preflight P-1 passes | |
| sudo systemctl disable --now ufw 2>/dev/null || true | |
| sudo nft flush ruleset || true | |
| sudo nft add table ip nftban || true | |
| # CI containers do not run sshd, and the Ubuntu runner's default | |
| # sshd_config has Port commented out, so the SSH-port detector | |
| # falls through all 4 sources. Seed the state-file source | |
| # directly — source 3 in the detection chain, which is exactly | |
| # what a post-install upgrade host would have. | |
| echo "22" | sudo tee /var/lib/nftban/state/ssh_port_active.state >/dev/null | |
| # phaseDetect does not require nftband to be running for dry-run, | |
| # only preflight does — and preflight reports the failure as one | |
| # of the five checks rather than aborting phaseDetect. | |
| # ------------------------------------------------------------------ | |
| # G3-U3 — --dry-run correctness: no mutation, plan rendered, valid | |
| # exit. We run the command, capture its output, and assert (a) plan | |
| # header is present, (b) no mutation occurred on the filesystem. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U3 — --dry-run renders plan, no mutation | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| # PR-22B: snapshot ALL protected paths, including the | |
| # update-history.json truth surface. The previous version of | |
| # this gate whitelisted /var/lib/nftban/state/update_plan.json | |
| # and swallowed diff output behind "|| true" — exactly the | |
| # shape the audit flagged as "falsely reassuring CI." | |
| # Seed a known update-history.json FIRST, BEFORE taking the | |
| # snapshot, so both before and after include the file. Without | |
| # the seed, an empty-to-empty diff hides history pollution. | |
| sudo mkdir -p /var/lib/nftban | |
| echo '{"schema_version":"1.0","entries":[]}' | sudo tee /var/lib/nftban/update-history.json >/dev/null | |
| 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) | |
| # PR-P2-4 (G3-EXEC-TRACE): wrap the dry-run under strace so we | |
| # can hard-assert the process did NOT spawn any forbidden | |
| # mutator. Falsifiability at the syscall layer, not just the | |
| # Go-function-call layer. | |
| set +e | |
| sudo bash scripts/ci-exec-trace-assert.sh \ | |
| ./bin/nftban-installer --mode=upgrade --dry-run \ | |
| --source --source-dir="$PWD" \ | |
| --state-dir=/var/lib/nftban/state 2>&1 | tee /tmp/dryrun.out | |
| rc=${PIPESTATUS[0]} | |
| set -e | |
| echo "dry-run exit code: $rc" | |
| # Must always render the plan header (G3-U3 output contract). | |
| grep -q "NFTBan Update — Plan" /tmp/dryrun.out | |
| grep -q "Current version" /tmp/dryrun.out | |
| grep -q "Target version" /tmp/dryrun.out | |
| # INV-U-001 must be visible in the rendered plan (architectural | |
| # constraint verified in the output, not buried in source). | |
| grep -q "INV-U-001" /tmp/dryrun.out | |
| # PR-22B hard assertions — no soft "|| true". Any deviation is a | |
| # contract failure that must fail the gate. Size-only snapshot | |
| # (no mtime) so touch-style no-op reopens don't trigger. | |
| after=$(find /var/lib/nftban /etc/nftban -type f -printf '%p %s\n' 2>/dev/null | sort) | |
| after_hist=$(sudo sha256sum /var/lib/nftban/update-history.json | awk '{print $1}') | |
| after_state=$(sudo test -f /var/lib/nftban/state/install_state && sudo sha256sum /var/lib/nftban/state/install_state | awk '{print $1}' || echo "missing") | |
| if [[ "$before_hist" != "$after_hist" ]]; then | |
| echo "::error::G3-U3 FAIL: update-history.json modified by dry-run (before=$before_hist after=$after_hist) — audit finding F regression" | |
| exit 1 | |
| fi | |
| if [[ "$before_state" != "$after_state" ]]; then | |
| echo "::error::G3-U3 FAIL: install_state modified by dry-run (before=$before_state after=$after_state) — sf.DryRun must suppress Transition persistence" | |
| exit 1 | |
| fi | |
| if [[ "$before" != "$after" ]]; then | |
| echo "::error::G3-U3 FAIL: filesystem under /var/lib/nftban or /etc/nftban changed during dry-run" | |
| diff <(echo "$before") <(echo "$after") || true | |
| exit 1 | |
| fi | |
| # Hard assertion: no file under /etc/nftban/** was modified. | |
| if ! diff -q <(echo "$before" | grep '^/etc/nftban/') \ | |
| <(echo "$after" | grep '^/etc/nftban/') >/dev/null; then | |
| echo "::error::G3-U3 FAIL: /etc/nftban was modified during --dry-run" | |
| 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. | |
| case "$rc" in | |
| 0|1) echo "G3-U3 PASS — exit=$rc (preflight result reflected correctly); history + state + fs all unchanged" ;; | |
| *) echo "::error::G3-U3 FAIL: unexpected exit code $rc"; exit 1 ;; | |
| esac | |
| # ------------------------------------------------------------------ | |
| # G3-U1 — update lifecycle entry: the plan output must identify that | |
| # the update trigger was observed by the lifecycle-capable installer, | |
| # not the legacy shell path. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U1 — update enters installer binary (not shell) | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| # Confirm the installer binary itself printed "mode=upgrade" in | |
| # its run header — that is proof the lifecycle-capable entry | |
| # point was used for the initial control flow. | |
| grep -q "mode=upgrade" /tmp/dryrun.out || { | |
| echo "::error::G3-U1 FAIL: run header did not identify mode=upgrade" | |
| exit 1 | |
| } | |
| echo "G3-U1 PASS — update mode observed by installer binary" | |
| # ------------------------------------------------------------------ | |
| # G3-U2 — current → target version detection: both must be visible | |
| # and non-placeholder for source-install case (sourceDir provided). | |
| # ------------------------------------------------------------------ | |
| - name: G3-U2 — current + target versions detected | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| CURRENT=$(grep 'Current version' /tmp/dryrun.out | sed 's/.*:\s*//' | head -1) | |
| TARGET=$(grep 'Target version' /tmp/dryrun.out | sed 's/.*:\s*//' | head -1) | |
| echo "Detected — current=[$CURRENT] target=[$TARGET]" | |
| if [[ -z "$CURRENT" || "$CURRENT" == "(not detected)" ]]; then | |
| echo "::error::G3-U2 FAIL: current version not detected" | |
| exit 1 | |
| fi | |
| if [[ -z "$TARGET" || "$TARGET" == "(not detected)" ]]; then | |
| echo "::error::G3-U2 FAIL: target version not detected (source install)" | |
| exit 1 | |
| fi | |
| echo "G3-U2 PASS — current=$CURRENT target=$TARGET" | |
| # ------------------------------------------------------------------ | |
| # G3-U4 — preflight enforcement: all seven checks must appear in the | |
| # plan's Preflight block (PR-16 P-1..P-5 + PR-17 P-6..P-7). We don't | |
| # require every check to pass — CI environments don't have nftband | |
| # running — but every check MUST be evaluated and reported. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U4 — preflight reports all 7 checks | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| for name in \ | |
| authority_nftban \ | |
| service_nftband_active \ | |
| artifact_version_file \ | |
| dependency_nft \ | |
| state_no_stale_in_progress \ | |
| rebuild_recovery_available \ | |
| install_origin_coherent; do | |
| grep -q "$name" /tmp/dryrun.out || { | |
| echo "::error::G3-U4 FAIL: preflight check '$name' missing from plan" | |
| exit 1 | |
| } | |
| done | |
| echo "G3-U4 PASS — all 7 preflight checks reported" | |
| # ------------------------------------------------------------------ | |
| # G3-U4-deepen (PR-17) — recovery planning metadata must be rendered | |
| # so apply (PR-18) has explicit input about rollback reachability. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U4-deepen — recovery plan metadata rendered | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| grep -q "Recovery plan" /tmp/dryrun.out || { | |
| echo "::error::G3-U4-deepen FAIL: plan has no Recovery block" | |
| exit 1 | |
| } | |
| grep -q "Mechanism" /tmp/dryrun.out || { | |
| echo "::error::G3-U4-deepen FAIL: Recovery block missing Mechanism" | |
| exit 1 | |
| } | |
| echo "G3-U4-deepen PASS — recovery planning metadata present" | |
| # ------------------------------------------------------------------ | |
| # PR-18 — structural call-path audit. Scans the runUpdateApply source | |
| # for forbidden patterns before any runtime test. Fails fast if | |
| # apply ever gains a direct mutation surface. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U5..U10 — structural call-path audit of update scope | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| # PR-22B: widen audit scope from update_apply.go only to the | |
| # full update reach set. The audit found dry-run writes that | |
| # the narrower scope missed (update_dryrun.go, phaseDetect | |
| # reuse in phases.go). Shared dispatchers in main.go are also | |
| # checked for direct writes, but are allowed to call the | |
| # history writer via the writeHistory helper (guarded by | |
| # IsApplyTerminal). | |
| srcs=( | |
| cmd/nftban-installer/update_apply.go | |
| cmd/nftban-installer/update_dryrun.go | |
| ) | |
| echo "Auditing ${srcs[*]} for forbidden patterns..." | |
| fail=0 | |
| for src in "${srcs[@]}"; do | |
| for pat in \ | |
| 'exec\.Run\("nft"[^)]*add' \ | |
| 'exec\.Run\("nft"[^)]*flush' \ | |
| 'exec\.Run\("nft"[^)]*delete' \ | |
| 'exec\.Run\("nft"[^)]*create' \ | |
| 'exec\.Run\("systemctl"[^)]*stop' \ | |
| 'exec\.Run\("systemctl"[^)]*start' \ | |
| 'exec\.Run\("systemctl"[^)]*restart' \ | |
| 'exec\.Run\("systemctl"[^)]*reload' \ | |
| 'exec\.Run\("systemctl"[^)]*enable' \ | |
| 'exec\.Run\("systemctl"[^)]*disable' \ | |
| 'exec\.Run\("systemctl"[^)]*mask' \ | |
| 'exec\.Run\("systemctl"[^)]*unmask' \ | |
| 'exec\.Run\("ufw"' \ | |
| 'exec\.Run\("iptables"[^-]' \ | |
| 'exec\.Run\("apt-get"[^)]*remove' \ | |
| 'exec\.Run\("apt-get"[^)]*purge' \ | |
| 'exec\.Run\("dnf"[^)]*remove' \ | |
| 'exec\.Run\("dnf"[^)]*erase' \ | |
| 'exec\.WriteFileAtomic\(.*"/etc/nftban/' \ | |
| 'exec\.WriteFileAtomic\(.*"/usr/lib/nftban/' \ | |
| 'os\.WriteFile\(' \ | |
| 'os\.Create\(' \ | |
| 'os\.MkdirAll\(' \ | |
| 'os\.Rename\(' \ | |
| '\.conf\.local'; do | |
| if grep -nE "$pat" "$src" 2>/dev/null; then | |
| echo "::error::G3-U5..U10 FAIL: forbidden pattern '$pat' in $src" | |
| fail=1 | |
| fi | |
| done | |
| done | |
| if (( fail > 0 )); then | |
| echo "::error::Structural audit failed — update scope contains forbidden call pattern" | |
| exit 1 | |
| fi | |
| echo "G3-U5..U10 structural audit PASS" | |
| # ------------------------------------------------------------------ | |
| # PR-18 — unit tests enforce the contract mechanically. | |
| # apply_contract_test.go self-tests the whitelist + audit harness. | |
| # update_apply_test.go pipes every runUpdateApply trace through | |
| # AuditRecordedCommands + AuditWrittenFiles (happy, rebuild-fail, | |
| # validator-fail, no-retry, no-reinterpretation, conf.local | |
| # byte-preservation). | |
| # ------------------------------------------------------------------ | |
| - name: G3-U5..U10 — unit tests for runUpdateApply call-path purity | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| go test -v ./internal/installer/update/... ./cmd/nftban-installer/... | |
| # ------------------------------------------------------------------ | |
| # PR-19 G3-U11 — state↔exit agreement. Structural check: every | |
| # state.Transition(...) call inside runUpdateApply must be followed | |
| # by a return of either that state's own ExitCode() OR the exit | |
| # code variable that drove the state selection (preventing | |
| # hard-coded contradictions like the original StateFailedAbort + | |
| # ExitDegraded split). | |
| # ------------------------------------------------------------------ | |
| - name: G3-U11 — state↔exit agreement structural check | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| src=cmd/nftban-installer/update_apply.go | |
| # Forbidden pattern: any "sf.Transition(state.StateFailed..." | |
| # paired with "return state.ExitDegraded" in the same branch. | |
| # We detect this by looking for returns of literal exit constants | |
| # adjacent to StateFailed transitions. | |
| if grep -B3 -nE 'return state\.(ExitDegraded|ExitCommitted)' "$src" | \ | |
| grep -qE 'sf\.Transition\(state\.StateFailed'; then | |
| echo "::error::G3-U11 FAIL: hard-coded exit literal returned after StateFailed transition — use st.ExitCode() or <phase>.ExitCode" | |
| exit 1 | |
| fi | |
| echo "G3-U11 PASS — no state↔exit contradiction detected structurally" | |
| # ------------------------------------------------------------------ | |
| # PR-19 G3-U12 — history integrity. Unit tests above already cover | |
| # every state → status mapping. This step is an explicit grep to | |
| # catch any future "success" coercion — if someone later hard-codes | |
| # a success status for a non-committed state, CI fails. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U12 — history status-from-state non-coercion | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| src=cmd/nftban-installer/main.go | |
| # Every "return history.StatusSuccess" must be on a line where | |
| # the previous non-empty line is "case state.StateCommitted:". | |
| # This catches future changes that move StatusSuccess to a | |
| # different case arm. | |
| lines=$(grep -n 'history\.StatusSuccess' "$src" | wc -l) | |
| if [[ "$lines" -eq 0 ]]; then | |
| echo "::error::G3-U12 FAIL: no history.StatusSuccess reference found at all" | |
| exit 1 | |
| fi | |
| # For each line N containing StatusSuccess, confirm line N-1 | |
| # contains "case state.StateCommitted:" (after trimming). | |
| fail=0 | |
| while IFS=: read -r lineno _; do | |
| prev=$((lineno - 1)) | |
| prevline=$(sed -n "${prev}p" "$src") | |
| if ! echo "$prevline" | grep -qE 'case\s+state\.StateCommitted'; then | |
| echo "::error::G3-U12 FAIL: history.StatusSuccess at line $lineno not preceded by 'case state.StateCommitted:' — prev line: $prevline" | |
| fail=1 | |
| fi | |
| done < <(grep -n 'history\.StatusSuccess' "$src") | |
| if (( fail > 0 )); then | |
| exit 1 | |
| fi | |
| echo "G3-U12 PASS — every history.StatusSuccess is gated by case state.StateCommitted" | |
| # ------------------------------------------------------------------ | |
| # PR-19 G3-U13 — source/package coherence. Unit test | |
| # TestHistoryInstallType_Source asserts --source → "source". This | |
| # step adds a structural check: historyInstallType's switch MUST | |
| # contain a cfg.source case. Catches accidental regression on the | |
| # "source installs labeled as rpm" bug. | |
| # ------------------------------------------------------------------ | |
| - name: G3-U13 — historyInstallType has source case | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| src=cmd/nftban-installer/main.go | |
| if ! grep -qE 'case cfg\.source' "$src"; then | |
| echo "::error::G3-U13 FAIL: historyInstallType missing 'case cfg.source' — source installs will be mislabeled" | |
| exit 1 | |
| fi | |
| echo "G3-U13 PASS — source case present in historyInstallType" | |
| # ------------------------------------------------------------------ | |
| # G4: PKG-EFFECTIVE-PARITY L5 upgrade transition (Slot 5 / row 14) | |
| # ------------------------------------------------------------------ | |
| # Honest test of v1.106.0 → current upgrade ownership transition. | |
| # v1.106.0 DEB shipped /etc/nftban as root:root 0755 (D-NEW-12 origin); | |
| # current DEB ships it as root:nftban 0750 (post-AUTH-HARDENING / PR #573). | |
| # The upgrade path must converge to the current state regardless of | |
| # pre-upgrade state. If the upgrade does NOT transition ownership, | |
| # this gate reports failure honestly — masking is forbidden. | |
| # | |
| # Scope: ubuntu-24.04 matrix entry only (DEB; full systemd available). | |
| # RPM upgrade transition can be added in a follow-up; v1.106.0 RPM | |
| # already shipped /etc/nftban correctly via %attr() so it has no | |
| # ownership-transition gap to test. | |
| - name: G4 — v1.106.0 → current upgrade transition (DEB only) | |
| if: matrix.label == 'ubuntu-24.04' | |
| shell: bash | |
| run: | | |
| set -Eeuo pipefail | |
| echo "=== G4: PKG-EFFECTIVE-PARITY L5 upgrade transition ===" | |
| # Install build dependencies for inline DEB build | |
| sudo apt-get install -y dpkg-dev fakeroot build-essential file curl | |
| # Build current DEB inline (uses packaging/build_nftban.sh deb path | |
| # — same as build-packages.yml). Binaries are already in bin/ from | |
| # the earlier "Build nftban-installer + nftban-validate" step; we | |
| # need the missing nftban-core + nftband binaries too. | |
| go build -trimpath -o bin/nftban-core ./cmd/nftban-core/ 2>/dev/null || \ | |
| sudo install -m 0755 /bin/true bin/nftban-core | |
| go build -trimpath -o bin/nftband ./cmd/nftband/ 2>/dev/null || \ | |
| sudo install -m 0755 /bin/true bin/nftband | |
| # Build the current DEB | |
| chmod +x packaging/build_nftban.sh | |
| ( cd packaging && ./build_nftban.sh deb ) || { | |
| echo "::warning::G4: current DEB inline build failed — upgrade test cannot run" | |
| echo "::warning::G4: this is acceptable for PR-B introduction; follow-up to extract DEB from build-packages.yml" | |
| exit 0 | |
| } | |
| CURRENT_DEB=$(ls /home/runner/work/*/*/build/packages/*.deb 2>/dev/null | head -1 || ls build/packages/*.deb 2>/dev/null | head -1 || true) | |
| if [[ -z "$CURRENT_DEB" || ! -f "$CURRENT_DEB" ]]; then | |
| echo "::warning::G4: current DEB not found post-build — skipping upgrade test" | |
| exit 0 | |
| fi | |
| echo "G4: current DEB built at $CURRENT_DEB" | |
| # Download v1.106.0 release DEB (Ubuntu 24.04 variant) | |
| V106_URL="https://github.com/itcmsgr/nftban/releases/download/v1.106.0/nftban-ubuntu24.04-amd64.deb" | |
| mkdir -p /tmp/v106 | |
| if ! curl -fsSL -o /tmp/v106/nftban-v1.106.0.deb "$V106_URL"; then | |
| echo "::warning::G4: v1.106.0 release DEB download failed — skipping upgrade test (network/release availability)" | |
| exit 0 | |
| fi | |
| # Install v1.106.0 (best-effort; may fail postinst due to missing | |
| # kernel state but the package payload + ownership land regardless) | |
| sudo apt-get install -y /tmp/v106/nftban-v1.106.0.deb || \ | |
| sudo dpkg -i /tmp/v106/nftban-v1.106.0.deb || \ | |
| echo "::warning::G4: v1.106.0 install reported errors (postinst); proceeding to stat snapshot" | |
| # PRE-UPGRADE: snapshot the v1.106.0-installed state | |
| echo "=== G4 PRE-UPGRADE STATE (v1.106.0 installed) ===" | |
| PRE_MODE=$(stat -c '%a' /etc/nftban 2>/dev/null || echo "MISSING") | |
| PRE_OWNER=$(stat -c '%U:%G' /etc/nftban 2>/dev/null || echo "MISSING") | |
| echo "pre-upgrade /etc/nftban: $PRE_MODE $PRE_OWNER" | |
| # HONEST: log the pre-upgrade state to GITHUB_STEP_SUMMARY for visibility | |
| { | |
| echo "## G4 v1.106.0 → current upgrade transition" | |
| echo "" | |
| echo "**Pre-upgrade** (v1.106.0 installed): \`/etc/nftban\` = $PRE_MODE $PRE_OWNER" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # UPGRADE to current | |
| echo "=== G4 UPGRADE to current ===" | |
| sudo apt-get install -y "$CURRENT_DEB" || \ | |
| sudo dpkg -i "$CURRENT_DEB" || \ | |
| echo "::warning::G4: current install reported errors (postinst); proceeding to stat snapshot" | |
| # POST-UPGRADE: snapshot transitioned state | |
| echo "=== G4 POST-UPGRADE STATE (current installed) ===" | |
| POST_MODE=$(stat -c '%a' /etc/nftban 2>/dev/null || echo "MISSING") | |
| POST_OWNER=$(stat -c '%U:%G' /etc/nftban 2>/dev/null || echo "MISSING") | |
| echo "post-upgrade /etc/nftban: $POST_MODE $POST_OWNER" | |
| { | |
| echo "**Post-upgrade** (current installed): \`/etc/nftban\` = $POST_MODE $POST_OWNER" | |
| echo "" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| # ASSERT: post-upgrade /etc/nftban must be root:nftban 0750 | |
| [[ ${#POST_MODE} -eq 3 ]] && POST_MODE="0$POST_MODE" | |
| if [[ "$POST_MODE" != "0750" || "$POST_OWNER" != "root:nftban" ]]; then | |
| echo "::error::G4 FAIL: v1.106.0 → current upgrade did NOT transition /etc/nftban ownership" | |
| echo "::error:: expected post-upgrade: 0750 root:nftban" | |
| echo "::error:: observed post-upgrade: $POST_MODE $POST_OWNER" | |
| echo "::error:: pre-upgrade was: $PRE_MODE $PRE_OWNER" | |
| echo "::error:: Operator decision needed: add postinst chown loop, OR document" | |
| echo "::error:: v1.106.0 → vN as one-time ownership-transition gap in release notes." | |
| { | |
| echo ":x: **G4 FAILED**: upgrade did NOT transition ownership." | |
| echo "" | |
| echo "Pre-upgrade was \`$PRE_MODE $PRE_OWNER\`; post-upgrade is \`$POST_MODE $POST_OWNER\`." | |
| echo "Expected: \`0750 root:nftban\`." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| echo "G4 PASS: v1.106.0 → current upgrade transitions /etc/nftban → 0750 root:nftban" | |
| { | |
| echo ":white_check_mark: **G4 PASSED**: upgrade transitioned ownership correctly." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| summary: | |
| name: Update Canonization summary | |
| needs: update-canonization | |
| if: always() | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Verdict | |
| run: | | |
| if [[ "${{ needs.update-canonization.result }}" != "success" ]]; then | |
| echo "::error::Update Canonization Gate FAILED — see matrix job output" | |
| exit 1 | |
| fi | |
| echo "Update Canonization Gate PASSED on all matrix targets" |