Under Model A the runtime runs as the manager, so establishing the /docker ownership model needs root. Granting the manager a blanket 'sudo chown'/'sudo chmod' in the scoped sudoers would be root-equivalent (chown /etc/sudoers, ...). Introduce a self-contained, root-owned helper that performs only a FIXED set of reconciles on FIXED LibrePortal paths, with owners derived from config + a baked manager name (never the caller) and a strictly-validated app-name argument. - scripts/system/libreportal-ownership: the helper (actions: reconcile, traversal, containers-top, app-perms, webui, taskdir, app-data-nobody) - run_privileged: runOwnership wrapper (sudo the installed helper; run the bundled copy directly when already root mid-install) - init.sh: installOwnershipHelper bakes the manager name and installs it root:root 0755 to /usr/local/sbin (manager can't modify it) - libreportal_folders/app_folder/app_update_specifics/task processor: delegate the ownership chowns to runOwnership instead of runSystem chown This removes chown/chmod-on-/docker from the runtime sudo surface, a prerequisite for a non-root-equivalent scoped sudoers. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
117 lines
4.8 KiB
Bash
117 lines
4.8 KiB
Bash
#!/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" "$@"
|
|
}
|
|
|
|
# Trigger a fixed ownership reconcile through the ROOT-OWNED helper installed at
|
|
# /usr/local/sbin/libreportal-ownership. This is how the manager-run runtime
|
|
# (Model A) establishes the ownership model — manager owns the control plane, the
|
|
# docker install user owns the containers — without the scoped sudoers having to
|
|
# grant a blanket `sudo chown`/`sudo chmod` (which would be root-equivalent: chown
|
|
# /etc/sudoers and so on). The helper validates its own (fixed-path) operations,
|
|
# so the sudoers can allow it wholesale.
|
|
# action ∈ {reconcile [mode]|traversal|containers-top|app-perms|webui|taskdir|
|
|
# app-data-nobody <app>}
|
|
# At install time (already root) the helper may not be installed yet, so run the
|
|
# bundled copy directly — no sudo, no escalation, since we are root already.
|
|
runOwnership() {
|
|
local helper="/usr/local/sbin/libreportal-ownership"
|
|
if [[ -x "$helper" ]]; then
|
|
sudo "$helper" "$@"
|
|
elif [[ $EUID -eq 0 ]]; then
|
|
bash "${script_dir:-/docker/install}/scripts/system/libreportal-ownership" "$@"
|
|
else
|
|
sudo "$helper" "$@"
|
|
fi
|
|
}
|
|
|
|
# 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 "$@"
|
|
}
|