From 15fc42c858bb2d1c6e6a525cf79f565077e06943 Mon Sep 17 00:00:00 2001 From: librelad Date: Sun, 24 May 2026 19:40:35 +0100 Subject: [PATCH] refactor(layout): consolidate out-of-/docker files + fix sysctl dir Organise the system footprint outside /docker: - All LibrePortal executables now live together in /usr/local/lib/libreportal/ (root:root): the 7 root helpers AND the CLI wrapper. /usr/local/bin/libreportal becomes a symlink onto $PATH. run_privileged._runRootHelper, init.sh (initRootHelpers + scoped-sudoers Cmnd_Alias + command setup) all point there. The wrapper is now root-owned too (manager can't tamper with its entrypoint). - Fix a real bug: rootless sysctl settings were written to /etc/sysctl/99-custom.conf, a dir does NOT read, so net.ipv4.ip_unprivileged_port_start / kernel.unprivileged_userns_clone never persisted across reboot. Moved to /etc/sysctl.d/99-libreportal-rootless.conf (the existing reload now actually applies them). Consistent libreportal* naming. - Drop dead fqdn_file=/root/libreportal-fqdn.txt global (never used). - Add FOOTPRINT.md: a manifest of every file LibrePortal places outside /docker (doubles as an uninstall checklist). Co-Authored-By: Claude Opus 4.7 Signed-off-by: librelad --- FOOTPRINT.md | 59 ++++++++++++++++++++++++ init.sh | 37 ++++++++++----- scripts/docker/command/run_privileged.sh | 14 +++--- variables.sh | 6 ++- 4 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 FOOTPRINT.md diff --git a/FOOTPRINT.md b/FOOTPRINT.md new file mode 100644 index 0000000..da41065 --- /dev/null +++ b/FOOTPRINT.md @@ -0,0 +1,59 @@ +# LibrePortal system footprint (outside `/docker`) + +Everything LibrePortal *is* lives under `/docker` (app data, configs, the install +tree, the database). This file catalogues the few things it must place **outside** +`/docker` to integrate with the host. The OS dictates where most of these live — +sudoers, systemd units, sysctl, and `$PATH` entries can only be read from their +fixed locations, so they can't all sit in one folder. What we control, we keep +together in **`/usr/local/lib/libreportal/`**; everything else is named +`libreportal*` so the whole footprint is greppable and removable. + +## Executables — `/usr/local/lib/libreportal/` (root:root, our own dir) + +| File | Owner | Purpose | +|------|-------|---------| +| `libreportal` | root | the CLI wrapper (symlinked onto `$PATH`, see below) | +| `libreportal-ownership` | root | reconcile the `/docker` ownership model | +| `libreportal-dns` | root | edit `/etc/resolv.conf` (nameservers) | +| `libreportal-ssh-access` | root | manage admin `authorized_keys` + sshd `PasswordAuthentication` | +| `libreportal-socket` | root | docker-socket read perms (type switcher) | +| `libreportal-svc` | root | generate/install the task-processor systemd unit | +| `libreportal-bininstall` | root | install the restic/kopia backup-engine binaries | +| `libreportal-appcfg` | root | rewrite AdGuard/CrowdSec/ownCloud config files | + +These are the scoped-sudoers trust boundary: root-owned in a root-owned dir, so the +manager can `sudo` them but can't modify them. Source of truth: `scripts/system/` +in the repo; installed by `init.sh` → `initRootHelpers` (re-installed only on a +reinstall, not by the quick-deploy). + +## OS-mandated locations (must live where the OS reads them) + +| Path | Owner | Purpose | +|------|-------|---------| +| `/usr/local/bin/libreportal` | root | **symlink** → `/usr/local/lib/libreportal/libreportal` (puts the CLI on `$PATH`) | +| `/etc/sudoers.d/libreportal` | root | scoped least-privilege grant for the manager | +| `/etc/systemd/system/libreportal.service` | root | the task-processor service (`User=libreportal`) | +| `/etc/sysctl.d/99-libreportal-hardening.conf` | root | kernel LPE-surface hardening | +| `/etc/sysctl.d/99-libreportal-rootless.conf` | root | rootless sysctl settings + "rootless configured" marker | + +## Third-party tools we install (not ours, conventional home) + +`/usr/local/bin/{restic,kopia,ufw-docker,docker-compose}` — installed on demand +(restic/kopia via the `libreportal-bininstall` helper). `/usr/local/bin` is the +correct home for these; left under their own names. + +## System users + +`libreportal` (the manager) and `dockerinstall` (the rootless docker user), each +with a home under `/home/`. The rootless daemon config lives at +`~dockerinstall/.config/docker/daemon.json`. + +## Uninstall sketch + +``` +sudo systemctl disable --now libreportal.service +sudo rm -f /etc/systemd/system/libreportal.service /etc/sudoers.d/libreportal +sudo rm -f /etc/sysctl.d/99-libreportal-*.conf +sudo rm -rf /usr/local/lib/libreportal /usr/local/bin/libreportal +# optional: the backup-engine binaries, the users, and /docker itself +``` diff --git a/init.sh b/init.sh index f7b779d..b0fa564 100755 --- a/init.sh +++ b/init.sh @@ -97,8 +97,11 @@ sshd_config="/etc/ssh/sshd_config" sudo_bashrc="/home/$sudo_user_name/.bashrc" hosts_file="/etc/hosts" hostname_file="/etc/hostname" -fqdn_file="/root/libreportal-fqdn.txt" -command_script="/usr/local/bin/libreportal" +# All LibrePortal executables installed outside /docker live together here +# (root-owned). The user-facing CLI is symlinked into $PATH from /usr/local/bin. +lp_lib_dir="/usr/local/lib/libreportal" +command_script="$lp_lib_dir/libreportal" +command_symlink="/usr/local/bin/libreportal" # Directories docker_dir="/docker" @@ -699,7 +702,7 @@ initUsers() # NOPASSWD: ALL, and never appended to /etc/sudoers — a malformed main file # locks out sudo entirely). Under Model A the runtime runs AS this user and # reaches root ONLY via: the unprivileged docker-install user (data plane, - # confined by rootless), the root-owned /usr/local/sbin/libreportal-* helpers + # confined by rootless), the root-owned /usr/local/lib/libreportal/ helpers # (each a fixed, self-validated op the manager can't modify), and a fixed set # of system binaries. Deliberately excluded: bash/su and tee/cp/chmod/chown/ # sed/mv/rm/install (each root-equivalent). See scripts/system/libreportal-*. @@ -709,13 +712,13 @@ initUsers() sudoers_tmp=$(mktemp) cat > "$sudoers_tmp" </dev/null <<'EOF' #!/usr/bin/env bash @@ -1186,7 +1197,9 @@ fi # LibrePortal Command End EOF sudo chmod +x $command_script - sudo chown $sudo_user_name:$sudo_user_name $command_script + sudo chown root:root $command_script + # Put it on $PATH via a symlink (replaces any older real file at this path). + sudo ln -sfn "$command_script" "$command_symlink" source $sudo_bashrc } diff --git a/scripts/docker/command/run_privileged.sh b/scripts/docker/command/run_privileged.sh index 81d18d4..b8408f7 100644 --- a/scripts/docker/command/run_privileged.sh +++ b/scripts/docker/command/run_privileged.sh @@ -87,17 +87,17 @@ runBackupOp() { } # 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 +# /usr/local/lib/libreportal/ 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" + local helper="/usr/local/lib/libreportal/$name" if [[ -x "$helper" ]]; then sudo "$helper" "$@" elif [[ $EUID -eq 0 ]]; then diff --git a/variables.sh b/variables.sh index d43f135..c056384 100755 --- a/variables.sh +++ b/variables.sh @@ -40,7 +40,11 @@ default_subnet="10.100.0" # Files docker_rooted_socket="/var/run/docker.sock" swap_file=/swapfile -sysctl="/etc/sysctl/99-custom.conf" +# Rootless sysctl settings + the "rootless configured" marker. MUST live under +# /etc/sysctl.d/ — `sysctl --system` only reads there (+ /etc/sysctl.conf), NOT +# the old non-standard /etc/sysctl/ path, so settings written elsewhere never +# persist across reboot. +sysctl="/etc/sysctl.d/99-libreportal-rootless.conf" docker_log_file=libreportal.log backup_log_file=backup.log db_file=database.db