port ssh keys and hardening as one section, and make the hardening actually work
They were two sections, and being two is what let the second lock you out of a machine the first had failed to put a key on. Step 8 could warn-and-skip — no ssh-keys.zip, or an unrecognised menu choice, since its case had no default arm — and still mark itself done; step 9 then disabled password authentication and root login regardless. No key, no password, no root, on a box that may be in a datacentre. Nothing here turns off password authentication without first confirming a usable key is in place, and the refusal says why rather than skipping quietly. The hardening also did not do anything on a modern Ubuntu, and could not be seen not to: It sed'd /etc/ssh/sshd_config. Ubuntu includes /etc/ssh/sshd_config.d/*.conf from line 12 of that file, and sshd takes the FIRST value it obtains for a keyword rather than the last. Cloud images ship 50-cloud-init.conf containing `PasswordAuthentication yes`, read long before the line the sed edited. The run reported "SSH hardened" and password login stayed on. The settings now go in a drop-in named 01-machine-setup.conf, which is the only placement that wins under first-value-wins. It also sed'd ChallengeResponseAuthentication, renamed to KbdInteractiveAuthentication in OpenSSH 8.7. On 24.04 the old name is nowhere in the file, so that substitution matched nothing at all. State is read with `sshd -T`, which reports what sshd resolves across the main file and every drop-in — reading the config files tells you what is written, not what wins. Keys are counted by asking ssh-keygen to parse authorized_keys rather than by counting lines: comments, blanks and a half-finished paste all look like lines, and "there is a file" is not "there is a key that works". A pasted key is validated before it is stored, and matched on the key body rather than the whole line, so re-running does not authorise the same key four times over four runs. sshd -t validates the new config before anything is reloaded, and the drop-in is restored or removed if it does not parse — a config sshd refuses is a machine with no ssh after the next restart. Reload rather than restart, so the session this is running over is not the experiment, and the run says out loud to test a new connection before closing the current one. Generating a keypair now says the obvious thing the original did not: the private key is on the server, and a private key living on the machine it opens is a spare copy of the lock rather than a second factor. Verified against this host (1 key, already hardened, correctly does nothing) and with sshd_effective stubbed to a fresh-cloud-image state — the guard refuses and harden_sshd is never reached. Also verified key validation, dedup and 0700/0600 permissions. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -28,6 +28,8 @@ source "$SCRIPT_DIR/lib/system.sh"
|
||||
source "$SCRIPT_DIR/lib/disk.sh"
|
||||
# shellcheck source=lib/files.sh
|
||||
source "$SCRIPT_DIR/lib/files.sh"
|
||||
# shellcheck source=lib/ssh.sh
|
||||
source "$SCRIPT_DIR/lib/ssh.sh"
|
||||
|
||||
# Trap errors with context. Installed here rather than in lib/base.sh, because
|
||||
# that file is definitions only and a trap is a side effect on whoever sources it.
|
||||
@@ -828,13 +830,108 @@ elif ! skip; then
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# 15. SSH access
|
||||
# =============================================================================
|
||||
#
|
||||
# Keys and hardening in one section, deliberately. They were two in the original,
|
||||
# and being two is what let the second one lock you out of a machine the first
|
||||
# one had failed to put a key on.
|
||||
|
||||
step "SSH access"
|
||||
if ! skip; then
|
||||
KEY_COUNT="$(authorized_key_count)"
|
||||
|
||||
echo ""
|
||||
info "SSH access — how you get into this machine"
|
||||
echo " authorised keys for ${USERNAME}: ${KEY_COUNT}"
|
||||
echo " password login: $(sshd_effective passwordauthentication)"
|
||||
echo " root login: $(sshd_effective permitrootlogin)"
|
||||
|
||||
# ── a key first ──
|
||||
if ((KEY_COUNT == 0)); then
|
||||
echo ""
|
||||
warn "${USERNAME} has no authorised key. Password login cannot be turned off until it has one."
|
||||
echo " [1] paste a public key (the contents of your ~/.ssh/id_ed25519.pub)"
|
||||
echo " [2] generate a new keypair on this machine"
|
||||
echo " [3] leave it for now"
|
||||
echo ""
|
||||
|
||||
SSH_KEY_DONE=false
|
||||
while [[ "$SSH_KEY_DONE" == false ]]; do
|
||||
if ! read -rp " Which one? (1/2/3) [1]: " SSH_KEY_CHOICE; then
|
||||
echo ""
|
||||
fail "No answer."
|
||||
fi
|
||||
case "${SSH_KEY_CHOICE:-1}" in
|
||||
1)
|
||||
read -rp " Paste the public key: " SSH_PASTED || fail "No answer."
|
||||
add_authorized_key "$SSH_PASTED" && SSH_KEY_DONE=true
|
||||
;;
|
||||
2)
|
||||
generate_user_key "${USERNAME}@$(hostname)"
|
||||
ok "keypair generated and authorised"
|
||||
echo ""
|
||||
echo " The PRIVATE key is on this machine at ${USER_HOME}/.ssh/id_ed25519."
|
||||
echo " Copy it to the machine you connect FROM, then delete it here —"
|
||||
echo " a private key that lives on the server it opens is not a"
|
||||
echo " second factor, it is a spare copy of the lock."
|
||||
SSH_KEY_DONE=true
|
||||
;;
|
||||
3) SSH_KEY_DONE=true ;;
|
||||
*) warn "Pick 1, 2 or 3." ;;
|
||||
esac
|
||||
done
|
||||
KEY_COUNT="$(authorized_key_count)"
|
||||
else
|
||||
fix_ssh_permissions
|
||||
fi
|
||||
|
||||
# ── then hardening, and only then ──
|
||||
if [[ "$(sshd_effective passwordauthentication)" == "no" && "$(sshd_effective permitrootlogin)" == "no" ]]; then
|
||||
echo ""
|
||||
echo " already hardened, nothing to do"
|
||||
SUMMARY+=("SSH: already hardened, ${KEY_COUNT} key(s) for ${USERNAME}")
|
||||
elif ((KEY_COUNT == 0)); then
|
||||
echo ""
|
||||
warn "not hardening: ${USERNAME} still has no authorised key"
|
||||
echo " Turning off password login now would leave no way in at all. Add a"
|
||||
echo " key and run this again."
|
||||
SUMMARY+=("SSH: NOT hardened — no key for ${USERNAME}, password login left on")
|
||||
else
|
||||
echo ""
|
||||
info "Harden sshd?"
|
||||
echo " Turns off password login, keyboard-interactive login and root"
|
||||
echo " login. ${USERNAME} has ${KEY_COUNT} authorised key(s), so you keep a way in."
|
||||
echo ""
|
||||
echo " Written to ${SSHD_DROPIN} rather than sshd_config, and named 01- so"
|
||||
echo " it is read before the cloud-init drop-in that would otherwise win."
|
||||
echo " Checked with 'sshd -t' before anything is reloaded, and reloaded"
|
||||
echo " rather than restarted so this session is not the experiment."
|
||||
echo ""
|
||||
warn "Test a new ssh session before closing this one."
|
||||
if confirm "Proceed?"; then
|
||||
if harden_sshd; then
|
||||
ok "password login: $(sshd_effective passwordauthentication), root login: $(sshd_effective permitrootlogin)"
|
||||
SUMMARY+=("SSH: hardened (password and root login off, ${KEY_COUNT} key(s))")
|
||||
else
|
||||
SUMMARY+=("SSH: hardening FAILED — sshd rejected the config, nothing changed")
|
||||
fi
|
||||
else
|
||||
warn "skipped by request"
|
||||
SUMMARY+=("SSH: SKIPPED by request — password login left on")
|
||||
fi
|
||||
fi
|
||||
step_ok
|
||||
fi
|
||||
|
||||
# =============================================================================
|
||||
# NOT PORTED YET
|
||||
# =============================================================================
|
||||
#
|
||||
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
|
||||
#
|
||||
# ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades ·
|
||||
# dns · static ip · fail2ban · unattended-upgrades ·
|
||||
# git config · docker · zsh + prompt (incl. .tmux.conf) · tailscale · neovim · js runtimes ·
|
||||
# dev tools · ufw · zshrc
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user