7faa9098de
Restructure around a single entry point (automations.sh) with a Gum wizard and a self-extracting bundle for repo-less installs. Add scripts/oslib.sh so the provisioning scripts (setup-host, harden-ssh, harden-jumphost, sshuser) run on Alpine/Debian/Alma; seed root keys from globals/. - ntfy SSH-login alerts (user, source IP, key, region, jump target) via pam_exec - daily auto-updates with AUTO_REBOOT=idle (reboots only when no SSH active) and opt-in Alpine stable-branch upgrades - generic + per-deployment cloud-init; Gitea release workflow on tag - README/LICENSE/.gitignore/.gitattributes (force LF); repo URLs -> Gitea
128 lines
5.4 KiB
Bash
128 lines
5.4 KiB
Bash
#!/bin/sh
|
|
#
|
|
# ntfy-ssh-login.sh -- pam_exec session hook that posts an SSH login event to
|
|
# an ntfy topic. POSIX sh (runs under busybox ash on Alpine too).
|
|
#
|
|
# Installed at /opt/scripts/ntfy-ssh-login.sh and wired into /etc/pam.d/sshd:
|
|
# session optional pam_exec.so /opt/scripts/ntfy-ssh-login.sh
|
|
#
|
|
# Reads /etc/ssh-notify.conf (see ssh-notify.conf.example). It reports:
|
|
# - the user and the source IP (PAM_USER / PAM_RHOST)
|
|
# - the SSH public key the user authenticated with (fingerprint)
|
|
# - the next hop in a ProxyJump path, when discoverable (best-effort)
|
|
# - the bastion's region tag, so you know which location it is
|
|
# and only fires for users in NOTIFY_GROUPS (if set).
|
|
#
|
|
# Notes:
|
|
# - Key capture needs `ExposeAuthInfo yes` in sshd_config (the harden
|
|
# scripts set it); it falls back to parsing the auth log.
|
|
# - Jump-target capture is best-effort: a ProxyJump opens a direct-tcpip
|
|
# channel (no session), so the target only appears in sshd logs at
|
|
# LogLevel VERBOSE/DEBUG. Absent that, it is omitted.
|
|
|
|
set -eu
|
|
|
|
CONF="${SSH_NOTIFY_CONF:-/etc/ssh-notify.conf}"
|
|
[ -r "$CONF" ] || exit 0
|
|
# shellcheck disable=SC1090
|
|
. "$CONF"
|
|
|
|
# Only act on session open, and only if a destination URL is configured.
|
|
[ "${PAM_TYPE:-}" = "open_session" ] || exit 0
|
|
[ -n "${NTFY_URL:-}" ] || exit 0
|
|
|
|
user="${PAM_USER:-unknown}"
|
|
rhost="${PAM_RHOST:-unknown}"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Read the most recent auth-log lines, wherever this distro keeps them.
|
|
# ---------------------------------------------------------------------------
|
|
read_authlog() {
|
|
if command -v journalctl >/dev/null 2>&1; then
|
|
journalctl -n 300 --no-pager 2>/dev/null
|
|
elif [ -r /var/log/auth.log ]; then tail -n 300 /var/log/auth.log
|
|
elif [ -r /var/log/secure ]; then tail -n 300 /var/log/secure
|
|
elif [ -r /var/log/messages ]; then tail -n 300 /var/log/messages
|
|
fi
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Group / security-level filter. NOTIFY_GROUPS empty => notify for everyone.
|
|
# ---------------------------------------------------------------------------
|
|
ugroups="$(id -nG "$user" 2>/dev/null || echo '')"
|
|
if [ -n "${NOTIFY_GROUPS:-}" ]; then
|
|
match=0
|
|
for g in $NOTIFY_GROUPS; do
|
|
for ug in $ugroups; do [ "$g" = "$ug" ] && match=1 && break; done
|
|
[ "$match" = 1 ] && break
|
|
done
|
|
[ "$match" = 1 ] || exit 0
|
|
fi
|
|
|
|
# Per-group priority override: NOTIFY_PRIORITY_MAP="ssh-admins:high ssh-jumpers:min"
|
|
prio="${NTFY_PRIORITY:-min}"
|
|
if [ -n "${NOTIFY_PRIORITY_MAP:-}" ]; then
|
|
for entry in $NOTIFY_PRIORITY_MAP; do
|
|
g="${entry%%:*}"; p="${entry#*:}"
|
|
for ug in $ugroups; do [ "$g" = "$ug" ] && prio="$p"; done
|
|
done
|
|
fi
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Which SSH key did the user authenticate with?
|
|
# ---------------------------------------------------------------------------
|
|
keyinfo=""
|
|
if [ -n "${SSH_USER_AUTH:-}" ] && [ -r "${SSH_USER_AUTH:-}" ]; then
|
|
# Lines look like: publickey ssh-ed25519 AAAA... [comment]
|
|
pk="$(awk '$1=="publickey"{print $2" "$3; exit}' "$SSH_USER_AUTH" 2>/dev/null || true)"
|
|
if [ -n "$pk" ]; then
|
|
# ssh-keygen -l prints: "<bits> SHA256:<fp> <comment...> (<ALGO>)".
|
|
# $2 is the fingerprint; $NF is the "(ALGO)" field regardless of comment.
|
|
keyinfo="$(printf '%s\n' "$pk" | ssh-keygen -lf - 2>/dev/null | awk '{print $NF" "$2}')"
|
|
[ -n "$keyinfo" ] || keyinfo="$(printf '%s' "$pk" | awk '{print $1}')"
|
|
fi
|
|
fi
|
|
if [ -z "$keyinfo" ]; then
|
|
# Fallback: the "Accepted publickey for USER ..." auth-log line carries
|
|
# the algorithm + SHA256 fingerprint.
|
|
line="$(read_authlog | grep "Accepted publickey for $user " | tail -n1 || true)"
|
|
keyinfo="$(printf '%s' "$line" | sed -n 's/.*: \([A-Za-z0-9-]*\) \(SHA256:[A-Za-z0-9+/=]*\).*/\1 \2/p')"
|
|
fi
|
|
[ -n "$keyinfo" ] || keyinfo="(key unknown)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Best-effort: the next hop in a ProxyJump path (direct-tcpip target).
|
|
# ---------------------------------------------------------------------------
|
|
jump=""
|
|
jline="$(read_authlog | grep -i 'direct-tcpip' | grep -F "$rhost" | tail -n1 || true)"
|
|
[ -z "$jline" ] && jline="$(read_authlog | grep -i 'direct-tcpip' | tail -n1 || true)"
|
|
# Match "... to HOST port PORT" or "... HOST:PORT ...".
|
|
jump="$(printf '%s' "$jline" | sed -n 's/.* to \([^ ]*\) port \([0-9]*\).*/\1:\2/p')"
|
|
[ -n "$jump" ] || jump="$(printf '%s' "$jline" | grep -oE '[A-Za-z0-9._-]+:[0-9]+' | tail -n1 || true)"
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Compose and send.
|
|
# ---------------------------------------------------------------------------
|
|
ts="$(date --utc +%FT%T.%3N%Z 2>/dev/null || date -u +%FT%TZ)"
|
|
selfhost="$(hostname -f 2>/dev/null || hostname 2>/dev/null || echo unknown)"
|
|
|
|
body="SSH login: ${user} from ${rhost}
|
|
key: ${keyinfo}"
|
|
[ -n "$jump" ] && body="${body}
|
|
jump-target: ${jump}"
|
|
body="${body}
|
|
host: ${selfhost} at ${ts}"
|
|
|
|
# Build curl args.
|
|
set -- -fsS -m 5 \
|
|
-H "X-Title: ${NTFY_TITLE:-Bastion Notification}" \
|
|
-H "X-Priority: ${prio}"
|
|
[ -n "${NTFY_TOKEN:-}" ] && set -- "$@" -H "Authorization: Bearer ${NTFY_TOKEN}"
|
|
[ -n "${NTFY_EMAIL:-}" ] && set -- "$@" -H "X-Email: ${NTFY_EMAIL}"
|
|
tags="warning"
|
|
[ -n "${NTFY_REGION:-}" ] && tags="${tags},${NTFY_REGION}"
|
|
set -- "$@" -H "X-Tags: ${tags}"
|
|
|
|
curl "$@" -d "$body" "$NTFY_URL" >/dev/null 2>&1 || true
|
|
exit 0
|