From 3c02574dd0c696163b4432988d9189c893f660db Mon Sep 17 00:00:00 2001 From: William Gill Date: Sun, 14 Jun 2026 16:53:39 -0500 Subject: [PATCH 1/2] fix(launcher): install git across all distros in the curl-pipe bootstrap The one-liner bootstrap only auto-installed git on Alpine (apk), so piping it onto a fresh Debian/Alma host with no git fell straight into 'git clone' and died with 'git: command not found'. oslib's pkg_install can't help here -- the repo isn't on disk yet. Install git inline via apk/apt-get/dnf/yum, and fail with a clear message if it still can't. Co-Authored-By: Claude Opus 4.8 (1M context) --- automations.sh | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/automations.sh b/automations.sh index 64ca819..43a5b19 100644 --- a/automations.sh +++ b/automations.sh @@ -37,7 +37,21 @@ else echo "[x] Running standalone (piped). Set REPO_URL=... so I can clone the repo." >&2 exit 1 } - command -v git >/dev/null 2>&1 || { command -v apk >/dev/null 2>&1 && apk add -q git; } + # We need git to clone, but oslib.sh's pkg_install isn't on disk yet (that's + # what we're cloning). Install git inline across the supported package + # managers -- apk (Alpine), apt-get (Debian/Ubuntu), dnf/yum (Alma/RHEL). + if ! command -v git >/dev/null 2>&1; then + echo "[+] git not found; installing it..." >&2 + if command -v apk >/dev/null 2>&1; then apk add -q git || true + elif command -v apt-get >/dev/null 2>&1; then { apt-get update -qq && DEBIAN_FRONTEND=noninteractive apt-get install -y -qq git; } || true + elif command -v dnf >/dev/null 2>&1; then dnf install -y -q git || true + elif command -v yum >/dev/null 2>&1; then yum install -y -q git || true + fi + fi + command -v git >/dev/null 2>&1 || { + echo "[x] git is required to clone the repo, but it isn't installed and I couldn't install it automatically (need root + a supported package manager). Install git, then re-run." >&2 + exit 1 + } _tmp="$(mktemp -d -t automations.XXXXXX)" echo "[+] Cloning $REPO_URL ($REPO_BRANCH)..." git clone --depth 1 --branch "$REPO_BRANCH" "$REPO_URL" "$_tmp" From 60433e4c8dea5c4828a9020ba2efe80f2cd2fa87 Mon Sep 17 00:00:00 2001 From: William Gill Date: Sun, 14 Jun 2026 16:53:39 -0500 Subject: [PATCH 2/2] fix(harden): keep hardening and the ntfy notifier alive when sshguard can't install On a fresh AlmaLinux 9.8 box, install_bruteforce_protection ran unguarded under 'set -euo pipefail'. When sshguard (from EPEL) wasn't installable at that moment, the single failed dnf aborted the ENTIRE harden run before it wrote sshd_config or installed the pam_exec login notifier -- leaving a stock, unhardened box and a silently-missing ntfy hook. - oslib: install the iptables backend best-effort first, then sshguard, and return sshguard's status so callers can treat it as non-fatal. - harden-ssh/harden-jumphost: install_openssh now dies with a clear message on failure; sshguard is '|| warn' so sshd hardening and the notifier still apply. - install_login_notifier verifies the script + pam hook landed and logs 'Login notifier ACTIVE' (or a loud warning) instead of failing silently. - ntfy-ssh-login.sh: NTFY_DEBUG=1 logs delivery attempts + curl errors to /var/log/ssh-notify.log so the next silent failure leaves a trace. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/harden-jumphost.sh | 7 +++++-- scripts/harden-ssh.sh | 10 +++++++--- scripts/ntfy-ssh-login.sh | 17 ++++++++++++++++- scripts/oslib.sh | 30 +++++++++++++++++++++++++----- 4 files changed, 53 insertions(+), 11 deletions(-) diff --git a/scripts/harden-jumphost.sh b/scripts/harden-jumphost.sh index bdb80bc..9a75a9c 100644 --- a/scripts/harden-jumphost.sh +++ b/scripts/harden-jumphost.sh @@ -57,8 +57,11 @@ log "Detected OS: ${OS_ID} (family ${OS_FAMILY}, init ${INIT_SYSTEM})" # 1. Packages # ---------------------------------------------------------------------------- log "Installing OpenSSH + sshguard + iptables..." -install_openssh -install_bruteforce_protection +install_openssh || die "OpenSSH packages failed to install; cannot harden. Fix the package error above, then re-run." +# sshguard is best-effort (see harden-ssh.sh): never let a missing brute-force +# package abort the whole bastion hardening. +install_bruteforce_protection \ + || warn "sshguard not installed; brute-force protection is OFF. Add it later with: dnf install -y epel-release sshguard. Continuing with the rest of the hardening." ensure_gum || warn "gum not installed; sshuser will use its CLI mode." SFTP_PATH="$(sftp_server_path)" diff --git a/scripts/harden-ssh.sh b/scripts/harden-ssh.sh index 1393063..20fe080 100644 --- a/scripts/harden-ssh.sh +++ b/scripts/harden-ssh.sh @@ -64,7 +64,7 @@ log "Detected OS: ${OS_ID} (family ${OS_FAMILY}, init ${INIT_SYSTEM})" # ---------------------------------------------------------------------------- if ! command -v ssh >/dev/null 2>&1; then log "ssh not found; installing openssh..." - install_openssh + install_openssh || die "Could not install OpenSSH; cannot harden. Fix the package error above, then re-run." fi log "Checking OpenSSH version supports PQ KEX..." @@ -89,8 +89,12 @@ KEX_LIST="" # 2. Install packages (OS-gated inside oslib) # ---------------------------------------------------------------------------- log "Installing OpenSSH server + sshguard + iptables..." -install_openssh -install_bruteforce_protection +install_openssh || die "OpenSSH packages failed to install; cannot harden SSH. Fix the package error above, then re-run." +# sshguard is best-effort: a host where it can't install right now (e.g. EPEL +# momentarily unreachable) must still get the sshd_config hardening AND the login +# notifier -- not a silently half-configured box. Warn and press on. +install_bruteforce_protection \ + || warn "sshguard not installed; brute-force protection is OFF. Add it later with: dnf install -y epel-release sshguard. Continuing with the rest of the hardening." # The external SFTP subsystem binary path differs per distro. SFTP_PATH="$(sftp_server_path)" diff --git a/scripts/ntfy-ssh-login.sh b/scripts/ntfy-ssh-login.sh index ccca2c6..9b6ff96 100644 --- a/scripts/ntfy-ssh-login.sh +++ b/scripts/ntfy-ssh-login.sh @@ -123,5 +123,20 @@ tags="warning" [ -n "${NTFY_REGION:-}" ] && tags="${tags},${NTFY_REGION}" set -- "$@" -H "X-Tags: ${tags}" -curl "$@" -d "$body" "$NTFY_URL" >/dev/null 2>&1 || true +# Deliver. Failures are non-fatal -- a login must never be blocked by a notifier +# hiccup. Set NTFY_DEBUG=1 in the conf to log attempts + curl errors to +# /var/log/ssh-notify.log, so a silent failure (SELinux, egress, bad token, ...) +# leaves a trace instead of vanishing. +if [ "${NTFY_DEBUG:-0}" = "1" ]; then + log=/var/log/ssh-notify.log + printf '%s login user=%s rhost=%s -> %s\n' \ + "$(date -u +%FT%TZ 2>/dev/null || echo)" "$user" "$rhost" "$NTFY_URL" >> "$log" 2>/dev/null || true + if curl "$@" -d "$body" "$NTFY_URL" >>"$log" 2>&1; then + echo " -> delivered" >> "$log" 2>/dev/null || true + else + echo " -> curl FAILED (exit $?)" >> "$log" 2>/dev/null || true + fi +else + curl "$@" -d "$body" "$NTFY_URL" >/dev/null 2>&1 || true +fi exit 0 diff --git a/scripts/oslib.sh b/scripts/oslib.sh index 86f0ffa..ac1ec19 100644 --- a/scripts/oslib.sh +++ b/scripts/oslib.sh @@ -403,14 +403,21 @@ install_openssh() { } # Install sshguard + an iptables firewall backend. On RHEL/Alma sshguard lives -# in EPEL, so enable that first. +# in EPEL, so enable that first. The iptables backend is installed best-effort +# FIRST (it's usually already present as iptables-nft), then sshguard, and the +# function RETURNS sshguard's status -- so a caller can treat a sshguard miss +# (e.g. EPEL momentarily unreachable) as non-fatal and still apply the rest of +# the hardening instead of aborting the whole run. install_bruteforce_protection() { _require_detected case "$OS_FAMILY" in - alpine) pkg_install sshguard iptables ip6tables ;; - debian) pkg_install sshguard iptables ;; - rhel) pkg_install epel-release || true - pkg_install sshguard iptables ;; + alpine) pkg_install iptables ip6tables || true + pkg_install sshguard ;; + debian) pkg_install iptables || true + pkg_install sshguard ;; + rhel) pkg_install iptables || true # el9+: provided by iptables-nft + pkg_install epel-release || true # sshguard lives in EPEL + pkg_install sshguard ;; esac } @@ -538,6 +545,8 @@ NTFY_PRIORITY="${NTFY_PRIORITY:-min}" NTFY_REGION="${NTFY_REGION:-}" NOTIFY_GROUPS="${NOTIFY_GROUPS:-}" NOTIFY_PRIORITY_MAP="${NOTIFY_PRIORITY_MAP:-}" +# Set to 1 to log every delivery attempt (and curl errors) to /var/log/ssh-notify.log. +NTFY_DEBUG="${NTFY_DEBUG:-0}" CONF ) chmod 600 /etc/ssh-notify.conf @@ -556,6 +565,17 @@ CONF _warn "$pam not found; add this line to your sshd PAM stack manually:" _warn " $line" fi + + # Verify the hook actually landed and report loudly. A notifier that fails to + # install silently is worse than none -- you'd believe logins are watched + # when they aren't (exactly the trap that hid this on the first Alma run). + if [[ -x /opt/scripts/ntfy-ssh-login.sh ]] \ + && grep -qF '/opt/scripts/ntfy-ssh-login.sh' "$pam" 2>/dev/null; then + _log "Login notifier ACTIVE -> ${NTFY_URL:-}" + return 0 + fi + _warn "Login notifier did NOT fully install (script or pam hook missing)." + return 1 } # Locate the sshguard iptables backend binary (path varies by packaging).