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>
Disabling userland-proxy makes rootless dockerd require br_netfilter
(/proc/sys/net/bridge/bridge-nf-call-iptables), absent in the rootless
netns on Debian -> default bridge creation fails -> daemon won't start.
Drop the daemon.json userland-proxy=false write. Source-IP is preserved
at L7 by Traefik (X-Forwarded-For), so no real loss.
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>
FIX: dockerCommandRun rooted path is 'sudo $command' (unquoted word-split),
so 'docker ps --format "{{.Names}}"' was passing the format with LITERAL
quotes -> docker emitted '<name>' and the downstream grep never matched
(broken in rooted too). Switch all docker invocations to runFileOp, which
preserves args via "$@" in both modes (and runs as dockerinstall against
the rootless socket). Fixed monitoring.sh, dashy, tags_processor_network_mode.
Convert: jitsimeet (rm/wget/unzip/mv/sed/tee/gen-passwords on /docker ->
runFileOp/runFileWrite), authelia (config sed/mkdir/chmod/chown/secrets tee
-> runFileOp/runFileWrite; docker exec -> runFileOp docker, preserving
--password), reset_git (cp->/root runSystem, install-dir chown runInstallOp;
kept sudo -u manager). check_update/update_git_check need no change (all
sudo -u manager git, already least-privilege).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- restic_install, crowdsec_update/verify_firewall/fix_priority: pure host
ops (apt/cscli/nft/systemctl, /etc/crowdsec) -> runSystem.
- kopia_backup/borg_restore: ignore-file/target tee+chown+mkdir -> runFileOp/
runFileWrite; kept the 'sudo -E -u dockerinstall' engine calls as-is —
those already run as the unprivileged backup user (least-privilege; the
scoped sudoers will permit (dockerinstall)).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
firewall_initial_setup + firewall_clear_rules (ufw/ufw-docker),
host_access.sh (sshd/-T/-t, /etc/ssh, authorized_keys, systemctl reload),
set_socket_permissions (docker socket test/chmod), and webui_install_systemd
(systemd unit tee + systemctl) -> runSystem. These stay real-root in both
modes and define part of the eventual scoped allowlist. Left the
'sudo -u <manager> crontab' run-as-manager lines for a dedicated pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
- New runInstallOp helper for manager install-dir/template ops (rooted:
sudo; rootless: run as the current manager user, which owns the tree).
- adguard.sh, traefik.sh: container-config sed -> runFileOp.
- crowdsec.sh: host crowdsec systemctl/apt-get -> runSystem.
- dashy_update_conf.sh: conf-file mkdir/chown/md5sum/tee -> runFileOp/
runFileWrite; docker ps/restart -> dockerCommandRun.
Deferred (cross-owner copy / temp-file across /tmp<->/docker, need rootless
env to bridge correctly): owncloud_setup_config.sh, adguard_auth.sh.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
All operate on /docker data-plane (DB at $docker_dir, compose files,
task dir /docker/.../frontend/data/tasks): sqlite3/find/sed/mkdir/chmod/
chown/mv/rm/mkfifo/truncate/install/tee -> runFileOp/runFileWrite. The
two systemctl enable/start calls in the check processor -> runSystem.
Dropped spurious sudo on text-only echo/grep/date in db_app_scan.
Byte-identical in rooted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Add -a/--append to runFileWrite so the pervasive /docker/logs log-append
idiom (`… | sudo tee -a $logs_dir/$docker_log_file`) routes through the
mode-aware helper instead of raw sudo.
Convert scripts/config/docker/docker_config_to_container.sh fully: all
ops target /docker app config + logs (data-plane), so md5sum/grep/chmod/
cmp/editor -> runFileOp and the log-appends -> runFileWrite -a.
Byte-identical in rooted.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Give dockerCommandRunInstallUser an --argv mode that execs arguments
verbatim (sudo -u <user> env ... "$@") instead of bash -c "$*", and
point runFileOp at it. The old $*+bash -c re-parse silently mangled
backslashes/quotes in args — e.g. sed scripts (\1, \( become 1, ( ) and
the sqlite3 .backup arg — so rootless data-plane ops with regex were
broken. Verified: the WG_DEFAULT_DNS sed now applies correctly as the
install user. All existing runFileOp callers pass plain commands, so the
switch is safe (and fixes the latent sqlite3 case).
Convert scripts/network/dns/setup_dns.sh: /etc/resolv.conf edits and
ping -> runSystem; the WG_DEFAULT_DNS compose-file sed -> runFileOp.
Byte-identical in rooted; correct in rootless.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Enabling unprivileged user namespaces for rootless widens the kernel
attack surface reachable by unprivileged users (a known source of LPE
CVEs). Pair it with three distro-portable, low-impact sysctls that close
the surfaces those exploit chains rely on: kernel.kptr_restrict=2 (hide
kernel pointers), kernel.yama.ptrace_scope=1 (block cross-process
ptrace), net.core.bpf_jit_harden=2 (harden the JIT). Added as a separate
guarded LIBREPORTAL KERNEL HARDENING block so it's clearly deliberate and
independently idempotent.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Split the rootless network-driver knob out of network_docker into a
dedicated advanced subcategory (configs/network/network_rootless), marked
**ADVANCED** at both the file-header and field level so it's hidden behind
the advanced toggle in the WebUI. Registered in the network .category
order after network_docker. Pure config relocation — the install script
reads CFG_ROOTLESS_NET as a sourced var, so no script change needed.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Switch the rootless network stack from slirp4netns+builtin to pasta+
implicit (faster and propagates the real client source IP). The earlier
pasta+builtin attempt bricked the daemon because rootlesskit rejects
mismatched net/port-driver pairs; expose a single CFG_ROOTLESS_NET knob
(pasta default, slirp4netns fallback) and derive the matching port
driver in-script so an invalid combo can't be configured. Disable
userland-proxy in the rootless daemon.json (merged, not clobbered) so
containers see the real source IP. Both driver binaries are always
installed, so switching is a config flip + rootless re-setup.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
In rootless the container was set to user: <host-uid> (e.g. 1002:1002) with
group_add: <host-sub-gid> (e.g. 166528). Inside the daemon's user namespace
those are out of range — group_add made runc call setgroups() with an unmapped
GID ('setgroups: invalid argument'), so the WebUI container never started.
In rootless the container now runs as 0:0 (userns-root == the install user, which
owns the bind-mounts and the rootless socket) with socket gid 0. Rooted is
unchanged. Verified: libreportal-service comes up and talks to the rootless
socket.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The rootless dockerd override forced NET=pasta + PORT_DRIVER=builtin, which
rootlesskit rejects ('pasta requires port driver none or implicit'), so the
daemon failed to start every time (the real cause behind 'rootless socket not
found'). Use slirp4netns + builtin (valid, still skips the userspace
port-handler). Verified: daemon now comes up, docker Server 29.5.2 responds.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
dockerCommandRunInstallUser ssh'd to <user>@localhost, but nothing set up an
SSH server/keys/authorized_keys, so every rootless setup command (daemon
install, systemctl --user) silently no-op'd. Replace with 'sudo -u <user> env
…' that sets XDG_RUNTIME_DIR / DBUS_SESSION_BUS_ADDRESS / DOCKER_HOST / PATH
explicitly; linger keeps the user systemd + /run/user/<uid> alive so
systemctl --user works. No SSH server, no keys, less attack surface, and
sudo -u to an unprivileged user is not a root escalation.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
useradd was missing its login-name argument (and -m), so it failed — silently,
because local result=$(...) swallowed the exit code and checkSuccess reported
success. The rootless install user was therefore never created, which cascaded
into 'invalid user dockerinstall' and a daemon that never came up. Pass the
username + -m (subordinate uid/gid ranges come from login.defs), unmasked.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>