offer to add an ssh key even when one already exists

The zip option was already gone — it never came across in the port, since the
file is key material that cannot live in the repository and the script no longer
sits next to it. Pasting a public key was already the first option. What was
missing is the case where the account HAS a key: the section went straight to
hardening, so there was no way to authorise a second machine, a rebuilt laptop or
anyone else, and the original had no way to do it at all.

The same menu is now offered either way. What differs is whether it can be
declined without consequence: with no key, declining means the hardening below
refuses too, and the run says so rather than quietly moving on.

confirm() takes an optional default so this one can be [y/N]. Most questions in
this script are "do the thing you already asked for" and Enter should mean yes; a
genuine extra defaulting to yes is how people end up agreeing to things by
reflex.

A pasted key is trimmed before validation. Copying from a terminal or a password
manager routinely brings leading or trailing whitespace, and ssh-keygen will not
parse a key with it attached — which would have read as "that is not a valid
key" for a key that is perfectly fine.

Also corrected the reason unzip is in core utils, which still said it was there
to open ssh-keys.zip.

Verified: the add-another prompt appears and defaults to no, a whitespace-wrapped
key is trimmed and accepted, and the already-hardened path is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 18:42:58 +00:00
co-authored by Claude Opus 5
parent 9591f917f5
commit 192293cdca
3 changed files with 39 additions and 12 deletions
+12 -4
View File
@@ -145,18 +145,26 @@ prompt_value() {
# #
# ASSUME_YES=1 answers all of them, for an unattended run. # ASSUME_YES=1 answers all of them, for an unattended run.
confirm() { confirm() {
local message="${1:-Proceed?}" answer local message="${1:-Proceed?}"
[[ "${ASSUME_YES:-}" == "1" ]] && return 0 # Second argument flips the default. Most questions here are "do the thing you
# already asked for" and Enter should mean yes; a few are genuine extras, where
# defaulting to yes would have people agreeing to them by reflex.
local default="${2:-y}" answer prompt
[[ "${ASSUME_YES:-}" == "1" ]] && { [[ "$default" == "y" ]] && return 0 || return 1; }
if [[ "$default" == "y" ]]; then prompt="[Y/n]"; else prompt="[y/N]"; fi
while true; do while true; do
# EOF is not a yes. Without this an unattended run without ASSUME_YES would # EOF is not a yes. Without this an unattended run without ASSUME_YES would
# spin here forever. # spin here forever.
if ! read -rp " ${message} [Y/n]: " answer; then if ! read -rp " ${message} ${prompt}: " answer; then
echo "" echo ""
fail "No answer. Set ASSUME_YES=1 to run without prompts." fail "No answer. Set ASSUME_YES=1 to run without prompts."
fi fi
[[ -z "$answer" ]] && answer="$default"
case "$answer" in case "$answer" in
"" | y | Y | yes | Yes) return 0 ;; y | Y | yes | Yes) return 0 ;;
n | N | no | No) return 1 ;; n | N | no | No) return 1 ;;
*) warn "Answer y or n." ;; *) warn "Answer y or n." ;;
esac esac
+5 -5
View File
@@ -45,11 +45,11 @@ LAST_SKIPPED=()
# that make a machine worth sitting at. # that make a machine worth sitting at.
# #
# The first six are load-bearing and each is used by a later step — curl fetches # The first six are load-bearing and each is used by a later step — curl fetches
# in nine of them, jq parses the lazygit release API, unzip opens ssh-keys.zip, # in nine of them, jq parses the lazygit release API, gnupg dearmors the Docker
# gnupg dearmors the Docker keyring, git clones the Neovim config, and # keyring, git clones the Neovim config, unzip opens anything that arrives as an
# ca-certificates is what makes any of the fetching work. The rest are the # archive, and ca-certificates is what makes any of the fetching work. The rest
# environment: nothing calls them, they are here because a box you use should # are the environment: nothing calls them, they are here because a box you use
# have them. # should have them.
# #
# build-essential is the one entry with any reach beyond itself: it is a # build-essential is the one entry with any reach beyond itself: it is a
# meta-package (gcc, g++, make, libc6-dev, dpkg-dev), so on a machine where a # meta-package (gcc, g++, make, libc6-dev, dpkg-dev), so on a machine where a
+22 -3
View File
@@ -849,10 +849,26 @@ if ! skip; then
echo " root login: $(sshd_effective permitrootlogin)" echo " root login: $(sshd_effective permitrootlogin)"
# ── a key first ── # ── a key first ──
#
# The same menu whether or not there is already one, because "add another" is a
# real need — a second laptop, a rebuilt machine — and the original had no way
# to do it at all. What changes is whether the question can be declined: with no
# key, declining means the hardening below will refuse too, and the run says so.
ADD_KEY=false
if ((KEY_COUNT == 0)); then if ((KEY_COUNT == 0)); then
echo "" echo ""
warn "${USERNAME} has no authorised key. Password login cannot be turned off until it has one." 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)" ADD_KEY=true
else
fix_ssh_permissions
echo ""
ADD_KEY=false
confirm "Add another authorised key for ${USERNAME}?" n && ADD_KEY=true
fi
if $ADD_KEY; then
echo ""
echo " [1] paste a public key (one line, from your own ~/.ssh/id_ed25519.pub)"
echo " [2] generate a new keypair on this machine" echo " [2] generate a new keypair on this machine"
echo " [3] leave it for now" echo " [3] leave it for now"
echo "" echo ""
@@ -866,6 +882,11 @@ if ! skip; then
case "${SSH_KEY_CHOICE:-1}" in case "${SSH_KEY_CHOICE:-1}" in
1) 1)
read -rp " Paste the public key: " SSH_PASTED || fail "No answer." read -rp " Paste the public key: " SSH_PASTED || fail "No answer."
# Trimmed: pasting from a terminal or a password manager routinely
# brings leading or trailing whitespace, and ssh-keygen will not parse
# a key with it attached.
SSH_PASTED="${SSH_PASTED#"${SSH_PASTED%%[![:space:]]*}"}"
SSH_PASTED="${SSH_PASTED%"${SSH_PASTED##*[![:space:]]}"}"
add_authorized_key "$SSH_PASTED" && SSH_KEY_DONE=true add_authorized_key "$SSH_PASTED" && SSH_KEY_DONE=true
;; ;;
2) 2)
@@ -883,8 +904,6 @@ if ! skip; then
esac esac
done done
KEY_COUNT="$(authorized_key_count)" KEY_COUNT="$(authorized_key_count)"
else
fix_ssh_permissions
fi fi
# ── then hardening, and only then ── # ── then hardening, and only then ──