Two pre-existing bugs a genuinely-clean rootless install exposes:
copyFolder picked the copy user by destination only: a manager-owned
source (e.g. the install dir) copied into the dockerinstall-owned
containers/ ran the cp AS dockerinstall, which can't read the source ->
"cp: Permission denied". The `local result=$(...)` then masked the
failure (local returns 0) so checkSuccess printed success. This broke
installLibrePortalImageWebUI: the WebUI dir wasn't populated, so
initializeAppVariables couldn't read libreportal.config ("No app name
provided"), compose tags were never substituted, and the WebUI container
couldn't start (user: "USER_DATA"). Fix: when source and destination
owners differ (manager -> container), bridge with a tar pipe — the
manager reads, dockerinstall writes — with pipefail so a read-side
failure is no longer masked.
start.sh created the per-run install log with `sudo touch` (root:root
644) but tee's to it as the manager -> "tee: Permission denied" -> every
install-*.log was empty. Fix: chown the log to the user running the
install so the tee can append.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The scoped sudoers grants the manager (root) and (dockerinstall) but NOT
(itself), so the many 'sudo -u $sudo_user_name <cmd>' calls (crontab,
git/update, reinstall, swapfile, …) failed with 'a password is required'
once per CLI command. runAsManager runs the command plainly when already
the manager (the runtime case) and only sudo -u's when root (install
time), so it's correct in both contexts and needs no sudoers self-grant.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Move the last runtime-critical root file-primitive subsystems behind
root-owned helpers so the type switcher + task service work under a scoped
sudoers:
- scripts/system/libreportal-socket: {rootless|rooted} {on|off} chmod of
the docker sockets (paths computed from config, not caller-supplied;
exit 3 = absent so the *_found flags come from its exit code)
- scripts/system/libreportal-svc: GENERATES + installs the systemd unit
from config (mode/uid/baked manager) — never accepts unit content from
the caller (arbitrary unit = root). Idempotent install/enable/restart.
- ownership helper: add db-own + app-file <app> <relpath> actions
- run_privileged: runSocket / runSvc
- set_socket_permissions -> runSocket; webui_install_systemd -> runSvc
(+ crontab cleanup runs as the manager directly, no sudo -u self)
- before_start: db chown -> runOwnership db-own; traefik cert/yml ->
runOwnership app-file (retires updateFileOwnership/changeRootOwnedFile)
- init.sh installs all five helpers
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
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>
- copy_build_context: rsync/cp/rm -> runFileOp (writes the deployed tree AS the
container owner with --no-owner); drop the now-redundant runSystem chown.
- setup_lock: .setup_complete is in the docker-install-owned frontend/data ->
runFileOp touch/chmod/rm (drop the chown).
- tags_processor_docker_installation 'user:' enable + update_compose_yml
jail.local -> runFileOp (deployed compose/config under containers).
- crontab_clear: clear the manager's own crontab via runInstallOp.
- reinstall: cp init.sh to /root -> runSystem (genuine root path).
- create_successful_run_file: drop the pointless sudo echo -> runInstallWrite to
/docker/run.txt.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The old copy/move helpers ran 'sudo cp/mv X Y; sudo chown $user_name Y' (root +
arbitrary chown). Rework them to write AS the destination's owner — no root, no
chown — classifying by dest path like createTouch: /docker/containers/<app> ->
runFileOp (docker install user), manager-owned control plane -> runInstallOp.
The $user_name arg is now advisory (the path decides). Covers copyFile/copyFiles/
copyFolder/copyFolders/moveFile; copyResource is always containers -> runFileOp;
createFolders' non-container branch -> runInstallOp; updateFileOwnership (an
arbitrary user1:user2 chown) -> runSystem. Confirmed by callers (containers vs
$docker_dir/backup_install_dir/configs dests). Removes a class of root data ops
+ arbitrary-chown from the runtime.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The backup + ssh generators created their frontend/data dirs via plain/sudo
mkdir and wrote files via sudo tee/mv (root-owned), then called createTouch
(dockerinstall) which can't re-own a root file — so every write hit
'touch: Permission denied' in rootless and left root-owned data the
dockerinstall container/generators can't rewrite. Convert dir creation to
runFileOp mkdir and file writes to runFileWrite (both run as the container
owner: dockerinstall in rootless, manager in rooted), dropping the
temp/mv/createTouch dance. Also make the createFolders chokepoint mode-aware
(containers/ paths created via runFileOp) so it mirrors createTouch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The install/start paths and the switch reconcile managed /docker ownership
separately, so a fresh install produced different ownership than a post-switch
state — the root cause of the rootless 'touch: Permission denied' storm.
Consolidate onto the reconcile model:
- dockerContainerOwner(): single definition of the mode's container owner
(rooted -> manager, rootless -> config-authoritative docker install user).
- reconcileContainersTopOwnership(): owns + makes traversable the structural
containers/ top dir; now also run by the switch reconcile (previously only
the install pass set it, so a rootless->rooted switch left it stale).
- reconcileWebuiDirOwnership(): now uses dockerContainerOwner.
- reconcileDockerOwnership(): calls both helpers.
- fixFolderPermissions(): slimmed to the +x traversal bits; its ad-hoc
containers/ chown is now the shared helper.
- fixPermissionsBeforeStart(): drop changeRootOwnedFilesAndFolders (a
pre-de-sudo band-aid that only fixed root-owned files and ran contrary to
the don't-touch-third-party-data rule); reconcile the WebUI dir via the
shared helper instead. Delete the now-unused root_files_folders.sh and
regenerate the source arrays.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
A fresh rootless install left /docker/containers/libreportal/frontend owned
by the manager (webui_install_image chowned -R to $sudo_user_name) while the
WebUI container and the host-side runFileOp generators run as dockerinstall.
So every generator touch under frontend/data and frontend/logs failed with
'Permission denied' (~27 in the install log). reconcileDockerOwnership chowns
the WebUI dir to the mode's container owner, but only runs on a mode switch,
not on a fresh install.
Extract that WebUI-dir chown into reconcileWebuiDirOwnership (rooted ->
manager, rootless -> the config-authoritative docker install user; runs as
root so it can chown either way) and call it from both reconcileDockerOwnership
and the fresh-install WebUI setup. A fresh install now lands the same
ownership a switch does, so the dockerinstall generators can write.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The control-plane/app-install-user detail was noise on the success line;
keep it concise as 'Reconciled ownership for <mode>'.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
ROOT CAUSE of the WebUI-dir misownership on rooted->rootless:
check_install_type.sh sets the lowercase $docker_install_user to the MANAGER
user in rooted mode (it's a mode-dependent 'container owner' var). reconcile
trusted it, so mid-switch it held the stale rooted value (=manager) and chowned
the rootless WebUI dir to libreportal -> WebUI Exited(137) -> dockerStartAllApps
retried forever (the 'switch hangs' symptom). Now read CFG_DOCKER_INSTALL_USER
straight from the live config file (authoritative, never polluted), falling back
to the CFG var then a hard default.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Previous fix still no-op'd the WebUI-dir chown: in the CLI/switch context the
path globals (containers_dir etc.) and the install-user vars can be unset,
making webui_dir a relative path the [[ -d ]] check skips, and the chown user
empty. Resolve everything with absolute-path fallbacks and read the install
user from the live config file when the vars are empty (never empty now), and
log what was reconciled (incl. a 'WebUI dir not found' notice) so a switch is
diagnosable.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Bug found via round-trip: after rooted->rootless the WebUI dir stayed
libreportal instead of dockerinstall, so the rootless WebUI Exited(137).
Cause: reconcile referenced $docker_install_user, which is unset in the
CLI/switch context (only $CFG_DOCKER_INSTALL_USER is, like the rootless
helper uses) -> chown to an empty user no-op'd. Use
${docker_install_user:-$CFG_DOCKER_INSTALL_USER} (and ${sudo_user_name:-libreportal})
so reconcile resolves the users reliably in any context.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
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 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Correction from the maintainer: /docker was always libreportal:libreportal;
root:root only ever appeared as an artifact of un-de-sudo'd sudo commands, not
by design. reconcileDockerOwnership now always assigns the control plane to the
manager user regardless of mode (was wrongly root:root for rooted). The deeper
implication — that the de-sudo helpers' rooted=sudo path also re-creates
root-owned files — is being confirmed before realigning.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Round-trip test exposed it: during a rooted stint the WebUI (root-in-
container) writes root-owned files into its data dir; back in rootless the
WebUI user (dockerinstall) can't manage them -> container Exited(137).
Since the WebUI is LibrePortal's OWN regenerable 0:0 component, reconcile now
also chowns containers/libreportal to the mode's container owner (root rooted
/ install user rootless). Validated: after this the WebUI returns to HTTP 200.
Third-party app data under containers/ is still untouched (backup/restore).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Mode switches change /docker ownership expectations, but the switcher only
ever fixed the socket — never file ownership — so a rooted<->rootless swap
left the control plane owned for the wrong mode (CLI + de-sudo helpers then
can't access it).
Add reconcileDockerOwnership (single source of truth): swaps ONLY the owner
of LibrePortal's control plane (configs/logs/scripts/DB + /docker top) to the
mode owner (root rooted / manager rootless). It never resets mode bits (only
adds o+x on /docker for traversal and o+r on the DB for the WebUI), and never
touches /docker/containers/** app data, backups/, or ssl/ssh keys. Wired into
both switch branches between container-retag and app-start.
App data is deliberately NOT chowned: container UIDs re-map across modes
(rootless subuid offset), so a chown can't carry e.g. Postgres data across —
that's a backup->switch->restore operation. Switcher now warns to back up
stateful apps before switching and restore after.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Per the confirmed ownership model: files under /docker/containers/<app>/ are
app data owned by the docker install user; everything else is the manager-
owned control plane. createTouch now picks runFileOp vs runInstallOp by the
file's location and creates it directly as the right owner — no more
chown-to-another-user (which needs root the unprivileged runtime lacks).
The $2 user hint is now advisory. (Generator content-writes into
frontend/data still need converting to runFileWrite — next.)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Reinstall test on Debian 12 surfaced three rootless-only breakages (rooted
was byte-identical/fine):
1. pasta blocked by Debian's passt AppArmor profile (DENIED ptrace read ->
can't open container netns -> rootless dockerd never starts). Default
CFG_ROOTLESS_NET back to slirp4netns (reliable); pasta stays selectable
for hosts that relax the profile.
2. de-sudo mis-assigned helpers by owner. /docker management layer (apps DB
chowned to libreportal by install_sqlite, /docker/logs) is MANAGER-owned,
not dockerinstall. Add runInstallWrite; move apps-DB sqlite3 -> runInstallOp
and /docker/logs appends -> runInstallWrite. Revert ownership-SETUP scripts
(libreportal_folders, app_folder) to runSystem — they must run as root to
establish ownership during install. Container files (/docker/containers/<app>)
stay runFileOp.
3. kernel hardening sysctls written to /etc/sysctl/99-custom.conf, which
'sysctl --system' does not read -> never applied. Write them to
/etc/sysctl.d/99-libreportal-hardening.conf instead.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The WebUI data snapshots (locations.json, dashboard.json, snapshots_*.json,
etc.) are regenerated on every wizard/config change. Each file emitted two
extra success lines via createTouch — "Touching <file>" and "Updating
<file> with <user> ownership" — which spammed the output around the genuinely
useful "... JSON regenerated" line.
Add an optional "silent" flag to createTouch (third arg; default keeps the
existing loud behaviour for interactive install flows) and pass it from every
WebUI data generator/task. Touch + chown still run; only the logging is
suppressed for these background regenerations.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Propagate the ✓ Success / ✗ Error / ! Notice / ❯ Question glyphs (from markers.sh) through the rest of the pipeline: swap the inlined helpers in init.sh and generate_arrays.sh, and replace raw echo -e "${RED}ERROR:${NC}" calls with the isX helpers in config_check_missing.sh, check_success.sh, initilize_files.sh, and reset_git.sh.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
A free, open, self-hosted app platform (GNU AGPLv3): one-click app deploys,
Traefik reverse proxy with automatic SSL, rootless Docker support, gluetun
VPN routing, and a web dashboard to manage it all.
Free & open forever to self-host; optional paid hosted services fund it.
See PROMISE.md.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>