#!/bin/bash # Mode-aware privileged operations. # # Ownership model (single source of truth — see reconcileDockerOwnership): # The MANAGER user ($sudo_user_name, e.g. libreportal) runs the CLI + host # scripts and is in the docker group, so it owns and operates the LibrePortal # control plane in BOTH modes. root:root is never the intended owner — it only # ever appeared as an artifact of un-de-sudo'd `sudo` commands. # rooted — the manager owns everything under /docker (it talks to the root # docker socket via the docker group); ops run AS the manager. # rootless — the manager owns the control plane; the docker install user owns # /docker/containers/** (the rootless daemon requires it). # Only genuine system administration (apt/systemctl/ufw/sysctl/useradd, /etc) # needs real root — that goes through runSystem. # Run a command AS the manager user (plain if we're already it — the runtime # case — otherwise sudo -u to it, e.g. at install time when we're root). This is # how we keep files manager-owned instead of accidentally root-owned. runAsManager() { local mgr="${sudo_user_name:-libreportal}" if [[ "$(id -un)" == "$mgr" ]]; then "$@" else sudo -u "$mgr" "$@" fi } # /docker data-plane command (mkdir/chown/rm/cp/mv/sed/sqlite3/docker/etc.) on # app/container files. # rooted -> as the manager user (owns /docker, in the docker group) # rootless -> as the docker install user (owns /docker/containers/**, and has # DOCKER_HOST set so `docker ...` hits the rootless socket) # For stdin-fed writes (`… | sudo tee file`) use runFileWrite below. runFileOp() { if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then dockerCommandRunInstallUser --argv "$@" else runAsManager "$@" fi } # Write stdin to a /docker data-plane path (replaces `… | sudo tee path`). # Pass -a/--append as the first arg to append instead of truncate. # Usage: some_command | runFileWrite [-a] /path/to/file runFileWrite() { local append_flag=() if [[ "$1" == "-a" || "$1" == "--append" ]]; then append_flag=(-a) shift fi local dest="$1" if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then dockerCommandRunInstallUser "tee ${append_flag[*]} '$dest' >/dev/null" else runAsManager tee "${append_flag[@]}" "$dest" >/dev/null fi } # Op on a MANAGER-owned path — the LibrePortal clone/templates AND the /docker # control plane (apps DB, configs/, logs/, scripts). Owned by the manager in # BOTH modes, so it always runs as the manager. runInstallOp() { runAsManager "$@" } # Write stdin to a MANAGER-owned path (apps DB sidecars, configs/, logs/ — e.g. # the /docker/logs log-append idiom). Manager-owned in both modes. # Pass -a/--append as the first arg to append. runInstallWrite() { local append_flag=() if [[ "$1" == "-a" || "$1" == "--append" ]]; then append_flag=(-a) shift fi local dest="$1" runAsManager tee "${append_flag[@]}" "$dest" >/dev/null } # Backup-engine command (borg/restic/kopia) run AS the dedicated backup user # ($docker_install_user), with the environment preserved (-E) so the repo # password and BORG_/RESTIC_/KOPIA_ env vars reach the tool. Never root — the # scoped sudoers lets the manager drop to this user. Single funnel so the # backup subsystem's privilege drop has one audit point. runBackupOp() { sudo -E -u "$docker_install_user" "$@" } # Run one of the ROOT-OWNED LibrePortal helpers installed (root:root 0755) under # /usr/local/sbin by init.sh. These are how the manager-run runtime (Model A) # performs the genuine-root operations it can't drop — establishing the /docker # ownership model, editing /etc/resolv.conf, managing host SSH access — WITHOUT # the scoped sudoers granting blanket `sudo chown/chmod/tee/sed/cp` (which would # be root-equivalent: chown /etc/sudoers, tee a new sudoers drop-in, …). Each # helper validates its own fixed-path operations, so the sudoers can allow it # wholesale. At install time (already root) the installed helper may be absent, so # run the bundled copy directly — no sudo, no escalation, since we are root. _runRootHelper() { local name="$1"; shift local helper="/usr/local/sbin/$name" if [[ -x "$helper" ]]; then sudo "$helper" "$@" elif [[ $EUID -eq 0 ]]; then bash "${script_dir:-/docker/install}/scripts/system/$name" "$@" else sudo "$helper" "$@" fi } # Ownership reconcile: action ∈ {reconcile [mode]|traversal|containers-top| # app-perms|webui|taskdir|app-data-nobody } runOwnership() { _runRootHelper libreportal-ownership "$@"; } # /etc/resolv.conf edits: {clear|add } runResolv() { _runRootHelper libreportal-dns "$@"; } # Host SSH access (authorized_keys + sshd PasswordAuthentication): # {ensure-dir|key-count|pw-status|has-keys|read-keys|authkeys-path| # key-add |key-remove |pw-set } runSshAccess() { _runRootHelper libreportal-ssh-access "$@"; } # Docker-socket read perms for the type switcher: {rootless|rooted} {on|off} # (exit 3 = socket absent). runSocket() { _runRootHelper libreportal-socket "$@"; } # Install/refresh the systemd task-processor unit (root generates the unit from # config; no caller-supplied content): {install|enable|restart|start|status} runSvc() { _runRootHelper libreportal-svc "$@"; } # Genuine system-administration command (ufw/systemctl/apt/sysctl/useradd, /etc # edits). Needs real root in both modes; funnelled through one place so it can # later be confined to a scoped sudoers allowlist. runSystem() { sudo "$@" }