mirror of
https://github.com/jhd3197/ServerKit.git
synced 2026-10-30 08:36:55 +09:00
Critical install/update/uninstall hotfixes from the hardening plan (docs/plans/INSTALLER_UPDATER_UNINSTALLER_HARDENING_PLAN.md §4): - Firewall automation (G1): new sourceable scripts/lib/firewall.sh detects firewalld/ufw/nftables/iptables and opens 80/443; install.sh runs configure_firewall, update.sh re-ensures it idempotently. Exactly what we open is recorded in /etc/serverkit/install-state.json (scripts/lib/state.sh) so uninstall undoes only our rules. Fixes the #1 "installed but unreachable" failure on fresh RHEL-family boxes. - Accept Python 3.11 OR 3.12 (G2): locate_python now iterates python3.12/3.11/3 and provision_python installs the distro package (python3.11 on Debian 12, 3.12 on Ubuntu/Fedora, 3.11/3.12 on RHEL) before ever falling back to a source compile. - Use $PYTHON_BIN for secret generation instead of a bare python3. - Distro-first Node 20 (replacing unconditional NodeSource): install the distro nodejs+npm, fall back to NodeSource only when it's missing or < 18, and validate node>=18 before continuing. - Unify uninstall (G4): new scripts/lib/uninstall.sh is the single canonical teardown routine shared by uninstall.sh and `serverkit uninstall` (they had drifted — one ran `down -v`, one didn't). Default preserves user data (volumes kept, DB snapshotted to backups); --purge/--delete-volumes removes volumes+DB+data dirs; --keep-data preserves /etc + /var/lib + backups; --yes/--dry-run added; honors SERVERKIT_DIR. Removes the apt lock-wait drop-in on uninstall. Tests: new scripts/test/test_install.sh (11 cases, source-able install.sh) covers python/node detection, firewall dry-run + state, and the uninstall data-handling matrix. Existing test_update.sh still green. shellcheck clean (error + warning) on all new/changed shell. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
140 lines
5.4 KiB
Bash
140 lines
5.4 KiB
Bash
# shellcheck shell=bash
|
|
#
|
|
# scripts/lib/firewall.sh — sourceable firewall abstraction.
|
|
#
|
|
# Detects the active host firewall and opens/closes the ports ServerKit needs
|
|
# (80/443, optionally 5000). Fresh Rocky/Alma/RHEL/Fedora boxes ship firewalld
|
|
# active by default, which silently blocks 80/443 — making a perfectly good
|
|
# install look "broken" because the panel is unreachable. This helper fixes
|
|
# that automatically and, crucially, is reversible: callers persist exactly
|
|
# what was opened (see scripts/lib/state.sh) so uninstall can undo only those
|
|
# rules and never touch a user's own firewall config.
|
|
#
|
|
# Contract:
|
|
# firewall_detect → echoes: firewalld|ufw|nftables|iptables|none
|
|
# firewall_open <backend> <port...> → open ports (each "80/tcp")
|
|
# firewall_close <backend> <port...> → close the same ports
|
|
#
|
|
# Honors:
|
|
# FW_DRY_RUN=1 print intended commands instead of running them
|
|
# FIREWALL_BACKEND=... force the detected backend (used by unit tests)
|
|
#
|
|
# Everything is best-effort: a firewall we can't drive cleanly degrades to a
|
|
# warning rather than aborting the install.
|
|
|
|
# Run a privileged firewall command, or just print it under dry-run. Kept tiny
|
|
# so the dry-run output is exactly the command that would otherwise execute —
|
|
# which is what the installer unit tests assert against.
|
|
_fw_run() {
|
|
if [ "${FW_DRY_RUN:-0}" = "1" ]; then
|
|
printf ' [firewall] would run: %s\n' "$*"
|
|
return 0
|
|
fi
|
|
# Discard the tool's own chatter ("success", rule-exists noise) here so
|
|
# callers don't redirect — that redirection would also swallow the dry-run
|
|
# line above, which the installer unit tests assert on.
|
|
"$@" >/dev/null 2>&1
|
|
}
|
|
|
|
# Detect the active firewall. Order matters: a box can have several of these
|
|
# binaries installed but only one actually managing the input chain, so we pick
|
|
# the highest-level manager that reports itself active.
|
|
firewall_detect() {
|
|
if [ -n "${FIREWALL_BACKEND:-}" ]; then
|
|
printf '%s\n' "$FIREWALL_BACKEND"
|
|
return 0
|
|
fi
|
|
if command -v firewall-cmd >/dev/null 2>&1 && firewall-cmd --state >/dev/null 2>&1; then
|
|
printf 'firewalld\n'; return 0
|
|
fi
|
|
if command -v ufw >/dev/null 2>&1 && ufw status 2>/dev/null | grep -qiE '^Status:[[:space:]]*active'; then
|
|
printf 'ufw\n'; return 0
|
|
fi
|
|
if command -v nft >/dev/null 2>&1 && nft list ruleset >/dev/null 2>&1; then
|
|
printf 'nftables\n'; return 0
|
|
fi
|
|
if command -v iptables >/dev/null 2>&1; then
|
|
printf 'iptables\n'; return 0
|
|
fi
|
|
printf 'none\n'
|
|
}
|
|
|
|
# Split "80/tcp" into port + proto (proto defaults to tcp).
|
|
_fw_port() { printf '%s' "${1%%/*}"; }
|
|
_fw_proto() { case "$1" in */*) printf '%s' "${1##*/}" ;; *) printf 'tcp' ;; esac; }
|
|
|
|
firewall_open() {
|
|
local backend="$1"; shift
|
|
local spec port proto
|
|
case "$backend" in
|
|
firewalld)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
_fw_run firewall-cmd --permanent "--add-port=${port}/${proto}" || true
|
|
done
|
|
_fw_run firewall-cmd --reload || true
|
|
;;
|
|
ufw)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
_fw_run ufw allow "${port}/${proto}" || true
|
|
done
|
|
;;
|
|
iptables)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
# -C tests for an existing rule so we stay idempotent; -I only if absent.
|
|
_fw_run iptables -C INPUT -p "$proto" --dport "$port" -j ACCEPT || \
|
|
_fw_run iptables -I INPUT -p "$proto" --dport "$port" -j ACCEPT || true
|
|
done
|
|
;;
|
|
nftables)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
# Best-effort: requires an inet filter/input chain to exist.
|
|
_fw_run nft add rule inet filter input "$proto" dport "$port" accept || true
|
|
done
|
|
;;
|
|
none|*)
|
|
return 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
firewall_close() {
|
|
local backend="$1"; shift
|
|
local spec port proto
|
|
case "$backend" in
|
|
firewalld)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
_fw_run firewall-cmd --permanent "--remove-port=${port}/${proto}" || true
|
|
done
|
|
_fw_run firewall-cmd --reload || true
|
|
;;
|
|
ufw)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
_fw_run ufw delete allow "${port}/${proto}" || true
|
|
done
|
|
;;
|
|
iptables)
|
|
for spec in "$@"; do
|
|
port="$(_fw_port "$spec")"; proto="$(_fw_proto "$spec")"
|
|
_fw_run iptables -D INPUT -p "$proto" --dport "$port" -j ACCEPT || true
|
|
done
|
|
;;
|
|
nftables|none|*)
|
|
# nftables rule deletion needs the rule handle; we can't reliably
|
|
# find it after the fact, so leave it to the operator.
|
|
return 0
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Warn-and-document fallback text for when no firewall could be driven.
|
|
firewall_manual_hint() {
|
|
local ports="$1"
|
|
printf 'Open these ports manually so the panel is reachable: %s\n' "$ports"
|
|
}
|