librelad 3fbc997a2d First-run restore: actually restore the system config, and check the domains
The installer's restore path printed "Settings restored" and had never restored
a setting. backupRestoreSystemConfig only STAGES — right in general, since
overwriting a running control plane's config should not be automatic — but
nothing ever adopted the staged tree. The backup locations, domains and logins
landed in $restore_dir/system-config and stayed there.

So adoption is its own step now (`restore adopt`), allowed only on a machine
with nothing on it yet. backup/locations/ is adopted as a subtree, since the
index is part of the path and that directory is the whole point: it holds every
repository and its credentials, which is what makes "one password unlocks the
rest" true. Deliberately NOT adopted: the container account and its generated
password, port allocations, docker/rootless wiring, and storage/locations —
those describe the old machine, and a registry of drives this box does not have
would make every placement lookup resolve to a phantom.

The guard failed in the shape this project keeps hitting. It globbed the
containers directory, but the manager can traverse that without listing it, so
the glob returned a literal '*', the loop skipped it, and the function returned
"first run" on a machine with three apps. It adopted over a live install in
testing. It now asks the container user for the listing and fails closed: an
unreadable directory means "in use", never "empty".

Two config modes were inverted, found because a restore cannot restore from a
snapshot that was never taken:

  - storage location configs were 0640 and hold no secrets. The backup runs as
    the container user, could not read them, and restic wrote an INCOMPLETE
    snapshot and exited 3 — so EVERY system-config backup failed once a second
    storage location existed. Now 0644, with the test asserting they stay
    secret-free so that mode remains defensible.

  - backup location configs were 0644 and hold the repository password; nobody
    could read them. They cannot simply be tightened, because the backup has to
    read the credentials it uses — so the directory carries the restriction
    (config-secure, manager:container 0750) and the file stays readable to the
    two accounts that belong.

Fixing that surfaced a third: config-adopt clamped existing parent directories
to manager:manager 0750, closing configs/backup to the container user and
breaking the credential read the directory fix had just preserved.

restore domains reports which restored domains point here, and the installer
offers to drop the strays. Three verdicts, not two: setupCheckDomainPointsHere
falls back to hostname -I, and comparing a public A record to a private 10.x
address would condemn every correctly-pointed domain on a LAN-only box, which
is the deployment this product targets. Unverifiable is never offered for
deletion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 04:18:43 +01:00

88 lines
5.3 KiB
Bash

#!/bin/bash
locationAdd()
{
local name="$1"
local type="${2:-local}"
if [[ -z "$name" ]]; then
isError "locationAdd requires a name"
return 1
fi
case "$type" in
# connect resolves exactly like rest — it IS a restic REST repository.
# It is its own type only so the UI can tell a LibrePortal Connect
# destination apart from a REST server someone runs themselves, which
# otherwise look identical on disk.
local|sftp|rest|connect|s3|b2|gs|azure|rclone) ;;
*) isError "Unsupported location type: $type"; return 1 ;;
esac
local idx
idx=$(resticNextFreeIndex)
local default_engine="${CFG_BACKUP_ENGINE:-restic}"
local default_path_mode="auto"
local default_path=""
backupLocationEnsureDir "$idx"
local cfg_file
cfg_file=$(backupLocationConfig "$idx")
local owner
owner=$(backupLocationOwner)
{
echo "# Backup location $idx — added $(date -Iseconds)."
echo "# Edit via the Locations page on /backup, or directly here."
echo "CFG_BACKUP_LOC_${idx}_NAME=\"${name}\" # Location Name - Friendly label shown in the UI"
echo "CFG_BACKUP_LOC_${idx}_ENABLED=false # Enabled - Snapshot to this location"
echo "CFG_BACKUP_LOC_${idx}_ENGINE=${default_engine} # Engine - Backup engine used at this location [restic:Restic|borg:BorgBackup|kopia:Kopia] **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_PASSWORD=RANDOMIZEDPASSWORD1 # Repository Password - Used to encrypt/decrypt snapshots — back up offline!"
echo "CFG_BACKUP_LOC_${idx}_TYPE=${type} # Type - Backend [local:Local / mounted path|sftp:SFTP|rest:REST|s3:S3|b2:Backblaze B2|gs:Google Cloud Storage|azure:Azure|rclone:rclone]"
echo "CFG_BACKUP_LOC_${idx}_PATH_MODE=${default_path_mode} # Path Mode - Automatic uses the Default Backup Location from the Backup Engine config (one subfolder per location); Custom uses the path below [auto:Automatic|custom:Custom path]"
echo "CFG_BACKUP_LOC_${idx}_PATH=${default_path} # Custom Path - Filesystem path on this server (used when Path Mode = Custom)"
echo "CFG_BACKUP_LOC_${idx}_REQUIRE_MOUNT=false # Require Mounted Drive - For an external/removable disk: refuse to back up unless the path is on a real mount, so an unplugged drive never silently fills the system disk [true:Yes|false:No]"
echo "CFG_BACKUP_LOC_${idx}_URI= # URI Override - Custom restic URI (leave blank to build from the fields below) **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_SSH_USER= # SSH User - For sftp type"
echo "CFG_BACKUP_LOC_${idx}_SSH_HOST= # SSH Host - For sftp type"
echo "CFG_BACKUP_LOC_${idx}_SSH_PORT=22 # SSH Port - For sftp type **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_SSH_PATH= # SSH Remote Path - Path on the remote host where the repo lives"
echo "CFG_BACKUP_LOC_${idx}_SSH_AUTH=key # SSH Authentication - [key:SSH key (managed by LibrePortal)|password:Password (via sshpass)]"
echo "CFG_BACKUP_LOC_${idx}_SSH_PASS= # SSH Password - Used only when SSH Authentication is set to Password"
echo "CFG_BACKUP_LOC_${idx}_S3_ACCESS_KEY="
echo "CFG_BACKUP_LOC_${idx}_S3_SECRET_KEY="
echo "CFG_BACKUP_LOC_${idx}_B2_ACCOUNT_ID="
echo "CFG_BACKUP_LOC_${idx}_B2_ACCOUNT_KEY="
echo "CFG_BACKUP_LOC_${idx}_APPEND_ONLY=false # Append-only - Refuse forget/prune for this location (ransomware-safe) **ADVANCED**"
echo "CFG_BACKUP_LOC_${idx}_CUSTOM_RETENTION=false"
echo "CFG_BACKUP_LOC_${idx}_KEEP_LAST="
echo "CFG_BACKUP_LOC_${idx}_KEEP_DAILY="
echo "CFG_BACKUP_LOC_${idx}_KEEP_WEEKLY="
echo "CFG_BACKUP_LOC_${idx}_KEEP_MONTHLY="
echo "CFG_BACKUP_LOC_${idx}_KEEP_YEARLY="
# runInstallWrite/runInstallOp: configs/ is manager-owned, and the container
# user these used to run as cannot write there — the write failed while the
# success message printed anyway.
} | runInstallWrite "$cfg_file" >/dev/null
runInstallOp chmod 0644 "$cfg_file"
# This file holds CFG_BACKUP_LOC_<idx>_PASSWORD — the key to every backup
# the user has — and it was readable by any local account; `nobody`
# demonstrably could. It cannot simply be 0640: the backup runs as the
# container user and has to read the credentials it is about to use. So the
# DIRECTORY carries the restriction (manager:container 0750) and the mode
# here stays readable to the two accounts that belong.
runOwnership config-secure >/dev/null 2>&1 || true
if declare -f replacePlainPasswords >/dev/null 2>&1; then
replacePlainPasswords "$cfg_file"
fi
source "$cfg_file"
isSuccessful "Location $idx '$name' added at $(backupLocationDir "$idx") (type: $type, engine: $default_engine, disabled by default)"
if declare -f webuiGenerateBackupLocations >/dev/null 2>&1; then
webuiGenerateBackupLocations
fi
echo "$idx"
}