Files
automations/cloud-init/jumphost.yml
T
57_WolveandClaude Opus 5 339c62a1b0 fix(firewall): skip the host firewall on Proxmox
Proxmox VE and Proxmox Mail Gateway are Debian, so os_detect classified them as
debian and harden-firewall.sh installed the iptables backend on top of
pve-firewall.

The conflict is not the obvious one. pve-firewall does NOT delete third-party
rules: it restores with `iptables-restore -n` (--noflush), only ever flushes
chains matching its own patterns (PVEFW-*, tapNiM-*, vethNiM-*, fwbrN-*,
GROUP-*), appends `-A INPUT -j PVEFW-INPUT` only when that hook is missing, and
never sets a built-in chain's policy. Our rules survive it. We are the ones
doing damage:

- `-P INPUT DROP` is ours alone, and PVEFW-HOST-IN RETURNs on accept rather than
  ACCEPTing (it still has to check tap rules), so traffic Proxmox explicitly
  allowed falls out of its chain onto our DROP -- we silently override the
  platform's own accepts.
- netfilter-persistent restores at boot with a full iptables-restore (no
  --noflush), wiping PVE's hook along with everything else until the daemon
  re-appends it ~10s later.
- a deny-by-default chain has to enumerate the whole platform to stay usable:
  8006, 5405-5412/udp corosync, 60000-60050, 5900-5999, 3128, 22, 111/udp, plus
  Ceph when hyperconverged -- and `-i lo`, or pveproxy loses pvedaemon on :85.
- under the nftables backend (PVE 8.2+) an nft DROP beats an iptables ACCEPT, so
  our rules would not even be authoritative.

So don't manage a firewall there at all:

- oslib: is_proxmox() -- matches hosts shipping pve-firewall (VE/PMG), not PBS.
- harden-firewall.sh: a third backend, "pve", that deliberately does nothing.
  apply explains and exits 0, allow/deny refuse loudly rather than fake success
  for a rule they didn't add, list shows pve-firewall status. It overrides an
  explicit FW_BACKEND; FW_IGNORE_PVE=1 is the one escape hatch.
- harden-ssh.sh / harden-jumphost.sh: skip the firewall and install the
  standalone INPUT -> sshguard boot hook instead. That jump is safe alongside
  pve-firewall -- inserted with -I, it sits ahead of the appended PVEFW-INPUT
  hook and keeps first look at NEW connections.

Detection only helps hosts built from here on, so `disable` cleans up one that
was hardened earlier: it detects leftovers (and says which signal fired), sets
INPUT ACCEPT *before* flushing so it can't drop the SSH session it runs over,
deletes /etc/firewall and the engine, disables boot restore and renames the
saved rulesets aside rather than persisting the open state, re-adds the sshguard
jump, and restarts pve-firewall. `apply` points at it when it spots leftovers.

Documented plainly that skipped is NOT protected: Proxmox's firewall is off by
default (cluster-wide enable defaults to 0, and the daemon tears its chains down
every ~10s while it is), so these hosts have no host firewall until someone
enables it -- and the node panel's "Firewall: Yes" is ignored while the
datacenter one says No.

Also: svc_disable + fw_restore_services/fw_saved_files in oslib
(fw_enable_restore now derives from the former), and usage() prints the whole
header block instead of a hardcoded line range.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-10 18:51:50 -05:00

63 lines
2.8 KiB
YAML

#cloud-config
#
# Jump-host (bastion) bootstrap -- Alpine, Debian, or Alma Linux.
#
# Same base steps as base.yml, but applies the bastion hardening
# (scripts/harden-jumphost.sh) instead of plain SSH hardening:
# - ssh-admins : full shell for maintenance
# - ssh-jumpers : ProxyJump only, to the JUMP_TARGETS whitelist
#
# Fill in REPO_URL, HOST, and JUMP_TARGETS, then paste as instance user-data.
# Add jumper/admin users afterwards with the installed `sshuser` tool.
#
# NOTE: harden-jumphost.sh prints a freshly generated root private key to
# stdout (-> the cloud console/serial log). Capture it there, or rely on the
# keys seeded from globals/ and ignore it.
runcmd:
- |
set -e
# ===== config =====
REPO_URL=https://git.anomalous.dev/57_Wolve/automations.git
REPO_BRANCH=main
HOST=ssh-1 # <svc>-<n>; "ssh" is the bastion service code
BASE_DOMAIN=srvno.de
DATACENTER="Globally Everywhere"
SSH_PORT=22
ALLOWED_IP= # optional: whitelist your client IP
ENABLE_FIREWALL=1 # deny-by-default host firewall (0 to skip;
# always skipped on Proxmox -- pve-firewall owns it)
JUMP_TARGETS="10.0.0.5:22 10.0.0.6:22" # hosts jumpers may ProxyJump to
# Optional login notifications (pam_exec -> ntfy). Leave NTFY_URL empty to
# skip. NTFY_REGION defaults to the region segment of this host's FQDN.
NTFY_URL=
NTFY_TOKEN= # bearer token; empty if unauth publish
NTFY_EMAIL=
NTFY_REGION=
# ==================
# Prerequisites (OS-agnostic).
if command -v apk >/dev/null 2>&1; then apk add --no-cache bash git curl
elif command -v apt-get >/dev/null 2>&1; then apt-get update -qq && apt-get install -y -qq bash git curl
elif command -v dnf >/dev/null 2>&1; then dnf install -y -q bash git curl
fi
git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" /opt/automations
cd /opt/automations
# Hostname + shared MOTD.
HOST="$HOST" BASE_DOMAIN="$BASE_DOMAIN" DATACENTER="$DATACENTER" bash scripts/setup-host.sh
# Seed root's (ssh-admins) authorized_keys from globals/.
. scripts/lib.sh && load_globals \
&& install -d -m 700 /root/.ssh \
&& resolve_ssh_keys >> /root/.ssh/authorized_keys || true
sort -u /root/.ssh/authorized_keys -o /root/.ssh/authorized_keys 2>/dev/null || true
# Bastion hardening (admins shell + jumpers ProxyJump whitelist + optional
# login notifications).
SSH_PORT="$SSH_PORT" ALLOWED_IP="$ALLOWED_IP" JUMP_TARGETS="$JUMP_TARGETS" \
ENABLE_FIREWALL="$ENABLE_FIREWALL" \
NTFY_URL="$NTFY_URL" NTFY_TOKEN="$NTFY_TOKEN" NTFY_EMAIL="$NTFY_EMAIL" NTFY_REGION="$NTFY_REGION" \
FORCE=1 bash scripts/harden-jumphost.sh