get.docker.com/rootless aborts when ip_tables/ip6_tables aren't loaded.
Ubuntu 24.04 and 26.04 ship both modules but don't autoload them on a
fresh box, so rootless setup died there — and because the caller captures
its output into $result, the reason never reached the console or the
error report. The install continued, reported success, and printed
credentials for a WebUI that was never running.
initPrerequires now modprobes both modules and persists them to
/etc/modules-load.d/libreportal-rootless.conf for subsequent boots,
failing with an actionable message when the kernel genuinely lacks them
(container/VM kernels without netfilter).
installDockerRootless gets its own guard, since it also runs outside
init.sh via start_docker / rootless_start_setup. It only attempts
modprobe when it can — the de-sudoed manager has no modprobe in the
LP_SYSTEM allowlist, matching how ubuntu.sh handles sudo-apt — and
returns non-zero rather than proceeding into a failure whose message
would be swallowed. Already-loaded modules are a clean no-op, so the
normal post-install re-run path is unaffected.
Uninstall removes the drop-in alongside the sysctl ones, and it's listed
in the footprint summary.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`apt install p7zip*` only still resolves on Debian 13 / Ubuntu 24.04+
because the renamed `7zip` package happens to declare `Provides: p7zip`.
That is an alias we don't control, so pick the real package name against
the freshly-updated lists instead: `7zip` where it exists, `p7zip-full`
otherwise. Match on a real package stanza rather than apt-cache's exit
status, which returns 0 with empty output for provided-only names.
Also in this path:
- apt -> apt-get for the scripted calls, so the "apt does not have a
stable CLI interface" warning stops polluting the captured $result.
- drop a duplicated `pv` from the package list.
- move the package list below `apt-get update` so the 7-Zip probe reads
current lists.
Debian 10 sysctl check now reads OS_TYPE/OS_VERSION from detectOS rather
than shelling out to lsb_release, which minimal images don't ship. This
also stops a non-Debian release numbered "10" from matching.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A fresh install creates an empty containers root but leaves the rootless
daemon's own container state untouched. Restarting the daemon then runs its
container-restore pass, which resurrects the previous install's containers —
and Docker materialises each missing bind-mount source first, creating an empty
DIRECTORY even where the mount is a file.
That is the trigger behind the <app>.config stub directories: at 19:38:59 the
daemon re-created every missing mount source for a container built 40 minutes
earlier, runc then failed with "not a directory: Are you trying to mount a
directory onto a file", and the abandoned stubs collided with the install's own
copies 18 seconds later.
Add dockerRemoveStrandedContainers, run right after the rootless daemon
restart: remove containers whose compose project directory no longer exists, so
the next restart has nothing to resurrect. Scoped to project directories under
the LibrePortal containers root, so unrelated containers on the host are never
touched, and gated on the daemon answering.
Signed-off-by: librelad <librelad@digitalangels.vip>
Per feedback — shorten the install-failure error (drop the extra hint line) and
the rootless MTU notice.
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CFG_NETWORK_MTU=1500 was baked blindly into the rootless daemon uplink
(DOCKERD_ROOTLESS_ROOTLESSKIT_MTU) and every app's container network. On links
whose real path MTU is below 1500 (Qubes/NAT/VPN with PMTU discovery blocked),
image manifests + tiny images pull fine but large image LAYERS stall and reset
mid-blob with "httpReadSeeker: ... EOF" — apps silently fail to install. Probed
here: path MTU ~1328, Docker at 1500 → black hole.
- New scripts/network/network_mtu.sh: networkDetectMtu (don't-fragment ICMP
ladder → largest standard MTU that gets through, 1500 when ICMP gives no
signal) + networkEffectiveMtu (CFG_NETWORK_MTU: a number is verbatim, "auto"
probes once and caches $docker_dir/.network_mtu) + networkRedetectMtu.
- CFG_NETWORK_MTU default 1500 -> auto. Rootless setup now writes the resolved
MTU into the override (and re-detects per install); the per-app NETWORK_MTU_TAG
uses the resolved value too. Explicit numbers still win.
Verified on this box: auto -> 1300; alpine (multi-MB) + the full ~100MB navidrome
image now pull to completion where they previously EOF'd. Applied live (override
1300, config=auto, cache=1300, rootless docker restarted).
Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drop the GitHub-release version comparison entirely. We install slirp4netns
from apt regardless, so comparing against the GitHub-latest tag only produced
a perpetual 'outdated' loop and a no-op re-install. apt-get install -y is
already idempotent, so run it unconditionally and report the resulting
version.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
slirp4netns --version prints multiple lines (version, commit, libslirp,
SLIRP_CONFIG_VERSION_MAX). The old 'awk {print $2}' ran on every line and
also picked the literal word 'version' from line 1, producing a multi-line
blob that leaked into the 'is outdated' notice. Read only the first line and
take field 3 (the actual number), strip the leading v from the GitHub tag so
the comparison is meaningful, and skip the check if the tag fetch fails.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
Initial rootless setup ran 'systemctl --user start docker' immediately
after install, but the rootless net/port-driver override.conf (and the
daemon-reload that loads it) aren't written until further down. So the
first start always failed — 'Job for docker.service failed' plus a
spurious '✗ Error Setting up Rootless' in the error report — even though
the later 'systemctl --user restart docker' brought the daemon up fine
once the override was in place.
Drop the premature start from the install step (keep install + enable);
the restart after the override is written is now the first real start.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The 'Installing System Requirements' step ran apt-get install with no
output until checkSuccess reported afterwards, so it looked frozen
while packages were being fetched. Print a notice up front.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
'local result=$(cmd)' resets $? to 0 (the local builtin's own exit), so the
following checkSuccess always saw success regardless of cmd's real exit — the
mechanism that masked the de-sudo write failures. Split declaration from
assignment ('local result; result=$(cmd)') across all 235 active-code sites
(84 files) so the command's exit reaches checkSuccess. No behaviour change
beyond $? now being accurate (no set -e in runtime code; multi-line
assignments transform safely).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The Debian-shipped passt AppArmor profile (/etc/apparmor.d/usr.bin.passt)
denies the accesses pasta needs to plumb rootlesskit's netns:
- ptrace_read on the rootlesskit child to enter its user namespace
- read /run/user/<uid>/dockerd-rootless/netns (the netns file)
- read /proc/<pid>/net/{tcp,tcp6,udp,udp6} for implicit port forwarding
Without these the rootless docker daemon fails with:
pasta failed with exit code 1:
Couldn't open user namespace /proc/<pid>/ns/user: Permission denied
scripts/docker/install/rootless/rootless_apparmor.sh:
New installRootlessApparmorForPasta() — idempotent fixup.
1. Adds `include if exists <local/usr.bin.passt>` to the main profile
(one line; re-adding is a no-op via grep).
2. Writes /etc/apparmor.d/local/usr.bin.passt with the four rules
pasta needs. The /local/ pattern is the standard Debian AppArmor
hook for site-managed overrides — survives `apt upgrade passt`
because it's outside the package's managed paths.
3. Reloads via apparmor_parser -r.
Called from installDockerRootless after the override.conf write, gated
on $rootless_net == pasta. slirp4netns installs skip it.
This box was already manually patched while debugging the pasta swap —
the installer-side change makes it idempotent across reinstalls and
applies the same fix on any other host that installs rootless docker
with pasta as the net driver.
Signed-off-by: librelad <librelad@digitalangels.vip>
On a fresh install the requirement checks run before the things they
probe exist, leaking raw command stderr:
- check_install_type.sh: `$( (id -u "$user") )` printed
"id: 'dockerinstall': no such user" to the terminal AND — since id's
error goes to stderr, not the captured stdout — the next line's
`[[ "$ISUSER" == *"no such user"* ]]` could never match, so the
rootless-user-absent branch was dead. Add `2>&1` (matching siblings on
lines 25/31): no leak, and the check now works.
- grep on $sysctl (the rootless marker conf, absent until rootless is set
up) printed "grep: /etc/sysctl.d/99-libreportal-rootless.conf: No such
file or directory". Add -s to the four $sysctl greps
(check_docker_rootless, rootless_start_setup, rootless_docker x2);
"marker absent" is still detected (non-zero exit), just without the
file-not-found message.
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>
Foundation for a scoped sudoers: route every genuine system-admin command
(systemctl/ufw/ufw-docker/nft/apt/apt-get/pacman/sysctl/useradd/usermod/
service/wg/wg-quick/cscli/loginctl) through runSystem instead of raw sudo
across 28 active scripts. runSystem is 'sudo "$@"' so this is byte-identical
in every mode (safe on live installs) — it just collects all real-root use at
one chokepoint that will define the eventual /etc/sudoers.d allowlist.
Also: revert a crowdsec advice message the sweep wrongly rewrote (the admin
types sudo, not runSystem), and give crontab_check_processor.sh the same
startup bootstrap as the task processor — it runs standalone via cron and
already used runFileOp/runFileWrite (undefined there), so it was silently
broken; now it sources the helpers + docker-type config.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
The 'Update the .bashrc file' step printed its header but, when the rootless
block was already present, the if-guard skipped the whole body with no output
— looked like nothing happened. Add an else that notes it's already configured.
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>
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>
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>
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>
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>