16 Commits

Author SHA1 Message Date
librelad
1dc915f642 feat(switcher): reconcileDockerOwnership — safe owner-only control-plane reconcile on mode switch
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>
2026-05-24 01:16:49 +01:00
librelad
affd96fb42 fix(rootless): don't disable userland-proxy (breaks rootless bridge on Debian)
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>
2026-05-24 00:23:37 +01:00
librelad
68110d199c fix(rootless): slirp4netns default, manager-vs-container helper split, sysctl path
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>
2026-05-24 00:12:06 +01:00
librelad
d7c0d12314 harden(desudo): funnel firewall/ssh/socket/systemd system ops through runSystem
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>
2026-05-23 23:46:58 +01:00
librelad
0c719b5912 harden(desudo): add runInstallOp helper + convert adguard/traefik/crowdsec/dashy
- 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>
2026-05-23 23:45:42 +01:00
librelad
a8248ccf7f harden(desudo): convert monitoring subsystem + global log-append idiom
- Global uniform pass: the $logs_dir/$docker_log_file log-append idiom
  (always /docker/logs, data-plane) -> runFileWrite -a across runtime
  files (check_success.sh logging backbone + several app scripts).
- monitoring.sh fully converted: containers_dir/docker_dir file ops
  (sqlite3/sed/mkdir/cp/rm/chmod/find, grafana tee-heredocs) -> runFileOp/
  runFileWrite; prometheus/grafana docker ps/kill/restart -> dockerCommandRun.
Byte-identical in rooted (all helpers reduce to sudo there).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 23:33:51 +01:00
librelad
bdd73b4686 harden(desudo): append-capable runFileWrite + convert config-to-container
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>
2026-05-23 23:26:13 +01:00
librelad
82839abea6 harden(desudo): arg-safe runFileOp + convert DNS subsystem off raw sudo
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>
2026-05-23 23:22:46 +01:00
librelad
0bf9c41c51 harden(rootless): offset userns surface with kptr/ptrace/bpf-jit sysctls
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>
2026-05-23 22:59:18 +01:00
librelad
829816b826 feat(rootless): default to pasta+implicit, disable userland-proxy, make net driver switchable
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>
2026-05-23 22:52:44 +01:00
librelad
049d5de6a8 fix(rootless): start daemon with slirp4netns, not invalid pasta+builtin
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>
2026-05-23 22:02:42 +01:00
librelad
49c1a23221 fix(rootless): run install-user commands via sudo -u, not SSH
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>
2026-05-23 21:42:29 +01:00
librelad
90584f0b30 fix(rootless): actually create the docker install user
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>
2026-05-23 21:23:12 +01:00
librelad
5c928fe9c0 feat(privilege): mode-aware privileged-op helper
Single place that decides how a privileged op runs by Docker mode:
- runFileOp / runFileWrite: /docker data-plane ops — rooted uses sudo (identical
  to today), rootless runs as the unprivileged install user (no root).
- runSystem: genuine system-admin ops, sudo in both modes, funnelled here so it
  can later be confined to a scoped sudoers allowlist.
Call sites converted to these are byte-for-byte unchanged under rooted, so
existing/live boxes can't regress; rootless gets the de-privileged path.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-23 20:35:18 +01:00
librelad
79a1ec4cc3 fix(install): resolve installer function name case-insensitively
dockerInstallApp built the installer name by upper-casing only the first
letter of the slug (libreportal -> installLibreportal), which can't match
camelCase installers like installLibrePortal. After the EasyDocker ->
LibrePortal rename this broke `libreportal` installs with
"installLibreportal: command not found".

If the naive name isn't a defined function, resolve it case-insensitively
against the function table (compgen -A function), and fail with a clear
message if nothing matches. Works for any compound brand/app name.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-22 00:34:26 +01:00
librelad
875a60f90f LibrePortal v0.1.0 — initial release
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>
2026-05-21 20:37:54 +01:00