LibrePortal/scripts/ssh/host_access.sh
librelad d17e8814d0 feat(desudo): root-owned DNS + host-SSH-access helpers
Two more runtime root file-primitive subsystems moved behind self-
validating root-owned helpers so the scoped sudoers needn't grant blanket
sudo sed/tee/cp on /etc (which is root-equivalent — sudo arg wildcards
match across '/', so even path-scoped entries are bypassable):

- scripts/system/libreportal-dns: {clear|add <ip>} — edits /etc/resolv.conf
  only, validates the IP argument
- scripts/system/libreportal-ssh-access: authorized_keys + sshd
  PasswordAuthentication management, with the lockout guards moved INTO the
  helper (the trust boundary) so a compromised manager can't bypass them
- run_privileged: _runRootHelper dispatcher + runResolv / runSshAccess
  (runOwnership now uses it too)
- init.sh: initRootHelpers installs all three helpers root:root 0755 with
  the manager name baked in
- setup_dns -> runResolv (+ ping de-sudo'd, works unprivileged); host_access
  + webui_ssh_access -> runSshAccess

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 18:21:46 +01:00

121 lines
3.7 KiB
Bash

#!/bin/bash
# Admin SSH access to THIS host. Manages the install user's authorized_keys —
# paste a public key to grant access — and, behind a lockout guard, sshd
# password authentication. Everything here is on-demand only: nothing runs
# during install or deploy. LibrePortal is the *server* here, so the admin
# brings their own public key; we never handle their private key.
#
# All the privileged work (editing ~/.ssh and /etc/ssh/sshd_config) lives in the
# root-owned helper /usr/local/sbin/libreportal-ssh-access (runSshAccess), which
# also enforces the lockout guards in the trust boundary. These functions are the
# manager-side CLI/WebUI front for it: they shape arguments and print the UX.
# The user admins actually log in as (has sudo). Falls back to libreportal.
hostSshUser()
{
echo "${sudo_user_name:-libreportal}"
}
hostSshHome()
{
local u; u=$(hostSshUser)
[[ "$u" == "root" ]] && { echo "/root"; return; }
echo "/home/$u"
}
hostSshAuthKeysFile()
{
echo "$(hostSshHome)/.ssh/authorized_keys"
}
# Refresh the WebUI access snapshot after a change. No-op if generator absent.
hostSshRefreshUi()
{
declare -f webuiGenerateSshAccess >/dev/null 2>&1 && webuiGenerateSshAccess >/dev/null 2>&1
return 0
}
hostSshEnsureDir()
{
runSshAccess ensure-dir
}
# Count valid authorized public keys.
hostSshKeyCount()
{
runSshAccess key-count 2>/dev/null || echo 0
}
# True when sshd currently allows password authentication.
hostSshPasswordAuthEnabled()
{
[[ "$(runSshAccess pw-status 2>/dev/null)" != "off" ]]
}
# Add a base64-encoded PUBLIC key to the install user's authorized_keys.
hostSshKeyAdd()
{
local key_b64="$1"
[[ -z "$key_b64" ]] && { isError "hostSshKeyAdd requires <base64-public-key>"; return 1; }
local out rc
out=$(runSshAccess key-add "$key_b64")
rc=$?
case "$out" in
added) isSuccessful "SSH key authorized for $(hostSshUser)" ;;
already-authorized) isNotice "That key is already authorized." ;;
*)
[[ $rc -ne 0 ]] && { isError "Could not add key (not a valid SSH public key?)"; return 1; }
;;
esac
hostSshRefreshUi
}
# Remove the authorized key whose fingerprint matches $1. The helper guards
# against removing the last key while password auth is off (lockout).
hostSshKeyRemove()
{
local fp="$1"
[[ -z "$fp" ]] && { isError "hostSshKeyRemove requires <fingerprint>"; return 1; }
local out rc
out=$(runSshAccess key-remove "$fp" 2>&1)
rc=$?
if [[ $rc -eq 2 ]]; then
isError "Refusing to remove the last key while password login is disabled — you'd be locked out. Re-enable password login first."
return 1
fi
case "$out" in
removed) isSuccessful "Removed SSH key $fp" ;;
no-match) isNotice "No key matched fingerprint $fp" ;;
*) [[ $rc -ne 0 ]] && { isError "Could not remove key."; return 1; } ;;
esac
hostSshRefreshUi
}
# Enable/disable sshd password authentication. Disabling is guarded (helper-side):
# there must be at least one authorized key, or you'd lock yourself out.
hostSshSetPasswordAuth()
{
local want="$1" # on|off
case "$want" in
on|off) ;;
*) isError "hostSshSetPasswordAuth requires on|off"; return 1 ;;
esac
local out rc
out=$(runSshAccess pw-set "$want" 2>&1)
rc=$?
if [[ $rc -eq 2 ]]; then
isError "Refusing to disable password login with no authorized keys — add a key first or you'll be locked out."
return 1
fi
if [[ $rc -ne 0 ]]; then
isError "sshd config test failed — restored backup, no change made."
return 1
fi
isSuccessful "Password login ${want} (sshd reloaded)."
hostSshRefreshUi
}