The main sweep — ~260 call sites across ~100 files move from string
concatenation on a single root to appDir/storageAppDirs/storageAppConfigs.
On a single-root install the resolved paths are identical, so this is a
no-op until a location is registered.
Enumerators were the interesting half. `for d in "$containers_dir"/*/`
appears in the menus, the registry/artifact scanners and the DNS setup —
and a shell glob cannot list a rootless 751 tree at all, which is the
same bug config_find_file.sh already documents in a comment. Routing them
through storageAppDirs (which enumerates as the owning user) fixes that
alongside the multi-root work.
Three places needed judgement rather than substitution:
db_app_scan.sh deletes database rows and port allocations for apps whose
folder is missing, and reaps "empty" app dirs. With a storage location
unmounted, every app on it looks exactly like that. Each of those
branches now gates on appStorageAvailable first — an app on an unplugged
drive is skipped with a notice, never deleted.
instance_create.sh rewrites cloned hooks so an instance touches its own
directory instead of the base app's. Its sed matched ${containers_dir}<type>,
which this sweep just replaced with $(appDir <type>) — so it would have
silently stopped redirecting, and an instance would have written to the
original's files (the adguard auth adapter case its own comment warns
about). Now matches both appDir forms, verified against bare, quoted,
unrelated-app, legacy and prose cases.
peer_shell/peer_pull streamed and extracted relative to the primary root.
Both now use the app's own root, and peer_shell keeps a single-root
fallback since it runs as a restricted SSH shell with no LibrePortal env.
Also fixes a pre-existing bug found on the way: webui_app_config.sh
tested "$containers_dir/frontend/data/last_update", one level short of the
real tree under the libreportal app dir, so the WebUI refresh trigger
after a config update has never once fired.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Installing rocketchat failed with
invalid IPv4 address: ParseAddr("IP_DATA_2"): unable to parse IP
ipUpdateComposeTags allocates one IP per SERVICE_TAG_N annotation and fills
IP_TAG_i only where SERVICE_TAG_i exists. The four new apps tagged only their
primary service, so every sidecar — matrix's postgres, mattermost's postgres,
rocketchat's mongo, and fifteen of stoat's sixteen — kept a literal IP_DATA_n
in the deployed compose and docker refused to create the container.
Tag every service that carries an ipv4_address, index-aligned with its IP_TAG.
For stoat that also meant moving caddy from SERVICE_TAG_1 to _6 so the indices
line up with the IPs rather than the reading order.
mastodon had the same latent break (IP_TAG_2 and _3 untagged) and is fixed the
same way — it would have failed on first install for the same reason.
SERVICE_TAG carries the compose *key*, not container_name: 'libreportal app
restart <app> <service>' passes it to 'docker compose restart', which only
understands keys.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two more runtime root file-primitive subsystems moved behind self-
validating root-owned helpers so the scoped sudoers needn't grant blanket
sudo sed/tee/cp on /etc (which is root-equivalent — sudo arg wildcards
match across '/', so even path-scoped entries are bypassable):
- scripts/system/libreportal-dns: {clear|add <ip>} — edits /etc/resolv.conf
only, validates the IP argument
- scripts/system/libreportal-ssh-access: authorized_keys + sshd
PasswordAuthentication management, with the lockout guards moved INTO the
helper (the trust boundary) so a compromised manager can't bypass them
- run_privileged: _runRootHelper dispatcher + runResolv / runSshAccess
(runOwnership now uses it too)
- init.sh: initRootHelpers installs all three helpers root:root 0755 with
the manager name baked in
- setup_dns -> runResolv (+ ping de-sudo'd, works unprivileged); host_access
+ webui_ssh_access -> runSshAccess
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
app_generate operates on the manager-owned install template -> runInstallOp
(cp/mv/sed); drop sudo on the interactive editor. localDnsApplyPihole edits
containers/pihole/.../custom.list (docker-install-owned) -> read via runFileOp,
build in a manager /tmp scratch, write back via runFileWrite.
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>
setupLocalDnsRewrites points every configured domain at the server's LAN IP
inside the self-hosted resolver, so app subdomains resolve locally and hit
Traefik directly (valid certs, no router hairpin). AdGuard gets a wildcard
rewrite per domain via its REST API; Pi-hole gets per-host A records in the
supported, mounted custom.list (no wildcard support there). Safe by
construction: idempotent, guarded by installed-checks, cannot corrupt the
resolver. Hooked into the Apply-DNS actions and resolver install. Also drops
the dead HOST_NAME read from the setupDNSIP stub.
NOTE: needs a live smoke-test — the AdGuard API call and Pi-hole reload
can't be exercised without the running containers.
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>