3 Commits

Author SHA1 Message Date
librelad
0aa92e128e webui: a one-shot channel for secrets, instead of the command line
A password typed in the WebUI has to reach the host, and both existing routes
leak it. As part of a task's command string it lands in
frontend/data/tasks/*.json — 0644, inside a world-readable directory — and is
visible in `ps` while the task runs; as a plain file there it is either
world-readable at 0644 or unreadable by the manager at 0640. Verified still true
on a clean install. A backup repository password sent that way is the key to
every backup the user has, readable by any local account.

libreportal-ownership gains `secret-dir`: the mirror of _webui_bind_access.
That one makes manager-owned config readable by the container; this makes a
container-written file readable by the MANAGER. The directory is
<container>:<manager> mode 2730 — setgid so each file inherits the manager's
group, the container writes it 0640, and 0730 leaves the directory unlistable
because the manager is handed a filename rather than going looking. Group rwx
is what lets it unlink after reading.

The WebUI then sends a REFERENCE ("secret:<id>") wherever it used to send the
value, and configUpdateBatch redeems it at the last moment before the write.
That is the single point every config write from the WebUI passes through, so
this covers every password field rather than only the backup ones — which is
what docs/roadmap/first-run-restore.md §4.1 asked for. A reference that cannot
be redeemed leaves the field unchanged rather than blanking it.

Verified on a live install: the container drops a secret, the manager applies it
by reference, the file is unlinked, `nobody` can neither read nor list it, and a
second redemption of the same reference fails.

footprint_version 9 -> 10 (root-owned helper changed).

Also fixes a block of constructor initialisations I spliced into the middle of
renderStorageChoices in aa44e0b: on a single-drive box — the case in the
screenshot that prompted this — rendering the Storage step silently reset
backupDest and cleared the import selections.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 08:48:03 +01:00
librelad
abf3a65c88 refactor(catalog): move sources into a config file (CFG_CATALOG_1..9), generate from it
Per the intended model: catalog sources should live in the config place (like
domains), and registry_catalog.json should be a purely GENERATED artifact
derived from them — not the source of truth. Replaces the earlier
$docker_dir/catalog/sources.json store.

- New configs/general/general_catalogs — CFG_CATALOG_1..9, one catalog base URL
  per slot ("url" or "url|channel"), domains-style. Official stays pinned as
  source #1 (derived from CFG_RELEASE_BASE_URL, not listed here). Slot N → source
  idx N+1 (stable id for the Add picker / `app add --source`).
- catalog_sources.sh now reads/writes those CFG vars (via updateConfigOption)
  instead of a JSON file; dropped catalogSourcesFile + the enable/disable toggle
  (presence = enabled; remove = clear the slot).
- configUpdateBatch regenerates registry_catalog.json when a CFG_CATALOG_* key
  changed — so pressing Save in the WebUI rebuilds the browse data.
- webuiRegistryCatalogScan is unchanged (still iterates catalogEnabledSources).

Verified: CFG_CATALOG_1/2 → sources at idx 2/3, empty slots skipped, url|channel
parsed, official pinned at idx 1.

Signed-off-by: librelad <librelad@digitalangels.vip>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-06 22:01:22 +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