#!/bin/bash # LibrePortal host-SSH-access helper — the only root-privileged management of the # admin's authorized_keys and sshd PasswordAuthentication the manager may trigger. # Installed root:root 0755 to /usr/local/sbin by init.sh. Self-contained (sources # no manager code) so the scoped sudoers can allow it instead of blanket # `sudo tee`/`sudo sed`/`sudo cp` on /etc/ssh + the admin's ~/.ssh (root). The # lockout guards live HERE, in the trust boundary, so a compromised manager can't # bypass them by editing the calling script. set -u [[ $EUID -eq 0 ]] || { echo "libreportal-ssh-access: must run as root" >&2; exit 1; } # Baked by init.sh at install (placeholder replaced); default if run unbaked. MANAGER="__MANAGER__" [[ "$MANAGER" == "__MANAGER__" || -z "$MANAGER" ]] && MANAGER="libreportal" if [[ "$MANAGER" == "root" ]]; then SSH_HOME="/root"; else SSH_HOME="/home/$MANAGER"; fi SSH_DIR="$SSH_HOME/.ssh" AKF="$SSH_DIR/authorized_keys" SSHD_CONFIG="/etc/ssh/sshd_config" _ensure_dir() { mkdir -p "$SSH_DIR" touch "$AKF" chmod 700 "$SSH_DIR" chmod 600 "$AKF" chown -R "$MANAGER:$MANAGER" "$SSH_DIR" } _key_count() { [[ -f "$AKF" ]] || { echo 0; return; } grep -cvE '^[[:space:]]*($|#)' "$AKF" 2>/dev/null || echo 0 } # 0 = password auth enabled (or unspecified default), 1 = disabled. _pw_enabled() { local v v=$(sshd -T 2>/dev/null | awk '/^passwordauthentication/ {print $2}') [[ -z "$v" ]] && v=$(grep -iE '^[[:space:]]*PasswordAuthentication' "$SSHD_CONFIG" 2>/dev/null | tail -1 | awk '{print tolower($2)}') [[ "$v" == "no" ]] && return 1 return 0 } key_add() { local key_b64="$1" [[ -z "$key_b64" ]] && { echo "key-add requires " >&2; return 1; } local pub pub=$(printf '%s' "$key_b64" | base64 -d 2>/dev/null | tr -d '\r' | grep -vE '^[[:space:]]*$' | head -1) [[ -z "$pub" ]] && { echo "empty key after decode" >&2; return 1; } if ! printf '%s\n' "$pub" | ssh-keygen -l -f - >/dev/null 2>&1; then echo "not a valid SSH public key" >&2; return 1 fi _ensure_dir local body; body=$(awk '{print $2}' <<< "$pub") if grep -qF "$body" "$AKF" 2>/dev/null; then echo "already-authorized" else printf '%s\n' "$pub" >> "$AKF" echo "added" fi chown "$MANAGER:$MANAGER" "$AKF" chmod 600 "$AKF" } key_remove() { local fp="$1" [[ -z "$fp" ]] && { echo "key-remove requires " >&2; return 1; } [[ "$fp" =~ ^[A-Za-z0-9:+/=._-]+$ ]] || { echo "invalid fingerprint" >&2; return 1; } [[ -f "$AKF" ]] || { echo "no authorized_keys file" >&2; return 1; } # Lockout guard (in the trust boundary): never remove the last key while # password login is off. if ! _pw_enabled && [[ "$(_key_count)" -le 1 ]]; then echo "refuse-last-key" >&2; return 2 fi local tmp removed=0 line lfp tmp=$(mktemp) while IFS= read -r line; do if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then printf '%s\n' "$line" >> "$tmp"; continue fi lfp=$(printf '%s\n' "$line" | ssh-keygen -l -f - 2>/dev/null | awk '{print $2}') if [[ -n "$lfp" && "$lfp" == "$fp" ]]; then removed=1; continue; fi printf '%s\n' "$line" >> "$tmp" done < "$AKF" if [[ "$removed" -eq 1 ]]; then cp "$tmp" "$AKF" chown "$MANAGER:$MANAGER" "$AKF" chmod 600 "$AKF" echo "removed" else echo "no-match" fi rm -f "$tmp" } pw_set() { local want="$1" case "$want" in on|off) ;; *) echo "pw-set requires on|off" >&2; return 1 ;; esac # Lockout guard: don't disable password login with no keys. if [[ "$want" == "off" && "$(_key_count)" -lt 1 ]]; then echo "refuse-no-keys" >&2; return 2 fi local value="yes"; [[ "$want" == "off" ]] && value="no" local backup="${SSHD_CONFIG}.libreportal.$(date +%s)" cp "$SSHD_CONFIG" "$backup" sed -i '/^[[:space:]]*#\?[[:space:]]*PasswordAuthentication\b/d' "$SSHD_CONFIG" printf 'PasswordAuthentication %s\n' "$value" >> "$SSHD_CONFIG" if ! sshd -t 2>/dev/null; then cp "$backup" "$SSHD_CONFIG" echo "sshd-test-failed" >&2; return 1 fi systemctl reload ssh 2>/dev/null || systemctl reload sshd 2>/dev/null echo "set:$want backup:$backup" } action="${1:-}"; shift 2>/dev/null || true case "$action" in ensure-dir) _ensure_dir ;; key-count) _key_count ;; pw-status) if _pw_enabled; then echo on; else echo off; fi ;; has-keys) [[ -f "$AKF" ]] ;; read-keys) [[ -f "$AKF" ]] && cat "$AKF" ;; authkeys-path) printf '%s\n' "$AKF" ;; key-add) key_add "${1:-}" ;; key-remove) key_remove "${1:-}" ;; pw-set) pw_set "${1:-}" ;; *) echo "usage: libreportal-ssh-access {ensure-dir|key-count|pw-status|has-keys|read-keys|authkeys-path|key-add |key-remove |pw-set }" >&2; exit 2 ;; esac