Fix: Complete SSH bootstrap hardening
This commit is contained in:
@@ -48,6 +48,20 @@ system_path() {
|
||||
printf '%s%s\n' "$BOOT_ROOT" "$path"
|
||||
}
|
||||
|
||||
valid_authorized_keys() {
|
||||
local keys="$1" line saw_key=0
|
||||
while IFS= read -r line || [[ -n "$line" ]]; do
|
||||
if [[ "$line" =~ ^[[:space:]]*$ || "$line" =~ ^[[:space:]]*# ]]; then
|
||||
continue
|
||||
fi
|
||||
if ! ssh-keygen -l -f /dev/stdin >/dev/null 2>&1 <<<"$line"; then
|
||||
return 1
|
||||
fi
|
||||
saw_key=1
|
||||
done <"$keys"
|
||||
(( saw_key ))
|
||||
}
|
||||
|
||||
safe_authorized_keys() {
|
||||
local username="$1" user_home="$2" uid ssh_dir keys
|
||||
uid="$(id -u "$username")" || return 1
|
||||
@@ -57,7 +71,7 @@ safe_authorized_keys() {
|
||||
[[ -d "$ssh_dir" && ! -L "$ssh_dir" && -f "$keys" && ! -L "$keys" ]] || return 1
|
||||
[[ "$(stat -Lc '%u:%a' "$ssh_dir")" == "$uid:700" ]] || return 1
|
||||
[[ "$(stat -Lc '%u:%a' "$keys")" == "$uid:600" ]] || return 1
|
||||
grep -qEv '^[[:space:]]*(#|$)' "$keys"
|
||||
valid_authorized_keys "$keys"
|
||||
}
|
||||
|
||||
safe_root_authorized_keys() {
|
||||
@@ -65,7 +79,7 @@ safe_root_authorized_keys() {
|
||||
keys="$(system_path /root/.ssh/authorized_keys)" || return 1
|
||||
[[ -f "$keys" && ! -L "$keys" ]] || return 1
|
||||
[[ "$(stat -Lc '%u:%a' "$keys")" == '0:600' ]] || return 1
|
||||
grep -qEv '^[[:space:]]*(#|$)' "$keys"
|
||||
valid_authorized_keys "$keys"
|
||||
}
|
||||
|
||||
detect_ssh_unit() {
|
||||
@@ -81,8 +95,9 @@ detect_ssh_unit() {
|
||||
|
||||
restore_ssh_dropin() {
|
||||
local restore
|
||||
if [[ -n "${ssh_backup:-}" && -e "$ssh_backup" ]]; then
|
||||
restore="$(mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.restore)" || return 1
|
||||
if (( ssh_had_prior )); then
|
||||
[[ -n "$ssh_backup" && -f "$ssh_backup" && ! -L "$ssh_backup" ]] || return 1
|
||||
restore="$(mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.restore)" || return 1
|
||||
if ! cp -a -- "$ssh_backup" "$restore"; then
|
||||
remove_ssh_artifact "$restore" || true
|
||||
return 1
|
||||
@@ -106,7 +121,7 @@ restore_ssh_transaction_traps() {
|
||||
|
||||
remove_ssh_artifact() {
|
||||
local artifact="$1"
|
||||
[[ -n "$artifact" && -e "$artifact" ]] || return 0
|
||||
[[ -n "$artifact" && ( -e "$artifact" || -L "$artifact" ) ]] || return 0
|
||||
if rm -f -- "$artifact"; then
|
||||
return 0
|
||||
fi
|
||||
@@ -115,121 +130,164 @@ remove_ssh_artifact() {
|
||||
return 1
|
||||
}
|
||||
|
||||
print_ssh_recovery() {
|
||||
if (( ssh_had_prior )); then
|
||||
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
|
||||
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
|
||||
else
|
||||
printf 'SSH rollback needs manual recovery. No prior drop-in existed.\n' >&2
|
||||
printf ' rm -f -- %q\n' "$ssh_dropin" >&2
|
||||
fi
|
||||
printf ' sshd -t\n' >&2
|
||||
printf ' systemctl reload %s\n' "$ssh_unit" >&2
|
||||
}
|
||||
|
||||
policy_is_no() {
|
||||
local policy="$1" setting="$2"
|
||||
awk -v setting="$setting" '
|
||||
$1 == setting { count += 1; if ($2 != "no") bad = 1 }
|
||||
END { exit count != 1 || bad }
|
||||
' <<<"$policy"
|
||||
}
|
||||
|
||||
effective_ssh_policy_is_hardened() {
|
||||
local username="$1" root_policy target_policy context
|
||||
context='host=localhost,addr=127.0.0.1'
|
||||
root_policy="$(sshd -T -C "user=root,$context")" || return 1
|
||||
policy_is_no "$root_policy" permitrootlogin || return 1
|
||||
policy_is_no "$root_policy" passwordauthentication || return 1
|
||||
policy_is_no "$root_policy" kbdinteractiveauthentication || return 1
|
||||
|
||||
target_policy="$(sshd -T -C "user=$username,$context")" || return 1
|
||||
policy_is_no "$target_policy" passwordauthentication || return 1
|
||||
policy_is_no "$target_policy" kbdinteractiveauthentication
|
||||
}
|
||||
|
||||
rollback_ssh_transaction() {
|
||||
local reload_restored="$1" rollback_failed=0
|
||||
restore_ssh_dropin || rollback_failed=1
|
||||
sshd -t || rollback_failed=1
|
||||
if (( reload_restored )); then
|
||||
systemctl reload "$ssh_unit" || rollback_failed=1
|
||||
fi
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
if (( rollback_failed )); then
|
||||
print_ssh_recovery
|
||||
else
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
handle_ssh_transaction_exit() {
|
||||
if [[ "$ssh_transaction_state" == preparing \
|
||||
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
|
||||
remove_ssh_artifact "$ssh_candidate" || true
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
elif [[ "$ssh_transaction_state" == activating || "$ssh_transaction_state" == activated ]]; then
|
||||
restore_ssh_dropin || true
|
||||
fi
|
||||
}
|
||||
|
||||
handle_ssh_transaction_signal() {
|
||||
local signal_status="$1"
|
||||
trap - INT TERM
|
||||
if [[ -n "$ssh_candidate" && -e "$ssh_candidate" ]]; then
|
||||
ssh_transaction_active=0
|
||||
if [[ "$ssh_transaction_state" == preparing \
|
||||
|| ( "$ssh_transaction_state" == activating && -e "$ssh_candidate" ) ]]; then
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
remove_ssh_artifact "$ssh_candidate" || true
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
exit "$signal_status"
|
||||
fi
|
||||
rollback_failed=0
|
||||
restore_ssh_dropin || rollback_failed=1
|
||||
sshd -t || rollback_failed=1
|
||||
systemctl reload "$ssh_unit" || rollback_failed=1
|
||||
ssh_transaction_active=0
|
||||
restore_ssh_transaction_traps
|
||||
if (( rollback_failed )); then
|
||||
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
|
||||
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
|
||||
printf ' sshd -t\n' >&2
|
||||
printf ' systemctl reload %s\n' "$ssh_unit" >&2
|
||||
else
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
rollback_ssh_transaction 1 || true
|
||||
fi
|
||||
exit "$signal_status"
|
||||
}
|
||||
|
||||
harden_server_ssh() {
|
||||
local username="$1" user_home="$2" sshd_dir ssh_dropin harden ssh_unit
|
||||
local ssh_candidate="" ssh_backup="" rollback_failed=0
|
||||
local ssh_transaction_active=0
|
||||
local ssh_candidate="" ssh_backup="" ssh_had_prior=0
|
||||
local ssh_transaction_state=""
|
||||
local ssh_saved_exit_trap ssh_saved_int_trap ssh_saved_term_trap
|
||||
sshd_dir="$(system_path /etc/ssh/sshd_config.d)" || return 1
|
||||
ssh_dropin="$sshd_dir/90-panama.conf"
|
||||
ssh_dropin="$sshd_dir/00-panama.conf"
|
||||
|
||||
printf 'Harden sshd (disable root login and password auth)? [Y/n]: '
|
||||
if [[ -L "$ssh_dropin" || ( -e "$ssh_dropin" && ! -f "$ssh_dropin" ) ]]; then
|
||||
printf 'SSH hardening unavailable: %s is not a regular file\n' "$ssh_dropin" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
if ! ssh_unit="$(detect_ssh_unit)"; then
|
||||
echo "SSH hardening unavailable: neither sshd.service nor ssh.service is installed" >&2
|
||||
return 2
|
||||
fi
|
||||
|
||||
printf 'Harden sshd (disable root, password, and keyboard-interactive authentication)? [Y/n]: '
|
||||
read -r harden </dev/tty || harden=""
|
||||
if [[ "$harden" =~ ^[Nn] ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if ! ssh_unit="$(detect_ssh_unit)"; then
|
||||
echo "SSH hardening failed: neither sshd.service nor ssh.service exists" >&2
|
||||
ssh_saved_exit_trap="$(trap -p EXIT)"
|
||||
ssh_saved_int_trap="$(trap -p INT)"
|
||||
ssh_saved_term_trap="$(trap -p TERM)"
|
||||
ssh_transaction_state=preparing
|
||||
trap 'handle_ssh_transaction_exit' EXIT
|
||||
trap 'handle_ssh_transaction_signal 130' INT
|
||||
trap 'handle_ssh_transaction_signal 143' TERM
|
||||
|
||||
if ! ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.tmp)"; then
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
return 1
|
||||
fi
|
||||
|
||||
ssh_candidate="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.tmp)" || return 1
|
||||
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\n' >"$ssh_candidate"; then
|
||||
if ! printf 'PermitRootLogin no\nPasswordAuthentication no\nKbdInteractiveAuthentication no\n' >"$ssh_candidate"; then
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
remove_ssh_artifact "$ssh_candidate" || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -e "$ssh_dropin" ]]; then
|
||||
ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .90-panama.XXXXXX.backup)" || {
|
||||
ssh_had_prior=1
|
||||
if ! ssh_backup="$(umask 077; mktemp --tmpdir="$sshd_dir" .00-panama.XXXXXX.backup)"; then
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
remove_ssh_artifact "$ssh_candidate" || true
|
||||
return 1
|
||||
}
|
||||
if ! cat -- "$ssh_dropin" >"$ssh_backup"; then
|
||||
fi
|
||||
if ! cp -a -- "$ssh_dropin" "$ssh_backup"; then
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
remove_ssh_artifact "$ssh_candidate" || true
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
|
||||
ssh_saved_exit_trap="$(trap -p EXIT)"
|
||||
ssh_saved_int_trap="$(trap -p INT)"
|
||||
ssh_saved_term_trap="$(trap -p TERM)"
|
||||
trap 'if [[ "${ssh_transaction_active:-0}" == 1 ]]; then restore_ssh_dropin || true; fi' EXIT
|
||||
trap 'handle_ssh_transaction_signal 130' INT
|
||||
trap 'handle_ssh_transaction_signal 143' TERM
|
||||
ssh_transaction_active=1
|
||||
|
||||
ssh_transaction_state=activating
|
||||
if ! mv -f -- "$ssh_candidate" "$ssh_dropin"; then
|
||||
ssh_transaction_active=0
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
remove_ssh_artifact "$ssh_candidate" || true
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
return 1
|
||||
fi
|
||||
ssh_candidate=""
|
||||
ssh_transaction_state=activated
|
||||
|
||||
if ! sshd -t; then
|
||||
restore_ssh_dropin || rollback_failed=1
|
||||
sshd -t || rollback_failed=1
|
||||
ssh_transaction_active=0
|
||||
restore_ssh_transaction_traps
|
||||
if (( rollback_failed )); then
|
||||
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
|
||||
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
|
||||
printf ' sshd -t\n' >&2
|
||||
printf ' systemctl reload %s\n' "$ssh_unit" >&2
|
||||
else
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
fi
|
||||
if ! sshd -t || ! effective_ssh_policy_is_hardened "$username"; then
|
||||
rollback_ssh_transaction 0 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! systemctl reload "$ssh_unit"; then
|
||||
restore_ssh_dropin || rollback_failed=1
|
||||
sshd -t || rollback_failed=1
|
||||
systemctl reload "$ssh_unit" || rollback_failed=1
|
||||
ssh_transaction_active=0
|
||||
restore_ssh_transaction_traps
|
||||
if (( rollback_failed )); then
|
||||
printf 'SSH rollback needs manual recovery. Backup: %s\n' "$ssh_backup" >&2
|
||||
printf ' cp -a -- %q %q\n' "$ssh_backup" "$ssh_dropin" >&2
|
||||
printf ' sshd -t\n' >&2
|
||||
printf ' systemctl reload %s\n' "$ssh_unit" >&2
|
||||
else
|
||||
remove_ssh_artifact "$ssh_backup" || true
|
||||
fi
|
||||
rollback_ssh_transaction 1 || true
|
||||
return 1
|
||||
fi
|
||||
|
||||
ssh_transaction_active=0
|
||||
ssh_transaction_state=""
|
||||
restore_ssh_transaction_traps
|
||||
remove_ssh_artifact "$ssh_backup" || return 1
|
||||
echo "Wrote $ssh_dropin; make sure your key works before logging out."
|
||||
@@ -305,7 +363,6 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
&& ! -L "$user_ssh_dir" ]] && safe_root_authorized_keys; then
|
||||
copy_root_key=0
|
||||
if [[ ! -e "$user_ssh_dir" ]]; then
|
||||
mkdir -p "$user_ssh_dir"
|
||||
copy_root_key=1
|
||||
elif [[ ! -d "$user_ssh_dir" \
|
||||
|| "$(stat -Lc '%u:%a' "$user_ssh_dir")" != "$(id -u "$username"):700" ]]; then
|
||||
@@ -315,15 +372,19 @@ if [[ "$(id -u)" -eq 0 ]]; then
|
||||
fi
|
||||
if (( copy_root_key )); then
|
||||
echo "Copying root's authorized_keys to $username"
|
||||
cp "$(system_path /root/.ssh/authorized_keys)" "$user_keys"
|
||||
chmod 700 "$user_ssh_dir"
|
||||
chmod 600 "$user_keys"
|
||||
chown "$username:$username" "$user_ssh_dir" "$user_keys"
|
||||
root_keys="$(system_path /root/.ssh/authorized_keys)"
|
||||
if ! runuser -u "$username" -- install -d -m 0700 -- "$user_ssh_dir" \
|
||||
|| ! runuser -u "$username" -- install -m 0600 -- /dev/stdin "$user_keys" \
|
||||
<"$root_keys"; then
|
||||
echo "SSH hardening unavailable: could not install root's key for $username" >&2
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
if safe_authorized_keys "$username" "$user_home"; then
|
||||
if ! harden_server_ssh "$username" "$user_home"; then
|
||||
harden_status=0
|
||||
harden_server_ssh "$username" "$user_home" || harden_status=$?
|
||||
if (( harden_status != 0 && harden_status != 2 )); then
|
||||
echo "SSH hardening failed; stopping before install handoff." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
Reference in New Issue
Block a user