LibrePortal/scripts/config/config_update.sh
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

92 lines
3.3 KiB
Bash
Executable File

#!/bin/bash
# Apply a batch of `CFG_KEY=VALUE` pairs joined by `|` (literal `|` in values
# encoded as `%7C`). Re-runs apps.json regen + startScan after.
configUpdateBatch()
{
local encoded_pairs="$1"
if [[ -z "$encoded_pairs" ]]; then
isNotice "configUpdateBatch called with no changes — skipping update."
fi
isHeader "Applying configuration changes"
local applied=0
local failed=0
local catalog_changed=false
IFS='|' read -ra pairs <<< "$encoded_pairs"
for pair in "${pairs[@]}"; do
[[ -z "$pair" ]] && continue
if [[ "$pair" =~ ^(CFG_[A-Z0-9_]+)=(.*)$ ]]; then
local key="${BASH_REMATCH[1]}"
local value="${BASH_REMATCH[2]//%7C/|}"
# A value the WebUI collected as a secret arrives here as a
# REFERENCE, never the secret itself. Redeem it at the last moment
# before the write.
#
# This is the whole point of the channel: the pair string reaching
# this function came in as part of a task's command, and tasks are
# recorded in frontend/data/tasks/*.json — 0644, inside a
# world-readable directory — as well as being visible in `ps` while
# they run. A backup repository password sent that way is the key to
# every backup the user has, readable by any local account.
#
# Done here rather than per caller because this is the single point
# every config write from the WebUI passes through, so every
# password field benefits at once.
if declare -f webuiSecretIsRef >/dev/null 2>&1 && webuiSecretIsRef "$value"; then
local _secret
if ! _secret=$(webuiSecretConsume "${value#secret:}"); then
isError "Could not read the submitted value for $key — leaving it unchanged."
((failed++))
continue
fi
value="$_secret"
fi
[[ "$key" == CFG_CATALOG_* ]] && catalog_changed=true
if updateConfigOption "$key" "$value"; then
((applied++))
else
((failed++))
fi
else
isNotice "Skipping malformed pair: $pair"
((failed++))
fi
done
isSuccessful "Applied $applied config change(s); $failed skipped/failed."
echo ""
echo "---- Regenerating apps.json from updated config..."
echo ""
if declare -f webuiGenerateLibrePortalConfig >/dev/null 2>&1; then
webuiGenerateLibrePortalConfig >/dev/null 2>&1 || true
isSuccessful "apps.json regenerated."
fi
echo ""
echo "---- Running system scan to apply new configuration..."
echo ""
if declare -f startScan >/dev/null 2>&1; then
startScan
isSuccessful "System scan completed."
fi
# Catalog sources are the source of truth for the App Center browse data;
# rebuild the generated registry_catalog.json when a CFG_CATALOG_* changed.
if [[ "$catalog_changed" == true ]] && declare -f catalogRefresh >/dev/null 2>&1; then
echo ""
echo "---- Refreshing catalog sources..."
echo ""
catalogRefresh
fi
echo ""
isSuccessful "Configuration update complete."
}