6 Commits

Author SHA1 Message Date
librelad
ff25b08ee8 Make multi-instance actually install, and stop apps stealing each other's network rows
Instance install (bugs found by running one end to end):

- The cloned compose kept the TYPE's tag namespace
  (#LIBREPORTAL|BOOKSTACK_APP_KEY_1_TAG|...) while the config had been
  re-namespaced to CFG_<SLUG>_*, so tagsProcessorAppConfigValues matched
  nothing, the placeholders survived and the pre-start guard refused to
  launch. Rewrite the tag names and *_DATA tokens too — narrowly, so an
  app whose compose sets a real env var named after itself is untouched.
- Tools/hooks kept uppercase CFG_<TYPE>_ reads, so an instance
  provisioned itself from the type's config and ignored its own values.
- Cloned hooks were never loaded: both loaders run at startup, before the
  instance dir exists, so _appCallHook's `declare -F` found nothing and
  every <slug>_install_* hook silently no-opped — for bookstack that is
  the readiness probe and the admin bootstrap. Source the instance's own
  scripts in-process, then regen arrays + manifest for later runs.
- bookstack's hook hardcoded the container name after `docker exec -e ...`
  flags, where the rewriter can't see it, so an instance's admin bootstrap
  ran against the BASE app's container — including a tinker DELETE of a
  user. Target "$app_name" instead, and teach the rewriter the
  container="<type>" assignment form used by auth adapters.

network_resources uniqueness:

UNIQUE(resource_type, resource_value) is right for 'ip' and 'port' but the
port-tag writer stores descriptive rows in the same table with INSERT OR
REPLACE, so every install DELETED the matching row from whichever app held
it. traefik_managed and url_accessible are booleans, so the whole table
could only ever hold one row of each. Observed live: installing a second
bookstack took all four traefik_managed/url_accessible rows from stoat and
bookstack, and removing that instance took the stolen rows with it.

Replace it with a partial unique index scoped to ip/port, and migrate
existing databases in place (SQLite can't drop a constraint, so the table
is rebuilt inside a transaction). The migration is invoked from
portUpdateComposeTags, not just databaseCreateTables — the latter only
runs from startPreInstall, which a working install never re-runs.

Verified: two bookstacks now hold port_tag_internal=80, traefik_managed
and url_accessible simultaneously; duplicate host ports and IPs are still
rejected; instance installs, serves HTTP 200, provisions its own admin in
its own database, and removes cleanly with no orphan rows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 23:15:21 +01:00
librelad
b7a0743d8b feat(network): add ipInSubnet + IP-only network reset scope
Foundations for network-drift healing:

- ipInSubnet(ip, cidr): prefix-aware CIDR membership (pure bash), so
  stored IPs can be checked against docker's real subnet. Honours the
  actual prefix, so a healthy /16-subnet + /24-ip-range install is not
  mistaken for drift.

- dockerInstallApp now accepts reset_network="ip": re-roll the static IP
  from the current subnet but PRESERVE published host ports (clears only
  IP rows; LIBREPORTAL_RESET_IP_ONLY keeps port_allocate reusing existing
  ports). This is the heal path — a subnet move strands the IP, not the
  port, so we don't churn bookmarks/forwards/proxy upstreams. reset="true"
  still re-rolls both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-02 15:54:55 +01:00
librelad
c6dd2659be refactor(de-sudo): apps DB access via runInstallOp, not sudo
The apps SQLite DB ($docker_dir/$db_file) is owned by the manager user, so
read/write it AS the manager via runInstallOp instead of sudo (root). 48 call
sites across 28 scripts. In rooted this drops root->manager (correct owner);
in rootless it's the manager too (using runFileOp/dockerinstall here was the
'unable to open database' bug). The broken 'command -v sudo sqlite3' check
lines are left untouched (separate pre-existing issue).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
2026-05-24 16:23:33 +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
582664aadf harden(desudo): convert crontab daemon, db-scan, port-allocation subsystems
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>
2026-05-23 23:36:13 +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