LibrePortal/scripts/system/libreportal-ssh-access
librelad afe0ef1c7e chore: drop duplicate doc files + fix wrong/stale comments
- docs: remove the docs/README.md index and docs/CONTRIBUTING.md pointer
  (duplicate filenames); the canonical contributing guide stays at
  docs/contributing/contributing.md. Clean tree, no name collisions.
- scripts/system/*: 6 helper headers + host_access.sh said the helpers
  install to /usr/local/sbin, but init.sh installs all of them to
  /usr/local/lib/libreportal/ (verified via initRootHelpers + the sudoers
  Cmnd_Alias). Corrected. The only remaining /usr/local/sbin is the legit
  PATH export in the task processor.
- frontend kernel: drop migration-era comments that are now false post-
  modularization (feature-registry 'passive/phase 0/unused', lifecycle
  'ctx.services lands with Phase 2', manifest 'scan generator lands') —
  describe current behaviour instead.

Comment-only edits to scripts/system/* — no footprint_version bump (no
behavioural change; bumping would force needless reinstalls).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-31 01:05:16 +01:00

130 lines
4.8 KiB
Bash

#!/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/lib/libreportal/ 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 <base64-public-key>" >&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 <fingerprint>" >&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 <b64>|key-remove <fp>|pw-set <on|off>}" >&2; exit 2 ;;
esac