Compare commits
2
Commits
eb0056ac94
...
a664cf68f5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a664cf68f5 | ||
|
|
a493fa5584 |
No files matched your search
@@ -108,7 +108,7 @@ in order, without stopping again:
|
||||
| `link-vicinae-scripts` | Publishes the Vicinae script commands |
|
||||
| `setup-identity` | git config, `gh auth login`, an SSH key — whichever were asked for |
|
||||
| `install-hardware` | NVIDIA, Secure Boot enrollment, Fedora's extras, firmware — each only if it was asked for. Last, because enrollment and firmware are consumed at the next boot |
|
||||
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81), the `nginx-bridge` network, and the nightly image-update timer |
|
||||
| `setup-server` | Server role only: linger, unprivileged ports from 80, firewalld (80/443/81, opened only where nothing already grants them), the `nginx-bridge` network, and the nightly image-update timer |
|
||||
| `link-server` | Server role only: creates `~/Server` and refreshes the links of whatever services this machine has enabled |
|
||||
|
||||
A server runs the shared stages plus its own two, and skips the rest —
|
||||
@@ -250,7 +250,7 @@ docs/ Settings reference, and the design specs behind the work
|
||||
|
||||
## Tests
|
||||
|
||||
190 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||
192 of them, under `tests/`. `tests/contracts.manifest` classifies every
|
||||
contract by the capabilities it needs. Run the hermetic set, or grant a
|
||||
specific external capability when automation needs it:
|
||||
|
||||
|
||||
+17
-2
@@ -16,8 +16,23 @@ unset PANAMA_ENV
|
||||
# Editor used by CLI
|
||||
export EDITOR="nvim"
|
||||
export SUDO_EDITOR="$EDITOR"
|
||||
export SSH_ASKPASS=/usr/bin/ksshaskpass
|
||||
export SSH_ASKPASS_REQUIRE=prefer
|
||||
# A graphical askpass, and only where there is a session to draw it in.
|
||||
# SSH_ASKPASS_REQUIRE=prefer means ssh and git reach for the dialog BEFORE the
|
||||
# terminal prompt, so on a headless machine the prompt that would have worked
|
||||
# is skipped in favour of one that cannot open: `git push` then fails reading
|
||||
# a username rather than asking for it. The desktop keeps the dialog; a
|
||||
# server, a serial console and a plain SSH session fall back to the terminal.
|
||||
if [ -n "${WAYLAND_DISPLAY:-}${DISPLAY:-}" ] && [ -x /usr/bin/ksshaskpass ]; then
|
||||
export SSH_ASKPASS=/usr/bin/ksshaskpass
|
||||
export SSH_ASKPASS_REQUIRE=prefer
|
||||
else
|
||||
# Cleared, not merely left unset. An askpass inherited from a parent that
|
||||
# did have a display -- an agent's shell, a forwarded environment, a login
|
||||
# that predates this guard -- is exactly as unusable here as one this file
|
||||
# set itself, and declining to add it does nothing about a value already in
|
||||
# the environment.
|
||||
unset SSH_ASKPASS SSH_ASKPASS_REQUIRE
|
||||
fi
|
||||
|
||||
# Define Paths
|
||||
export CARGO_PATH="$HOME/.cargo"
|
||||
|
||||
+7
-1
@@ -40,7 +40,13 @@ is what `panama server enable` refuses to start on.
|
||||
the volume path they were running with until their cutover, because the
|
||||
repo copy must render identically to the live one — renames happen with
|
||||
the service stopped, not in git.
|
||||
- Only 80, 443 and 81 are open (setup-server's doing). Everything else is
|
||||
- Only 80, 443 and 81 are open, and `setup-server` opens each one only where
|
||||
nothing already grants it. A port reachable through an enabled service or an
|
||||
accepting rich rule — a CDN-scoped grant, say — is left exactly as it is,
|
||||
because turning a narrow grant into a public one during an unattended
|
||||
`panama update` is not a decision an installer gets to make. A machine that
|
||||
manages its own firewall opts out for good with
|
||||
`echo manual > ~/.local/state/panama/firewall`. Everything else is
|
||||
reverse-proxied over the `nginx-bridge` network by container name, with no
|
||||
published ports of its own. A service that truly needs another host port
|
||||
says so in a README in its directory, and the port is opened by hand.
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
# is the shell environment plus containers, and almost everything it needs is
|
||||
# already core.
|
||||
#
|
||||
# setup-server opens 80/443/81 through it. Fedora Server ships it; a minimal
|
||||
# cloud image may not, and a server whose firewall step silently no-ops is a
|
||||
# server somebody believes is firewalled.
|
||||
# setup-server opens 80/443/81 through it, where nothing already grants them.
|
||||
# Fedora Server ships it; a minimal cloud image may not, and a server whose
|
||||
# firewall step silently no-ops is a server somebody believes is firewalled.
|
||||
firewalld
|
||||
# Node comes through nvm here for the same reason as on the desktop --
|
||||
# config/bash/shell switches versions per project from .nvmrc -- and because
|
||||
|
||||
@@ -43,11 +43,81 @@ fi
|
||||
# documents that in its own folder and it is opened by hand, because a list of
|
||||
# per-service firewall holes maintained by an installer is a list nobody
|
||||
# audits.
|
||||
if systemctl is-active firewalld >/dev/null 2>&1; then
|
||||
#
|
||||
# "Open" means reachable, not literally listed by --list-ports. A machine can
|
||||
# already reach 80 through a rich rule admitting one CDN's address set, or
|
||||
# through an enabled service. On such a machine `--add-port` is not the no-op
|
||||
# it looks like: it replaces a deliberate, narrow grant with a blanket one,
|
||||
# and does it during a routine `panama update` that asked nobody anything.
|
||||
# So each port is checked for existing coverage and left alone when it has
|
||||
# some. Panama opens what is shut; it does not widen what is already open.
|
||||
#
|
||||
# The bias is deliberate. Only a rule that positively reads as covering the
|
||||
# port counts, and anything unrecognised falls through to opening it, because
|
||||
# a fresh server that silently ends up unreachable is the worse failure --
|
||||
# see the firewalld comment in setup/packages/server-packages. For the same
|
||||
# reason only `accept` rules count: a rich rule that drops a port is not
|
||||
# coverage, it is the opposite, and treating it as coverage would leave the
|
||||
# reverse proxy unreachable on a machine nobody thought to check.
|
||||
#
|
||||
# A machine that manages its own firewall opts out once:
|
||||
#
|
||||
# echo manual > ~/.local/state/panama/firewall
|
||||
#
|
||||
# after which Panama never touches firewalld on it again. A file rather than
|
||||
# an environment variable because `panama update` runs unattended and asks
|
||||
# nothing; a decision that lives only as long as somebody exports a variable
|
||||
# is a decision that quietly lapses on the next run.
|
||||
STATE_DIR="${XDG_STATE_HOME:-$HOME/.local/state}/panama"
|
||||
FIREWALL_DECISION="$STATE_DIR/firewall"
|
||||
|
||||
# Every port an enabled permanent service already grants, one <port>/<proto>
|
||||
# per line. Read back out of firewalld rather than from a hardcoded http=80
|
||||
# table, so a machine's own service definition counts for exactly as much as
|
||||
# a shipped one.
|
||||
service_ports() {
|
||||
local service ports
|
||||
for service in $(sudo firewall-cmd --permanent --list-services 2>/dev/null || true); do
|
||||
ports="$(sudo firewall-cmd --permanent --info-service="$service" 2>/dev/null \
|
||||
| sed -n 's/^[[:space:]]*ports:[[:space:]]*//p')" || true
|
||||
[[ -n "$ports" ]] || continue
|
||||
printf '%s\n' $ports
|
||||
done
|
||||
}
|
||||
|
||||
# Does an accepting rich rule already admit this tcp port? Ranges count, so a
|
||||
# rule granting 8000-8100 covers 8080.
|
||||
rich_rule_covers() {
|
||||
local port="$1" rule value low high
|
||||
while IFS= read -r rule; do
|
||||
[[ "$rule" == *accept* ]] || continue
|
||||
[[ "$rule" == *'protocol="tcp"'* ]] || continue
|
||||
value="${rule#*port port=\"}"
|
||||
[[ "$value" != "$rule" ]] || continue
|
||||
value="${value%%\"*}"
|
||||
if [[ "$value" == *-* ]]; then
|
||||
low="${value%%-*}"; high="${value##*-}"
|
||||
[[ "$low" =~ ^[0-9]+$ && "$high" =~ ^[0-9]+$ ]] || continue
|
||||
(( port >= low && port <= high )) && return 0
|
||||
else
|
||||
[[ "$value" == "$port" ]] && return 0
|
||||
fi
|
||||
done < <(sudo firewall-cmd --permanent --list-rich-rules 2>/dev/null || true)
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ "$(cat "$FIREWALL_DECISION" 2>/dev/null || true)" == manual ]]; then
|
||||
log "Firewall left to this machine ($FIREWALL_DECISION says manual)"
|
||||
elif systemctl is-active firewalld >/dev/null 2>&1; then
|
||||
granted_ports="$(service_ports)"
|
||||
reload_needed=0
|
||||
for port in 80 443 81; do
|
||||
if sudo firewall-cmd --permanent --query-port="${port}/tcp" >/dev/null 2>&1; then
|
||||
log "Port ${port}/tcp already open"
|
||||
elif grep -qxF "${port}/tcp" <<<"$granted_ports"; then
|
||||
log "Port ${port}/tcp already granted by an enabled service; leaving it alone"
|
||||
elif rich_rule_covers "$port"; then
|
||||
log "Port ${port}/tcp already granted by a rich rule; leaving it alone"
|
||||
else
|
||||
log "Opening port ${port}/tcp"
|
||||
sudo firewall-cmd --permanent --add-port="${port}/tcp" >/dev/null
|
||||
|
||||
@@ -261,6 +261,7 @@ hermetic tests/setup/firefox-chrome-contract
|
||||
hermetic tests/setup/hardware-contract
|
||||
# Reads the host hardware predicates after exercising fixture sysfs trees.
|
||||
live-host tests/setup/hardware-predicates-contract
|
||||
hermetic tests/setup/headless-askpass-contract
|
||||
hermetic tests/setup/hooks-contract
|
||||
hermetic tests/setup/interview-contract
|
||||
hermetic tests/setup/launch-or-focus-contract
|
||||
@@ -278,6 +279,7 @@ hermetic tests/setup/role-contract
|
||||
# Root bootstrap uses a temporary filesystem, PATH adapters, real public-key
|
||||
# parsing, and a user namespace for the actual-root fixture guard.
|
||||
hermetic tests/setup/root-server-bootstrap-contract
|
||||
hermetic tests/setup/server-firewall-contract
|
||||
hermetic tests/setup/skills-contract
|
||||
hermetic tests/setup/test-runner-contract
|
||||
hermetic tests/setup/update-command-contract
|
||||
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# The graphical askpass, and the machines that have no graphics.
|
||||
#
|
||||
# config/bash/shell used to export SSH_ASKPASS=ksshaskpass with
|
||||
# SSH_ASKPASS_REQUIRE=prefer unconditionally. `prefer` means ssh and git reach
|
||||
# for that dialog BEFORE the terminal prompt, so on a server the prompt that
|
||||
# would have worked was skipped in favour of a KDE window that cannot open:
|
||||
#
|
||||
# error: /usr/bin/ksshaskpass died of signal 6
|
||||
# fatal: could not read Username for 'https://...'
|
||||
#
|
||||
# A `git push` from a headless box failed to ASK for a username rather than
|
||||
# failing to authenticate -- and the shell environment is the one half of
|
||||
# Panama a server does install, so nothing else was going to catch it.
|
||||
#
|
||||
# Pinned here: headless sessions never prefer a dialog, and a desktop with the
|
||||
# binary present still gets one.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
shell_file="$repo_dir/config/bash/shell"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
mkdir -p "$work/home"
|
||||
|
||||
# Sources the shell environment in a sandbox and reports one variable from it.
|
||||
# HOME is empty so the personal env file is not read, and the session is
|
||||
# non-interactive with no SSH_CONNECTION so the tmux hand-off cannot fire.
|
||||
askpass_state() {
|
||||
env -u DISPLAY -u WAYLAND_DISPLAY -u SSH_CONNECTION -u SSH_ASKPASS \
|
||||
-u SSH_ASKPASS_REQUIRE "$@" \
|
||||
HOME="$work/home" PANAMA_PATH="$repo_dir" \
|
||||
bash -c 'source "$1" >/dev/null 2>&1
|
||||
printf "%s|%s" "${SSH_ASKPASS:-}" "${SSH_ASKPASS_REQUIRE:-}"' \
|
||||
_ "$shell_file"
|
||||
}
|
||||
|
||||
# ── Headless is the case that broke ──────────────────────────────────────────
|
||||
got="$(askpass_state)"
|
||||
[[ "$got" == "|" ]] \
|
||||
|| note "a headless session still prefers a graphical askpass (got '$got')"
|
||||
|
||||
got="$(askpass_state SSH_CONNECTION='10.0.0.1 22 10.0.0.2 22')"
|
||||
[[ "$got" == "|" ]] \
|
||||
|| note "an SSH session still prefers a graphical askpass (got '$got')"
|
||||
|
||||
# The case the first version of this contract missed: declining to SET the
|
||||
# variables does nothing about ones already in the environment, inherited from
|
||||
# a parent that had a display or from a login predating the guard. A fresh
|
||||
# login shell on the repaired server still reported prefer until the headless
|
||||
# branch cleared them outright, so the clearing is what gets pinned.
|
||||
got="$(askpass_state SSH_ASKPASS=/usr/bin/ksshaskpass SSH_ASKPASS_REQUIRE=prefer)"
|
||||
[[ "$got" == "|" ]] \
|
||||
|| note "an inherited askpass survived a headless session (got '$got')"
|
||||
|
||||
# ── A desktop still gets its dialog ──────────────────────────────────────────
|
||||
# Only assertable where the binary exists; saying so beats asserting that a
|
||||
# machine without KDE installed fails to configure KDE's dialog.
|
||||
if [[ -x /usr/bin/ksshaskpass ]]; then
|
||||
got="$(askpass_state WAYLAND_DISPLAY=wayland-0)"
|
||||
[[ "$got" == "/usr/bin/ksshaskpass|prefer" ]] \
|
||||
|| note "a Wayland session does not prefer the graphical askpass (got '$got')"
|
||||
|
||||
got="$(askpass_state DISPLAY=:0)"
|
||||
[[ "$got" == "/usr/bin/ksshaskpass|prefer" ]] \
|
||||
|| note "an X session does not prefer the graphical askpass (got '$got')"
|
||||
else
|
||||
printf 'headless askpass contract: ksshaskpass is not installed, so the desktop half was not verified\n' >&2
|
||||
fi
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'headless askpass contract FAILED:\n' >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'headless askpass contract passed\n'
|
||||
Executable
+184
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# setup-server's firewall step: what it opens, and what it refuses to widen.
|
||||
#
|
||||
# The bug this pins cost a production VPS its firewall posture. That machine
|
||||
# reached 80 and 443 only from one CDN's address sets, through rich rules;
|
||||
# neither port appeared in --list-ports. The old step queried --query-port,
|
||||
# saw "not open", and blanket-opened 80, 443 and 81 -- turning a narrow grant
|
||||
# into a public one, and port 81 into a publicly reachable admin portal. It
|
||||
# did it during `panama update`, which asks nothing and runs unattended, so
|
||||
# there was no prompt and no diff for anyone to read.
|
||||
#
|
||||
# What is pinned here, and why each half matters:
|
||||
#
|
||||
# * A port already granted -- by an open port, an enabled service, or an
|
||||
# accepting rich rule -- is left alone. Panama opens what is shut; it
|
||||
# never widens what is already open.
|
||||
# * A port granted by nothing is still opened. The opposite failure is a
|
||||
# fresh server that is silently unreachable, which is worse: see the
|
||||
# firewalld comment in setup/packages/server-packages.
|
||||
# * A rich rule that DROPS a port is not coverage. Reading a block as a
|
||||
# grant would leave the reverse proxy unreachable on a machine nobody
|
||||
# thought to re-check.
|
||||
# * `echo manual > $XDG_STATE_HOME/panama/firewall` stops Panama touching
|
||||
# firewalld at all, and that opt-out is a file so it survives the next
|
||||
# unattended update.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
stage="$repo_dir/setup/scripts/setup-server"
|
||||
|
||||
findings=()
|
||||
note() { findings+=("$1"); }
|
||||
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# ── Stubs ────────────────────────────────────────────────────────────────────
|
||||
# Everything setup-server reaches for outside the firewall block is answered
|
||||
# "already done", so the only behaviour under test is which ports it decides
|
||||
# to open. firewall-cmd is the one stub that reports: it appends each mutating
|
||||
# call to $FW_LOG, and answers queries from the scenario in the environment.
|
||||
|
||||
bin="$work/bin"
|
||||
mkdir -p "$bin"
|
||||
|
||||
cat >"$bin/firewall-cmd" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--query-port=*)
|
||||
want="${arg#--query-port=}"; want="${want%/tcp}"
|
||||
for open in ${FW_OPEN:-}; do [[ "$open" == "$want" ]] && exit 0; done
|
||||
exit 1 ;;
|
||||
--add-port=*) printf 'add %s\n' "${arg#--add-port=}" >>"$FW_LOG"; exit 0 ;;
|
||||
--reload) printf 'reload\n' >>"$FW_LOG"; exit 0 ;;
|
||||
--list-services) printf '%s\n' "${FW_SERVICES:-}"; exit 0 ;;
|
||||
--info-service=*)
|
||||
name="${arg#--info-service=}"
|
||||
for pair in ${FW_SERVICE_PORTS:-}; do
|
||||
[[ "${pair%%:*}" == "$name" ]] && printf ' ports: %s\n' "${pair#*:}"
|
||||
done
|
||||
exit 0 ;;
|
||||
--list-rich-rules) printf '%s\n' "${FW_RICH:-}"; exit 0 ;;
|
||||
esac
|
||||
done
|
||||
exit 0
|
||||
STUB
|
||||
|
||||
# Forwards firewall-cmd so the stub above sees the call; swallows everything
|
||||
# else, stdin included, so `printf | sudo tee` cannot die of SIGPIPE here.
|
||||
cat >"$bin/sudo" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
case "${1:-}" in
|
||||
firewall-cmd) shift; exec firewall-cmd "$@" ;;
|
||||
*) cat >/dev/null 2>&1; exit 0 ;;
|
||||
esac
|
||||
STUB
|
||||
|
||||
cat >"$bin/systemctl" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
[[ "$*" == "is-active firewalld" ]] && exit "${FW_INACTIVE:-0}"
|
||||
exit 0
|
||||
STUB
|
||||
|
||||
cat >"$bin/loginctl" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
printf 'Linger=yes\n'
|
||||
STUB
|
||||
|
||||
cat >"$bin/podman" <<'STUB'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
STUB
|
||||
|
||||
chmod +x "$bin"/*
|
||||
|
||||
# Runs the stage in a scenario and prints the firewall actions it took, in
|
||||
# order, as a single space-separated line so a whole run compares in one go.
|
||||
run_stage() {
|
||||
local log="$work/fw.log" home="$work/home" state="$work/state"
|
||||
rm -rf "$home" "$state"; mkdir -p "$home" "$state/panama"
|
||||
: >"$log"
|
||||
[[ -n "${DECISION:-}" ]] && printf '%s\n' "$DECISION" >"$state/panama/firewall"
|
||||
env PATH="$bin:$PATH" HOME="$home" XDG_STATE_HOME="$state" \
|
||||
PANAMA_PATH="$repo_dir" FW_LOG="$log" \
|
||||
FW_OPEN="${FW_OPEN:-}" FW_SERVICES="${FW_SERVICES:-}" \
|
||||
FW_SERVICE_PORTS="${FW_SERVICE_PORTS:-}" FW_RICH="${FW_RICH:-}" \
|
||||
FW_INACTIVE="${FW_INACTIVE:-0}" \
|
||||
bash "$stage" </dev/null >"$work/out" 2>&1
|
||||
tr '\n' ' ' <"$log" | sed 's/ *$//'
|
||||
}
|
||||
|
||||
# Each scenario is one call: clear the environment it reads, set what it needs.
|
||||
scenario() { unset FW_OPEN FW_SERVICES FW_SERVICE_PORTS FW_RICH FW_INACTIVE DECISION; }
|
||||
|
||||
cloudflare_rules='rule family="ipv4" source ipset="cloudflare4" port port="80" protocol="tcp" accept
|
||||
rule family="ipv4" source ipset="cloudflare4" port port="443" protocol="tcp" accept'
|
||||
|
||||
# ── A machine with nothing granted gets all three ────────────────────────────
|
||||
scenario
|
||||
got="$(run_stage)"
|
||||
[[ "$got" == "add 80/tcp add 443/tcp add 81/tcp reload" ]] \
|
||||
|| note "a bare machine did not get all three ports opened; got: $got"
|
||||
|
||||
# ── Rich rules granting 80/443 are not widened ───────────────────────────────
|
||||
scenario; FW_RICH="$cloudflare_rules"
|
||||
got="$(run_stage)"
|
||||
[[ "$got" == "add 81/tcp reload" ]] \
|
||||
|| note "a CDN-only rich rule on 80/443 was widened to a public port; got: $got"
|
||||
|
||||
# ── An enabled service granting 80/443 is not widened ────────────────────────
|
||||
scenario; FW_SERVICES="http https"; FW_SERVICE_PORTS="http:80/tcp https:443/tcp"
|
||||
got="$(run_stage)"
|
||||
[[ "$got" == "add 81/tcp reload" ]] \
|
||||
|| note "ports granted by an enabled service were opened again; got: $got"
|
||||
|
||||
# ── Already-open ports are left alone, and nothing reloads ───────────────────
|
||||
scenario; FW_OPEN="80 443 81"
|
||||
got="$(run_stage)"
|
||||
[[ -z "$got" ]] \
|
||||
|| note "a fully configured firewall was touched anyway; got: $got"
|
||||
|
||||
# ── A drop rule is not a grant ───────────────────────────────────────────────
|
||||
scenario; FW_RICH='rule family="ipv4" port port="80" protocol="tcp" drop'
|
||||
got="$(run_stage)"
|
||||
[[ "$got" == "add 80/tcp add 443/tcp add 81/tcp reload" ]] \
|
||||
|| note "a rich rule DROPPING 80 was read as granting it; got: $got"
|
||||
|
||||
# ── A port range grants every port inside it ─────────────────────────────────
|
||||
scenario; FW_RICH='rule family="ipv4" port port="79-82" protocol="tcp" accept'
|
||||
got="$(run_stage)"
|
||||
[[ "$got" == "add 443/tcp reload" ]] \
|
||||
|| note "a rich rule granting 79-82 did not cover 80 and 81; got: $got"
|
||||
|
||||
# ── A udp grant is not a tcp grant ───────────────────────────────────────────
|
||||
scenario; FW_RICH='rule family="ipv4" port port="80" protocol="udp" accept'
|
||||
got="$(run_stage)"
|
||||
[[ "$got" == "add 80/tcp add 443/tcp add 81/tcp reload" ]] \
|
||||
|| note "a udp rich rule was read as granting tcp 80; got: $got"
|
||||
|
||||
# ── The opt-out ──────────────────────────────────────────────────────────────
|
||||
scenario; DECISION=manual
|
||||
got="$(run_stage)"
|
||||
[[ -z "$got" ]] \
|
||||
|| note "the manual opt-out did not stop Panama touching firewalld; got: $got"
|
||||
|
||||
grep -q 'Firewall left to this machine' "$work/out" \
|
||||
|| note 'the manual opt-out does not say why it skipped the firewall'
|
||||
|
||||
# ── An inactive firewalld is not an error ────────────────────────────────────
|
||||
scenario; FW_INACTIVE=1
|
||||
got="$(run_stage)"
|
||||
[[ -z "$got" ]] \
|
||||
|| note "firewalld reported inactive but ports were still touched; got: $got"
|
||||
|
||||
# ── Report ───────────────────────────────────────────────────────────────────
|
||||
if (( ${#findings[@]} > 0 )); then
|
||||
printf 'setup-server firewall contract FAILED:\n' >&2
|
||||
printf ' - %s\n' "${findings[@]}" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf 'setup-server firewall contract passed\n'
|
||||
Reference in new issue
Block a user