From 10af56b9c4768cee916774e052604ffbfd65fc8d Mon Sep 17 00:00:00 2001 From: librelad Date: Sun, 24 May 2026 01:40:58 +0100 Subject: [PATCH] refactor(desudo): rooted ops run as the manager user, not sudo->root Maintainer confirmed the intended model: the manager user (libreportal, in the docker group) owns /docker in BOTH modes and runs things directly; root:root was always an accident of un-de-sudo'd sudo. Rework the helpers accordingly: - add runAsManager (run as the manager: plain when already it at runtime, else sudo -u at install time) so files end up manager-owned, never root-owned. - runFileOp/runFileWrite: rooted -> runAsManager (was sudo->root); rootless unchanged (docker install user owns containers/). - runInstallOp/runInstallWrite: always runAsManager (control plane is manager- owned in both modes). - runSystem unchanged (genuine root: apt/systemctl/ufw/sysctl). All ~40 converted call sites inherit this via the helpers. reconcile's WebUI dir now -> manager in rooted / docker install user in rootless. Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- scripts/docker/command/run_privileged.sh | 107 +++++++++--------- .../permission/libreportal_folders.sh | 13 ++- 2 files changed, 59 insertions(+), 61 deletions(-) diff --git a/scripts/docker/command/run_privileged.sh b/scripts/docker/command/run_privileged.sh index 5b4836b..787de61 100644 --- a/scripts/docker/command/run_privileged.sh +++ b/scripts/docker/command/run_privileged.sh @@ -2,87 +2,84 @@ # Mode-aware privileged operations. # -# The privilege a file operation needs depends on the Docker mode: -# rooted — app/container files under /docker are root-owned, so ops run via -# sudo. This is byte-for-byte the historical behaviour. -# rootless — those files are owned by the unprivileged Docker install user, so -# ops run AS that user (via `sudo -u`, no root over the data plane). -# Centralising the branch here means each call site is written once and is -# correct in both modes, and rooted installs (incl. live boxes) are unchanged. +# 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 /docker data-plane command — mkdir/chown/rm/cp/mv/find/sqlite3/etc. on -# app or container files. -# rooted -> sudo -# rootless -> run as the Docker install user (no sudo) -# Note: for stdin-fed writes (e.g. `… | sudo tee file`) use runFileWrite below; -# this helper is for self-contained commands. +# 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 - sudo "$@" + runAsManager "$@" fi } -# Write stdin to a path with the right privilege (replaces `… | sudo tee path`). -# rooted -> sudo tee -# rootless -> tee as the Docker install user -# Pass -a/--append as the first arg to append instead of truncate (replaces -# `… | sudo tee -a path`, e.g. the /docker/logs log-append idiom). +# 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="" + local append_flag=() if [[ "$1" == "-a" || "$1" == "--append" ]]; then - append=" -a" + append_flag=(-a) shift fi local dest="$1" if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then - dockerCommandRunInstallUser "tee$append '$dest' >/dev/null" + dockerCommandRunInstallUser "tee ${append_flag[*]} '$dest' >/dev/null" else - sudo tee$append "$dest" >/dev/null + runAsManager tee "${append_flag[@]}" "$dest" >/dev/null fi } -# Genuine system-administration command (ufw/systemctl/apt/sysctl/useradd, /etc -# edits). Needs real root in both modes; kept as sudo and funnelled through one -# place so it can later be confined to a scoped sudoers allowlist. -runSystem() { - sudo "$@" -} - -# Op on a MANAGER-owned path — the LibrePortal clone + shipped templates AND the -# /docker management layer the runtime owns (apps DB, configs/, logs/, scripts). -# In rootless these are owned by the manager user; ops on them need no privilege. -# rooted -> sudo (root-owned; byte-identical) -# rootless -> (runs as the current user: root at install-time, -# the manager user at runtime — both can access) -# Container-owned data under /docker/containers// is NOT this — use runFileOp. -# For copies that read manager files and write container-owned ones, read here -# and pipe into runFileWrite so each side runs as the correct owner. +# 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() { - if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then - "$@" - else - sudo "$@" - fi + runAsManager "$@" } # Write stdin to a MANAGER-owned path (apps DB sidecars, configs/, logs/ — e.g. -# the /docker/logs log-append idiom). Mirror of runFileWrite for the manager -# owner. Pass -a/--append as the first arg to append. -# rooted -> sudo tee -# rootless -> tee as the current (manager) user +# the /docker/logs log-append idiom). Manager-owned in both modes. +# Pass -a/--append as the first arg to append. runInstallWrite() { - local append="" + local append_flag=() if [[ "$1" == "-a" || "$1" == "--append" ]]; then - append=" -a" + append_flag=(-a) shift fi local dest="$1" - if [[ "$CFG_DOCKER_INSTALL_TYPE" == "rootless" ]]; then - tee$append "$dest" >/dev/null - else - sudo tee$append "$dest" >/dev/null - fi + runAsManager tee "${append_flag[@]}" "$dest" >/dev/null +} + +# 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 "$@" } diff --git a/scripts/function/permission/libreportal_folders.sh b/scripts/function/permission/libreportal_folders.sh index ad8e586..44df614 100755 --- a/scripts/function/permission/libreportal_folders.sh +++ b/scripts/function/permission/libreportal_folders.sh @@ -43,14 +43,15 @@ reconcileDockerOwnership() done [[ -f "$docker_dir/$db_file" ]] && runSystem chmod o+r "$docker_dir/$db_file" - # LibrePortal's OWN WebUI container dir is regenerable and runs as 0:0 - # (root-in-container -> root rooted / install user rootless). Flip it to the - # mode's container owner so the WebUI survives a switch; it's safe to recurse - # because it's all one UID (no per-app uid to clobber). Third-party app data - # elsewhere under containers/ is still left untouched. + # LibrePortal's OWN WebUI container dir is regenerable, so flip it to the + # mode's container owner so the WebUI survives a switch (safe to recurse — + # it's all one UID, no per-app uid to clobber). Third-party app data + # elsewhere under containers/ is left untouched. + # rooted -> the manager (it owns everything under /docker in rooted) + # rootless -> the docker install user (owns /docker/containers/**) local webui_dir="${containers_dir}libreportal" if [[ -d "$webui_dir" ]]; then - local app_owner="root" + local app_owner="$owner" [[ "$mode" == "rootless" ]] && app_owner="$docker_install_user" runSystem chown -R "$app_owner:$app_owner" "$webui_dir" fi