Merge pull request 'fix(alpine): create /usr/local/sbin, and run the PAM sshd so pam_exec fires' (#7) from fix/alpine-posix-launcher into main

Reviewed-on: #7
This commit is contained in:
2026-09-02 13:18:59 +00:00
4 changed files with 95 additions and 2 deletions
+3
View File
@@ -158,6 +158,9 @@ seed_open_ports() {
# ============================================================================
write_apply() {
local save_cmd; save_cmd="$(fw_save_cmd)"
# Alpine's baselayout ships /usr/local/{bin,lib,share} but NOT sbin, so the
# redirect below fails with "No such file or directory" unless we make it.
install -d -m 0755 "${APPLY%/*}"
cat > "$APPLY" <<'ENGINE'
#!/bin/sh
# Managed by harden-firewall.sh -- do not edit by hand (regenerated on install).
+4 -1
View File
@@ -69,6 +69,7 @@ NOLOGIN="$(nologin_path)" # /sbin/nologin (Alpine/Alma) or /usr/sbin/nologi
# Install the sshuser tool alongside this script if present.
if [[ -f "$SCRIPT_DIR/sshuser.sh" ]]; then
install -d -m 0755 /usr/local/bin
install -m 0755 "$SCRIPT_DIR/sshuser.sh" /usr/local/bin/sshuser
log "Installed /usr/local/bin/sshuser"
fi
@@ -427,5 +428,7 @@ EOF
fi
log "Reloading ${SSHD_SVC}..."
svc_reload "$SSHD_SVC"
# Reload -- and on Alpine swap in the PAM sshd build if the running one
# predates this config (oslib explains why that can happen).
sshd_apply_config
log "Done."
+3 -1
View File
@@ -390,6 +390,8 @@ EOF
fi
log "Reloading ${SSHD_SVC}..."
svc_reload "$SSHD_SVC"
# Reload -- and on Alpine swap in the PAM sshd build if the running one
# predates this config (oslib explains why that can happen).
sshd_apply_config
log "Done. Your session, if any, should remain alive (reload preserves connections)."
log "Test from another machine before closing this session."
+85
View File
@@ -188,6 +188,77 @@ sshd_disable_keygen() {
fi
}
# ----------------------------------------------------------------------------
# Alpine keeps PAM support in a SEPARATE binary.
#
# openssh-server -> /usr/sbin/sshd (built WITHOUT PAM)
# openssh-server-pam -> /usr/sbin/sshd.pam (built WITH PAM)
#
# The OpenRC init picks between them in start_pre (checkconfig -> update_command:
# "sshd.pam if it is executable and the config says UsePAM yes"), and its
# reload/stop match the running process with `start-stop-daemon --exec "$command"`.
# So a host that is ALREADY running /usr/sbin/sshd never swaps over on its own:
# reload signals a process that doesn't match (nothing happens), stop matches
# nothing, and a follow-up start hits "address already in use". The daemon keeps
# serving without PAM -- which means /etc/pam.d/sshd, and therefore the pam_exec
# login notifier, is silently never consulted. Every other distro builds PAM into
# the one sshd binary, so this is Alpine-only.
# ----------------------------------------------------------------------------
sshd_wanted_binary() { # echo the sshd binary this host's config should be running
_require_detected
if [[ "$OS_FAMILY" == alpine ]] && [[ -x /usr/sbin/sshd.pam ]] && grep -qiE '^[[:space:]]*UsePAM[[:space:]]+yes' /etc/ssh/sshd_config 2>/dev/null; then
echo /usr/sbin/sshd.pam
else
echo /usr/sbin/sshd
fi
}
sshd_running_binary() { # echo the executable behind the running sshd master ('' if unknown)
local pid="" p
if [[ -r /run/sshd.pid ]]; then pid="$(cat /run/sshd.pid 2>/dev/null || true)"; fi
if [[ -z "$pid" ]]; then
for p in sshd.pam sshd; do
pid="$(pgrep -x "$p" 2>/dev/null | head -n1 || true)"
[[ -n "$pid" ]] && break
done
fi
[[ -n "$pid" ]] || return 0
readlink -f "/proc/$pid/exe" 2>/dev/null || true
}
# Apply a freshly written sshd_config. Normally a reload (keeps connections).
# On Alpine, when the running binary is not the one the config calls for, the
# service is stopped by pidfile and started again so the PAM build takes over --
# established sessions are separate processes and survive; only the listener
# blinks.
sshd_apply_config() {
_require_detected
local svc; svc="$(sshd_service)"
if [[ "$OS_FAMILY" == alpine ]]; then
local want run pid i
want="$(sshd_wanted_binary)"
run="$(sshd_running_binary)"
if [[ -n "$run" && "$run" != "$want" ]]; then
_warn "sshd is running $run, but this config needs $want -- restarting to swap it in."
_warn "(Existing SSH sessions survive; the listener is down for about a second.)"
pid=""
if [[ -r /run/sshd.pid ]]; then pid="$(cat /run/sshd.pid 2>/dev/null || true)"; fi
rc-service "$svc" stop >/dev/null 2>&1 || true
if [[ -n "$pid" ]]; then
kill "$pid" 2>/dev/null || true
i=0
while kill -0 "$pid" 2>/dev/null && [[ "$i" -lt 10 ]]; do sleep 1; i=$((i + 1)); done
kill -9 "$pid" 2>/dev/null || true
fi
rm -f /run/sshd.pid
svc_start "$svc" || _die "sshd failed to start as $want. Check: rc-service $svc start"
_log "sshd restarted as $(sshd_running_binary)."
return 0
fi
fi
svc_reload "$svc"
}
# ============================================================================
# Users & groups (busybox adduser/addgroup vs shadow useradd/groupadd)
# ============================================================================
@@ -286,6 +357,7 @@ install_boot_hook() { # install_boot_hook <name> <path-to-script>
rc-update add local default >/dev/null 2>&1 || true
"/etc/local.d/${name}.start" || true ;;
systemd)
install -d -m 0755 /usr/local/sbin
install -m 0755 "$src" "/usr/local/sbin/${name}"
cat > "/etc/systemd/system/${name}.service" <<UNIT
[Unit]
@@ -316,6 +388,8 @@ install_daily_job() { # install_daily_job <name> <script-src> [run-args...]
local args="$*"
# The job scripts use bash; ensure it's present (Alpine images often lack it).
command -v bash >/dev/null 2>&1 || pkg_install bash || true
# Alpine's baselayout ships /usr/local/{bin,lib,share} but NOT sbin.
install -d -m 0755 /usr/local/sbin
install -m 0755 "$src" "/usr/local/sbin/$name"
# Co-install oslib.sh so a script that sources it still works standalone.
local srcdir; srcdir="$(dirname "$src")"
@@ -587,6 +661,17 @@ CONF
# 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).
# "Wired into /etc/pam.d/sshd" is not the same as "will fire": on Alpine the
# PAM stack is only read when sshd IS the PAM build (see sshd_wanted_binary).
# Check that too, or we'd report ACTIVE for a hook that can never run.
if [[ "${OS_FAMILY:-}" == alpine ]]; then
if [[ ! -x /usr/sbin/sshd.pam ]]; then
_warn "Alpine: /usr/sbin/sshd.pam is missing, so /etc/pam.d/sshd is never read and this notifier cannot fire."
_warn " Fix: apk add openssh-server-pam && rc-service $(sshd_service) restart"
elif ! grep -qiE '^[[:space:]]*UsePAM[[:space:]]+yes' /etc/ssh/sshd_config 2>/dev/null; then
_warn "Alpine: sshd_config lacks 'UsePAM yes', so the PAM build is never selected and this notifier cannot fire."
fi
fi
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:-<NTFY_URL unset!>}"