Open what is shut, and never widen what is already open
setup-server queried --query-port, saw "not open", and blanket-opened 80, 443 and 81. On a machine reaching those ports through rich rules -- one CDN's address sets, say -- neither appears in --list-ports, so the step read a deliberate, narrow grant as "closed" and replaced it with a public one. It did this during `panama update`, which asks nothing and runs unattended, so there was no prompt and no diff for anyone to read. A production server lost its firewall posture that way, port 81 and the proxy's admin portal included. Each port is now checked for existing coverage first -- an open port, an enabled service, or an accepting rich rule, ranges included -- and left alone when it has some. The bias stays where it was: only a rule that positively reads as a grant counts, and anything unrecognised still falls through to opening the port, because a fresh server that is silently unreachable is the worse failure. A rule that DROPS a port is not coverage either; reading a block as a grant would leave the proxy unreachable on a machine nobody thought to re-check. A machine that manages its own firewall opts out once, with `echo manual > ~/.local/state/panama/firewall`, and Panama never touches firewalld on it again. A file rather than an environment variable, because the run that needs to respect it is the unattended one. tests/setup/server-firewall-contract pins all of it, and fails against the previous code on exactly the cases that cost the posture. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
This commit is contained in:
1 parent
eb0056ac94
commit
a493fa5584
6 files changed
+268
-7
No files matched your search
@@ -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
|
||||
|
||||
Reference in new issue
Block a user