← All four scripts
harden-3-ssh.sh
sshd hardening. Can lock you out, so it refuses to run without a working key and arms an automatic rollback.
Read this, do not pipe it. These scripts run as root. Copy the source into a file on your own machine, read it, and run it yourself. Nobody should curl a root shell script straight off a website, including this one.
#!/usr/bin/env bash
# harden-3-ssh.sh — sshd hardening.
#
# ⚠️ THIS CAN LOCK YOU OUT. Protections, in order:
# 1. Refuses to run if $ADMIN_USER has no authorized_keys (the classic way
# people lock themselves out: disable passwords with no key installed).
# 2. `sshd -t` validates the config BEFORE anything is reloaded.
# 3. A rollback timer restores the previous sshd_config and reloads.
# 4. You confirm from a SECOND session before the rollback is cancelled.
#
# sudo DRY_RUN=1 ./harden-3-ssh.sh
# sudo ./harden-3-ssh.sh
cd "$(dirname "${BASH_SOURCE[0]}")" || exit 1
# shellcheck source=lib.sh
source ./lib.sh
need_root
# Defaults to the human who invoked sudo, which is right on almost every box.
ADMIN_USER="${ADMIN_USER:-${SUDO_USER:-$(id -un)}}"
ROLLBACK_MIN="${ROLLBACK_MIN:-10}"
SSHD_CONF=/etc/ssh/sshd_config
DROPIN=/etc/ssh/sshd_config.d/99-ssh-hardening.conf
ROLLBACK_BIN=/usr/local/sbin/ssh-harden-rollback
head1 "Pre-flight (refusing is safer than locking you out)"
id "$ADMIN_USER" >/dev/null 2>&1 || { err "user '$ADMIN_USER' does not exist"; exit 1; }
AK="$(getent passwd "$ADMIN_USER" | cut -d: -f6)/.ssh/authorized_keys"
if [ ! -s "$AK" ]; then
err "REFUSING: $AK is missing or empty."
err " Disabling password auth now would lock you out permanently."
err " Install your public key first, then re-run."
exit 1
fi
KEYCOUNT=$(grep -cvE '^\s*(#|$)' "$AK" || true)
ok "$ADMIN_USER has $KEYCOUNT authorized key(s)"
# A key on disk does not prove key auth actually works. Make the human check.
if [ "$DRY_RUN" != "1" ]; then
warn "Have you logged in to THIS box using that key (not a password)?"
read -r -p "type 'yes' to continue: " a
[ "$a" = "yes" ] || { err "aborted"; exit 1; }
fi
head1 "Writing hardened sshd config"
write_file "$DROPIN" 0600 <<EOF
# Managed by harden-3-ssh.sh.
# A drop-in, so the distro sshd_config stays pristine and this is easy to revert.
PermitRootLogin no
PasswordAuthentication no
KbdInteractiveAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
PermitEmptyPasswords no
AuthenticationMethods publickey
MaxAuthTries 3
MaxSessions 10
LoginGraceTime 30
AllowUsers ${ADMIN_USER}
X11Forwarding no
AllowAgentForwarding no
AllowTcpForwarding no
PermitUserEnvironment no
ClientAliveInterval 300
ClientAliveCountMax 2
LogLevel VERBOSE
EOF
head1 "Validating config"
if sshd -t 2>/dev/null; then
ok "sshd -t passes"
else
err "sshd -t FAILED. Reverting the drop-in and changing nothing."
sshd -t 2>&1 | sed 's/^/ /'
rm -f "$DROPIN"
exit 1
fi
if [ "$DRY_RUN" = "1" ]; then
info "would back up config, arm rollback, reload sshd"
summary; exit 0
fi
head1 "Arming rollback and reloading"
BACKUP="/root/sshd-rollback-$(date +%s)"
mkdir -p "$BACKUP"
cp -a "$SSHD_CONF" "$BACKUP/sshd_config"
[ -d /etc/ssh/sshd_config.d ] && cp -a /etc/ssh/sshd_config.d "$BACKUP/sshd_config.d"
did "backed up ssh config to $BACKUP"
cat > "$ROLLBACK_BIN" <<EOF
#!/bin/sh
# Written by harden-3-ssh.sh. Restores SSH to its pre-hardening state.
rm -f "$DROPIN"
cp -a "$BACKUP/sshd_config" "$SSHD_CONF"
[ -d "$BACKUP/sshd_config.d" ] && cp -a "$BACKUP/sshd_config.d/." /etc/ssh/sshd_config.d/
systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null
logger -t ssh-harden-rollback "SSH config rolled back automatically"
EOF
chmod 700 "$ROLLBACK_BIN"
did "created $ROLLBACK_BIN"
rollback_arm ssh-harden-rollback "$ROLLBACK_MIN" "$ROLLBACK_BIN"
if systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null; then
did "sshd reloaded"
else
err "reload failed; running rollback now"
"$ROLLBACK_BIN"
exit 1
fi
# Existing sessions survive a reload, so this session proves nothing.
if confirm_still_connected "ssh ${ADMIN_USER}@<this-box> from a NEW terminal. It must succeed."; then
rollback_disarm ssh-harden-rollback
ok "SSH hardening confirmed"
info "Manual rollback remains available: $ROLLBACK_BIN"
else
warn "NOT confirmed. Doing nothing — rollback fires in <=${ROLLBACK_MIN} min."
warn " Or from the provider's console: $ROLLBACK_BIN"
fi
summary