mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 14:07:49 +01:00
feat: gate on nftables drift so the docker cut-off cannot come back silently
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
Pre-commit checks / pre-commit (push) Waiting to run
Shell tests / shell-tests (push) Waiting to run
Shell tests / arch-tests (push) Waiting to run
The forward-chain fix from 334c0cd9 only ever reached the live ruleset by
hand; /etc/nftables.conf kept the bare default-drop chain, so the next
reboot cut every bridged container off again and the Signal bot went deaf
a second time. Nothing compared generated against persisted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
2713ae9d66
commit
89a44b58b2
@ -0,0 +1,40 @@
|
||||
{
|
||||
"intent": "Make the Docker cut-off caused by a stale /etc/nftables.conf detectable instead of silent. The 2026-08-29 fix landed in wg_firewall.sh but was only ever hand-applied to the live ruleset; /etc/nftables.conf kept the bare default-drop forward chain, so the 2026-08-30 reboot reloaded it and cut every bridged container off again. Nothing compared the generated ruleset with the persisted one, so the regression was invisible until the Signal bot went deaf for a second time.",
|
||||
"scope": [
|
||||
"linux_configuration/features/lib/wg_firewall.sh (split render_nftables_ruleset out of write_nftables_ruleset; add verify_nft)",
|
||||
"linux_configuration/features/setup_wireguard_ssh.sh (new 'verify' subcommand, added to the require_root list)",
|
||||
"linux_configuration/features/lib/wg_sshd.sh (usage text only)",
|
||||
"Non-goal: no change to the generated ruleset itself — the forward-chain fix from 334c0cd9 is unchanged",
|
||||
"Non-goal: no change to the input chain, WireGuard, sshd hardening or DuckDNS"
|
||||
],
|
||||
"changes": [
|
||||
"render_nftables_ruleset(target) is split out of write_nftables_ruleset so the ruleset can be regenerated into a scratch file with no backup and no side effects; write_nftables_ruleset keeps the backup and delegates to it",
|
||||
"verify_nft() gates on two invariants and exits non-zero: (1) /etc/nftables.conf is byte-identical to what the script generates, printing a unified diff when it is not, and (2) the LIVE 'inet filter forward' chain still contains 'iifname \"docker0\" accept'. It names the exact re-apply command on failure",
|
||||
"'verify' wired into main() and into usage; it needs root because 'nft list chain' does"
|
||||
],
|
||||
"verification": [
|
||||
{
|
||||
"command": "sudo setup_wireguard_ssh.sh verify (on the live host, before re-applying)",
|
||||
"result": "pass",
|
||||
"evidence": "Exits non-zero and reports both failures on the currently-broken host: '/etc/nftables.conf has DRIFTED from this script' with a diff showing exactly the missing forward-chain block (ct established,related + docker0/br-* accepts), and 'live forward chain does NOT accept docker0'. This is the regression reproducing, which is what the gate is for."
|
||||
},
|
||||
{
|
||||
"command": "diff of rendered output vs /etc/nftables.conf",
|
||||
"result": "pass",
|
||||
"evidence": "The only difference is the intended forward-chain block; the input and output chains, DHCP/DNS/web rules and LAN subnet render byte-identical. Confirms the render/write refactor did not change the generated ruleset."
|
||||
},
|
||||
{
|
||||
"command": "shellcheck -x --severity=style linux_configuration/features/setup_wireguard_ssh.sh",
|
||||
"result": "pass",
|
||||
"evidence": "exit 0, no findings (follows the sourced libs)"
|
||||
}
|
||||
],
|
||||
"risks": [
|
||||
"verify_nft needs root for 'nft list chain'; run without sudo it reports the live-chain check as failed rather than as unknown",
|
||||
"The drift check is byte-exact, so an intentional hand-edit of /etc/nftables.conf will be reported as drift — which is the intended behaviour, but means the fix is always 're-run allow-web', never 'edit the file'"
|
||||
],
|
||||
"rollback": [
|
||||
"git revert this commit; write_nftables_ruleset keeps working because render_nftables_ruleset is only a refactor of its body",
|
||||
"After rollback, re-run 'sudo setup_wireguard_ssh.sh allow-web' if /etc/nftables.conf is still stale, and confirm with: docker run --rm --network bridge redis:8-alpine sh -c 'nc -w4 -z 1.1.1.1 443 && echo OK'"
|
||||
]
|
||||
}
|
||||
@ -5,12 +5,12 @@
|
||||
# 250-line cap. Sourced rather than run, so it inherits the caller's
|
||||
# strict mode and the variables defined above the source line.
|
||||
|
||||
write_nftables_ruleset() {
|
||||
# Rendering is separate from writing so that 'verify' can regenerate the
|
||||
# ruleset into a scratch file and diff it against what is actually on disk,
|
||||
# without backing anything up or touching the live configuration.
|
||||
render_nftables_ruleset() {
|
||||
local target="$1"
|
||||
detect_lan_subnet
|
||||
if [[ -f $NFT_CONF ]]; then
|
||||
cp "$NFT_CONF" "${NFT_CONF}.bak.$(date +%s)"
|
||||
log_warn "Backed up existing ${NFT_CONF} before overwriting."
|
||||
fi
|
||||
local web_rule=""
|
||||
if [[ $ALLOW_WEB == "true" ]]; then
|
||||
web_rule=$'\n\t\ttcp dport { 80, 443 } accept'
|
||||
@ -28,7 +28,7 @@ write_nftables_ruleset() {
|
||||
# saddr. Link-local only: :67 broadcasts never route in from the internet.
|
||||
dhcp_rule=$'\n\t\tudp sport 68 udp dport 67 accept'
|
||||
fi
|
||||
cat >"${NFT_CONF}.new" <<EOF
|
||||
cat >"$target" <<EOF
|
||||
#!/usr/sbin/nft -f
|
||||
flush ruleset
|
||||
|
||||
@ -71,6 +71,48 @@ table inet filter {
|
||||
EOF
|
||||
}
|
||||
|
||||
write_nftables_ruleset() {
|
||||
if [[ -f $NFT_CONF ]]; then
|
||||
cp "$NFT_CONF" "${NFT_CONF}.bak.$(date +%s)"
|
||||
log_warn "Backed up existing ${NFT_CONF} before overwriting."
|
||||
fi
|
||||
render_nftables_ruleset "${NFT_CONF}.new"
|
||||
}
|
||||
|
||||
# The one invariant that has now broken twice: the forward chain must let
|
||||
# Docker's own chains decide. Both times the repo carried the rule and
|
||||
# /etc/nftables.conf did not, so a reboot reloaded the stale file and cut
|
||||
# every bridged container off the internet while the host stayed online.
|
||||
# Nothing compared the two, so nothing noticed.
|
||||
readonly DOCKER_FORWARD_RULE='iifname "docker0" accept'
|
||||
|
||||
verify_nft() {
|
||||
local rendered status=0
|
||||
rendered="$(mktemp)"
|
||||
render_nftables_ruleset "$rendered"
|
||||
|
||||
if diff -q "$rendered" "$NFT_CONF" >/dev/null 2>&1; then
|
||||
log_ok "${NFT_CONF} matches what this script generates."
|
||||
else
|
||||
log_error "${NFT_CONF} has DRIFTED from this script. A reboot would apply the stale file."
|
||||
diff -u "$NFT_CONF" "$rendered" | head -40 || true
|
||||
status=1
|
||||
fi
|
||||
rm -f "$rendered"
|
||||
|
||||
if nft list chain inet filter forward 2>/dev/null | grep -qF "$DOCKER_FORWARD_RULE"; then
|
||||
log_ok "live forward chain lets Docker through."
|
||||
else
|
||||
log_error "live forward chain does NOT accept docker0 -- bridged containers have no network."
|
||||
status=1
|
||||
fi
|
||||
|
||||
if ((status != 0)); then
|
||||
log_error "Re-apply with: sudo $0 allow-web"
|
||||
fi
|
||||
return "$status"
|
||||
}
|
||||
|
||||
# 'flush ruleset' above deletes Docker's own tables along with everything
|
||||
# else, and dockerd only rebuilds them on its next network event -- so
|
||||
# without this, containers keep running with no NAT and no forwarding until
|
||||
|
||||
@ -227,6 +227,8 @@ Commands:
|
||||
add-peer <name> Provision a new phone/laptop and print its QR code.
|
||||
allow-web Open tcp/80 and tcp/443 in the firewall (for a web server on this host).
|
||||
allow-dns Open udp/tcp 53 from the LAN in the firewall (for the DNS blocker on this host).
|
||||
verify Check /etc/nftables.conf matches this script and that
|
||||
the live forward chain still lets Docker through.
|
||||
status Show WireGuard/firewall/sshd status.
|
||||
revoke <name> Remove a peer.
|
||||
help Show this message.
|
||||
|
||||
@ -82,7 +82,6 @@ source "$SCRIPT_DIR/lib/wg_firewall.sh"
|
||||
# shellcheck source=lib/wg_sshd.sh
|
||||
source "$SCRIPT_DIR/lib/wg_sshd.sh"
|
||||
|
||||
|
||||
main() {
|
||||
local cmd="${1:-help}"
|
||||
|
||||
@ -90,7 +89,7 @@ main() {
|
||||
# off -- exec sudo "$0" "$@" inside require_root must re-launch with the
|
||||
# subcommand still present, or sudo would silently run with no args.
|
||||
case "$cmd" in
|
||||
setup | add-peer | revoke | allow-web | allow-dns)
|
||||
setup | add-peer | revoke | allow-web | allow-dns | verify)
|
||||
require_root "$@"
|
||||
;;
|
||||
esac
|
||||
@ -120,6 +119,9 @@ main() {
|
||||
allow-dns)
|
||||
allow_dns
|
||||
;;
|
||||
verify)
|
||||
verify_nft
|
||||
;;
|
||||
status)
|
||||
status_cmd
|
||||
;;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user