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 <noreply@anthropic.com> Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
parent
d8cad2677d
commit
15fc42c858
59
FOOTPRINT.md
Normal file
59
FOOTPRINT.md
Normal file
@ -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
|
||||||
|
```
|
||||||
37
init.sh
37
init.sh
@ -97,8 +97,11 @@ sshd_config="/etc/ssh/sshd_config"
|
|||||||
sudo_bashrc="/home/$sudo_user_name/.bashrc"
|
sudo_bashrc="/home/$sudo_user_name/.bashrc"
|
||||||
hosts_file="/etc/hosts"
|
hosts_file="/etc/hosts"
|
||||||
hostname_file="/etc/hostname"
|
hostname_file="/etc/hostname"
|
||||||
fqdn_file="/root/libreportal-fqdn.txt"
|
# All LibrePortal executables installed outside /docker live together here
|
||||||
command_script="/usr/local/bin/libreportal"
|
# (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
|
# Directories
|
||||||
docker_dir="/docker"
|
docker_dir="/docker"
|
||||||
@ -699,7 +702,7 @@ initUsers()
|
|||||||
# NOPASSWD: ALL, and never appended to /etc/sudoers — a malformed main file
|
# 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
|
# 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,
|
# 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
|
# (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/
|
# of system binaries. Deliberately excluded: bash/su and tee/cp/chmod/chown/
|
||||||
# sed/mv/rm/install (each root-equivalent). See scripts/system/libreportal-*.
|
# sed/mv/rm/install (each root-equivalent). See scripts/system/libreportal-*.
|
||||||
@ -709,13 +712,13 @@ initUsers()
|
|||||||
sudoers_tmp=$(mktemp)
|
sudoers_tmp=$(mktemp)
|
||||||
cat > "$sudoers_tmp" <<EOF
|
cat > "$sudoers_tmp" <<EOF
|
||||||
# Scoped least-privilege grant for the LibrePortal manager. Generated by init.sh.
|
# Scoped least-privilege grant for the LibrePortal manager. Generated by init.sh.
|
||||||
Cmnd_Alias LP_HELPERS = /usr/local/sbin/libreportal-ownership, \\
|
Cmnd_Alias LP_HELPERS = ${lp_lib_dir}/libreportal-ownership, \\
|
||||||
/usr/local/sbin/libreportal-dns, \\
|
${lp_lib_dir}/libreportal-dns, \\
|
||||||
/usr/local/sbin/libreportal-ssh-access, \\
|
${lp_lib_dir}/libreportal-ssh-access, \\
|
||||||
/usr/local/sbin/libreportal-socket, \\
|
${lp_lib_dir}/libreportal-socket, \\
|
||||||
/usr/local/sbin/libreportal-svc, \\
|
${lp_lib_dir}/libreportal-svc, \\
|
||||||
/usr/local/sbin/libreportal-bininstall, \\
|
${lp_lib_dir}/libreportal-bininstall, \\
|
||||||
/usr/local/sbin/libreportal-appcfg
|
${lp_lib_dir}/libreportal-appcfg
|
||||||
Cmnd_Alias LP_SYSTEM = /usr/bin/systemctl, /usr/sbin/ufw, /usr/local/bin/ufw-docker, \\
|
Cmnd_Alias LP_SYSTEM = /usr/bin/systemctl, /usr/sbin/ufw, /usr/local/bin/ufw-docker, \\
|
||||||
/usr/sbin/nft, /usr/sbin/sysctl, /sbin/sysctl, \\
|
/usr/sbin/nft, /usr/sbin/sysctl, /sbin/sysctl, \\
|
||||||
/usr/bin/loginctl, /usr/sbin/service
|
/usr/bin/loginctl, /usr/sbin/service
|
||||||
@ -753,10 +756,15 @@ EOF
|
|||||||
# no-op on helpers without the placeholder.
|
# no-op on helpers without the placeholder.
|
||||||
initRootHelpers()
|
initRootHelpers()
|
||||||
{
|
{
|
||||||
|
# One root-owned home for every LibrePortal executable installed outside
|
||||||
|
# /docker (the helpers here; the CLI wrapper lands here too, see the command
|
||||||
|
# setup). Root-owned 0755 = the manager can't tamper with the helpers it
|
||||||
|
# sudo's (the trust boundary the scoped sudoers relies on).
|
||||||
|
sudo install -d -m 0755 -o root -g root "$lp_lib_dir"
|
||||||
local helper helper_src helper_dst helper_tmp
|
local helper helper_src helper_dst helper_tmp
|
||||||
for helper in libreportal-ownership libreportal-dns libreportal-ssh-access libreportal-socket libreportal-svc libreportal-bininstall libreportal-appcfg; do
|
for helper in libreportal-ownership libreportal-dns libreportal-ssh-access libreportal-socket libreportal-svc libreportal-bininstall libreportal-appcfg; do
|
||||||
helper_src="$script_dir/scripts/system/$helper"
|
helper_src="$script_dir/scripts/system/$helper"
|
||||||
helper_dst="/usr/local/sbin/$helper"
|
helper_dst="$lp_lib_dir/$helper"
|
||||||
if [[ ! -f "$helper_src" ]]; then
|
if [[ ! -f "$helper_src" ]]; then
|
||||||
isError "Root helper source missing ($helper_src) — skipping."
|
isError "Root helper source missing ($helper_src) — skipping."
|
||||||
continue
|
continue
|
||||||
@ -898,6 +906,9 @@ initLibrePortalCommand()
|
|||||||
fi
|
fi
|
||||||
isNotice "Custom command 'libreportal' is not installed. Installing..."
|
isNotice "Custom command 'libreportal' is not installed. Installing..."
|
||||||
|
|
||||||
|
# The CLI wrapper lives alongside the root helpers in $lp_lib_dir; a symlink
|
||||||
|
# in /usr/local/bin keeps `libreportal` on $PATH.
|
||||||
|
sudo install -d -m 0755 -o root -g root "$lp_lib_dir"
|
||||||
sudo rm -rf $command_script
|
sudo rm -rf $command_script
|
||||||
sudo tee -a "$command_script" >/dev/null <<'EOF'
|
sudo tee -a "$command_script" >/dev/null <<'EOF'
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
@ -1186,7 +1197,9 @@ fi
|
|||||||
# LibrePortal Command End
|
# LibrePortal Command End
|
||||||
EOF
|
EOF
|
||||||
sudo chmod +x $command_script
|
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
|
source $sudo_bashrc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -87,17 +87,17 @@ runBackupOp() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Run one of the ROOT-OWNED LibrePortal helpers installed (root:root 0755) under
|
# 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)
|
# /usr/local/lib/libreportal/ by init.sh. These are how the manager-run runtime
|
||||||
# performs the genuine-root operations it can't drop — establishing the /docker
|
# (Model A) performs the genuine-root operations it can't drop — establishing the
|
||||||
# ownership model, editing /etc/resolv.conf, managing host SSH access — WITHOUT
|
# /docker ownership model, editing /etc/resolv.conf, managing host SSH access —
|
||||||
# the scoped sudoers granting blanket `sudo chown/chmod/tee/sed/cp` (which would
|
# WITHOUT the scoped sudoers granting blanket `sudo chown/chmod/tee/sed/cp` (which
|
||||||
# be root-equivalent: chown /etc/sudoers, tee a new sudoers drop-in, …). Each
|
# would be root-equivalent: chown /etc/sudoers, tee a new sudoers drop-in, …).
|
||||||
# helper validates its own fixed-path operations, so the sudoers can allow it
|
# 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
|
# 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.
|
# run the bundled copy directly — no sudo, no escalation, since we are root.
|
||||||
_runRootHelper() {
|
_runRootHelper() {
|
||||||
local name="$1"; shift
|
local name="$1"; shift
|
||||||
local helper="/usr/local/sbin/$name"
|
local helper="/usr/local/lib/libreportal/$name"
|
||||||
if [[ -x "$helper" ]]; then
|
if [[ -x "$helper" ]]; then
|
||||||
sudo "$helper" "$@"
|
sudo "$helper" "$@"
|
||||||
elif [[ $EUID -eq 0 ]]; then
|
elif [[ $EUID -eq 0 ]]; then
|
||||||
|
|||||||
@ -40,7 +40,11 @@ default_subnet="10.100.0"
|
|||||||
# Files
|
# Files
|
||||||
docker_rooted_socket="/var/run/docker.sock"
|
docker_rooted_socket="/var/run/docker.sock"
|
||||||
swap_file=/swapfile
|
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
|
docker_log_file=libreportal.log
|
||||||
backup_log_file=backup.log
|
backup_log_file=backup.log
|
||||||
db_file=database.db
|
db_file=database.db
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user