refactor(desudo): funnel backup-engine privilege drop through runBackupOp

The borg/restic/kopia engines all dropped to the dedicated backup user
via scattered 'sudo -E -u $docker_install_user'. Centralize that into a
single runBackupOp helper so the backup subsystem has one audit point and
the scoped sudoers needs only the (dockerinstall) drop rule.

Also:
- owncloud config heredoc tees -> runSystem (container-UID file)
- webui_display_logins: fix the broken 'command -v sudo sqlite3' guard
  to 'command -v sqlite3' (body already runs sqlite3 via runInstallOp)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
This commit is contained in:
librelad 2026-05-24 18:01:51 +01:00
parent 0dc86e62f6
commit 0b27ed1072
23 changed files with 52 additions and 43 deletions

View File

@ -89,7 +89,7 @@ appOwnCloudSetupConfig()
if [[ $public == "true" ]]; then
# Add new lines at the end of the file
sudo tee -a "$tmp_awk_output" > /dev/null <<EOL
runSystem tee -a "$tmp_awk_output" > /dev/null <<EOL
'overwrite.cli.url' => 'https://$host_setup/',
'Overwriteprotocol' => 'https',
'trusted_domains' =>
@ -104,7 +104,7 @@ EOL
checkSuccess "Add overwrite and trusted_domain (public) lines to the config"
elif [[ $public == "false" ]]; then
# Add new lines at the end of the file
sudo tee -a "$tmp_awk_output" > /dev/null <<EOL
runSystem tee -a "$tmp_awk_output" > /dev/null <<EOL
'trusted_domains' =>
array(
0 => '$ip_setup',

View File

@ -16,7 +16,7 @@ backupAppDeleteSnapshot()
fi
resticEnvExport "$idx" || return 1
sudo -E -u "$docker_install_user" restic forget "$snapshot_id" $([[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" ]] && echo "--prune")
runBackupOp restic forget "$snapshot_id" $([[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" ]] && echo "--prune")
local rc=$?
resticEnvUnset
if [[ $rc -eq 0 ]]; then
@ -47,7 +47,7 @@ backupAppDeleteAll()
continue
fi
resticEnvExport "$idx" || continue
sudo -E -u "$docker_install_user" restic forget --tag "app=$app_name" $([[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" ]] && echo "--prune")
runBackupOp restic forget --tag "app=$app_name" $([[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" ]] && echo "--prune")
resticEnvUnset
done < <(resticEnabledLocations)
}

View File

@ -32,7 +32,7 @@ borgBackupAppToLocation()
# Logs to stderr; stdout is only the archive name for the caller's $().
isNotice "Snapshotting $app_name$loc_name (archive: $archive)" >&2
sudo -E -u "$docker_install_user" borg create \
runBackupOp borg create \
--comment "$comment" \
--compression auto,zstd \
"${exclude_args[@]}" \

View File

@ -13,7 +13,7 @@ borgCheckLocation()
fi
isNotice "Checking $(resticLocationName "$idx")${read_data:+ (full data verify)}"
sudo -E -u "$docker_install_user" borg "${args[@]}"
runBackupOp borg "${args[@]}"
local rc=$?
borgEnvUnset
if [[ $rc -eq 0 ]]; then
@ -29,7 +29,7 @@ borgLocationStats()
local idx="$1"
borgEnvExport "$idx" || return 1
local raw
raw=$(sudo -E -u "$docker_install_user" borg info --json 2>/dev/null)
raw=$(runBackupOp borg info --json 2>/dev/null)
borgEnvUnset
[[ -z "$raw" ]] && { echo '{"total_size":0,"total_file_count":0}'; return 0; }

View File

@ -28,11 +28,11 @@ borgForgetApp()
[[ -n "$keep_yearly" ]] && args+=(--keep-yearly "$keep_yearly")
isNotice "Applying retention for $app_name on $(resticLocationName "$idx")"
sudo -E -u "$docker_install_user" borg "${args[@]}"
runBackupOp borg "${args[@]}"
local rc=$?
if [[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" && $rc -eq 0 ]]; then
sudo -E -u "$docker_install_user" borg compact
runBackupOp borg compact
fi
borgEnvUnset

View File

@ -16,14 +16,14 @@ borgInitLocation()
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$BORG_REPO"
fi
if sudo -E -u "$docker_install_user" borg info "$BORG_REPO" >/dev/null 2>&1; then
if runBackupOp borg info "$BORG_REPO" >/dev/null 2>&1; then
isNotice "$(resticLocationName "$idx") already initialized at $BORG_REPO"
borgEnvUnset
return 0
fi
isNotice "Initializing $(resticLocationName "$idx") at $BORG_REPO"
if sudo -E -u "$docker_install_user" borg init --encryption=repokey-blake2 "$BORG_REPO"; then
if runBackupOp borg init --encryption=repokey-blake2 "$BORG_REPO"; then
isSuccessful "$(resticLocationName "$idx") initialized"
else
isError "Failed to initialize $(resticLocationName "$idx")"

View File

@ -19,10 +19,10 @@ borgRestoreSnapshot()
local rc
if [[ -n "$include_path" ]]; then
local stripped="${include_path#/}"
( cd "$target_dir" && sudo -E -u "$docker_install_user" borg extract "::$snapshot_id" "$stripped" )
( cd "$target_dir" && runBackupOp borg extract "::$snapshot_id" "$stripped" )
rc=$?
else
( cd "$target_dir" && sudo -E -u "$docker_install_user" borg extract "::$snapshot_id" )
( cd "$target_dir" && runBackupOp borg extract "::$snapshot_id" )
rc=$?
fi
borgEnvUnset
@ -39,9 +39,9 @@ borgDumpFile()
borgEnvExport "$idx" || return 1
local stripped="${file_path#/}"
if [[ -n "$target_file" ]]; then
sudo -E -u "$docker_install_user" borg extract --stdout "::$snapshot_id" "$stripped" | runFileWrite "$target_file"
runBackupOp borg extract --stdout "::$snapshot_id" "$stripped" | runFileWrite "$target_file"
else
sudo -E -u "$docker_install_user" borg extract --stdout "::$snapshot_id" "$stripped"
runBackupOp borg extract --stdout "::$snapshot_id" "$stripped"
fi
local rc=$?
borgEnvUnset

View File

@ -19,7 +19,7 @@ borgSnapshotsJson()
fi
local raw
raw=$(sudo -E -u "$docker_install_user" borg "${args[@]}" 2>/dev/null)
raw=$(runBackupOp borg "${args[@]}" 2>/dev/null)
local rc=$?
borgEnvUnset
[[ $rc -ne 0 || -z "$raw" ]] && { echo "[]"; return $rc; }
@ -63,7 +63,7 @@ borgSnapshotLatestId()
borgEnvExport "$idx" || return 1
local id
id=$(sudo -E -u "$docker_install_user" borg list --json --glob-archives "${app_name}-${host}-*" --last 1 2>/dev/null \
id=$(runBackupOp borg list --json --glob-archives "${app_name}-${host}-*" --last 1 2>/dev/null \
| grep -o '"name":"[^"]*"' | head -1 | cut -d'"' -f4)
borgEnvUnset
echo "$id"
@ -74,7 +74,7 @@ borgSnapshotListFiles()
local idx="$1"
local snapshot_id="$2"
borgEnvExport "$idx" || return 1
sudo -E -u "$docker_install_user" borg list --json-lines "::$snapshot_id" 2>/dev/null
runBackupOp borg list --json-lines "::$snapshot_id" 2>/dev/null
local rc=$?
borgEnvUnset
return $rc

View File

@ -41,7 +41,7 @@ kopiaBackupAppToLocation()
fi
local output
output=$(sudo -E -u "$docker_install_user" kopia snapshot create "$source_path" "${tags[@]}" --json 2>&1)
output=$(runBackupOp kopia snapshot create "$source_path" "${tags[@]}" --json 2>&1)
local rc=$?
[[ "$wrote_ignore" == true ]] && runFileOp rm -f "$ignore_file"

View File

@ -15,7 +15,7 @@ kopiaCheckLocation()
fi
isNotice "Checking $(resticLocationName "$idx") with Kopia${read_data:+ (data sample: ${read_data}%)}"
sudo -E -u "$docker_install_user" kopia "${args[@]}"
runBackupOp kopia "${args[@]}"
local rc=$?
kopiaEnvUnset
if [[ $rc -eq 0 ]]; then
@ -31,7 +31,7 @@ kopiaLocationStats()
local idx="$1"
kopiaEnvExport "$idx" || return 1
local raw
raw=$(sudo -E -u "$docker_install_user" kopia repository status --json 2>/dev/null)
raw=$(runBackupOp kopia repository status --json 2>/dev/null)
kopiaEnvUnset
[[ -z "$raw" ]] && { echo '{"total_size":0,"total_file_count":0}'; return 0; }

View File

@ -33,10 +33,10 @@ kopiaForgetApp()
[[ -n "$keep_monthly" ]] && policy_args+=(--keep-monthly "$keep_monthly")
[[ -n "$keep_yearly" ]] && policy_args+=(--keep-annual "$keep_yearly")
sudo -E -u "$docker_install_user" kopia "${policy_args[@]}" >/dev/null 2>&1
runBackupOp kopia "${policy_args[@]}" >/dev/null 2>&1
isNotice "Running Kopia maintenance for $app_name on $(resticLocationName "$idx")"
sudo -E -u "$docker_install_user" kopia maintenance run --full
runBackupOp kopia maintenance run --full
local rc=$?
kopiaEnvUnset
return $rc

View File

@ -16,7 +16,7 @@ kopiaInitLocation()
# Already initialized? `kopia repository status` returns 0 only if the
# config file is connected to a repo.
if sudo -E -u "$docker_install_user" kopia repository status --json >/dev/null 2>&1; then
if runBackupOp kopia repository status --json >/dev/null 2>&1; then
isNotice "$(resticLocationName "$idx") already initialized"
kopiaEnvUnset
return 0
@ -60,7 +60,7 @@ kopiaInitLocation()
esac
isNotice "Initializing $(resticLocationName "$idx") with Kopia"
if sudo -E -u "$docker_install_user" kopia "${args[@]}"; then
if runBackupOp kopia "${args[@]}"; then
isSuccessful "$(resticLocationName "$idx") initialized"
else
isError "Failed to initialize $(resticLocationName "$idx") with Kopia"

View File

@ -24,7 +24,7 @@ kopiaRestoreSnapshot()
final_target="$target_dir/${include_path#/}"
runFileOp mkdir -p "$final_target"
fi
sudo -E -u "$docker_install_user" kopia snapshot restore "$snapshot_id" "$final_target"
runBackupOp kopia snapshot restore "$snapshot_id" "$final_target"
local rc=$?
kopiaEnvUnset
return $rc
@ -40,9 +40,9 @@ kopiaDumpFile()
kopiaEnvExport "$idx" || return 1
# `kopia show` writes the file contents from a snapshot to stdout.
if [[ -n "$target_file" ]]; then
sudo -E -u "$docker_install_user" kopia show "${snapshot_id}:${file_path}" | sudo tee "$target_file" >/dev/null
runBackupOp kopia show "${snapshot_id}:${file_path}" | sudo tee "$target_file" >/dev/null
else
sudo -E -u "$docker_install_user" kopia show "${snapshot_id}:${file_path}"
runBackupOp kopia show "${snapshot_id}:${file_path}"
fi
local rc=$?
kopiaEnvUnset

View File

@ -14,7 +14,7 @@ kopiaSnapshotsJson()
local args=(snapshot list --all --json)
local raw
raw=$(sudo -E -u "$docker_install_user" kopia "${args[@]}" 2>/dev/null)
raw=$(runBackupOp kopia "${args[@]}" 2>/dev/null)
local rc=$?
kopiaEnvUnset
[[ $rc -ne 0 || -z "$raw" ]] && { echo "[]"; return $rc; }
@ -62,7 +62,7 @@ kopiaSnapshotListFiles()
local idx="$1"
local snapshot_id="$2"
kopiaEnvExport "$idx" || return 1
sudo -E -u "$docker_install_user" kopia snapshot list "$snapshot_id" --json 2>/dev/null
runBackupOp kopia snapshot list "$snapshot_id" --json 2>/dev/null
local rc=$?
kopiaEnvUnset
return $rc

View File

@ -36,7 +36,7 @@ resticBackupAppToLocation()
# the caller captures it with $() and feeds it to verify/retention.
isNotice "Snapshotting $app_name$loc_name" >&2
local output
output=$(sudo -E -u "$docker_install_user" restic backup \
output=$(runBackupOp restic backup \
--host "$host_tag" \
"${extra_tags[@]}" \
"${exclude_args[@]}" \

View File

@ -15,7 +15,7 @@ resticCheckLocation()
fi
isNotice "Checking $(resticLocationName "$idx")${read_data:+ (sample: ${read_data}%)}"
sudo -E -u "$docker_install_user" restic "${args[@]}"
runBackupOp restic "${args[@]}"
local rc=$?
resticEnvUnset
if [[ $rc -eq 0 ]]; then
@ -42,7 +42,7 @@ resticLocationStats()
{
local idx="$1"
resticEnvExport "$idx" || return 1
sudo -E -u "$docker_install_user" restic stats --json --no-lock 2>/dev/null
runBackupOp restic stats --json --no-lock 2>/dev/null
local rc=$?
resticEnvUnset
return $rc

View File

@ -10,9 +10,9 @@ resticDumpFile()
resticEnvExport "$idx" || return 1
if [[ -n "$target_file" ]]; then
sudo -E -u "$docker_install_user" restic dump "$snapshot_id" "$file_path" | sudo tee "$target_file" >/dev/null
runBackupOp restic dump "$snapshot_id" "$file_path" | sudo tee "$target_file" >/dev/null
else
sudo -E -u "$docker_install_user" restic dump "$snapshot_id" "$file_path"
runBackupOp restic dump "$snapshot_id" "$file_path"
fi
local rc=$?
resticEnvUnset

View File

@ -28,7 +28,7 @@ resticForgetApp()
[[ "$CFG_BACKUP_PRUNE_AFTER_FORGET" == "true" ]] && args+=(--prune)
isNotice "Applying retention for $app_name on $(resticLocationName "$idx")"
sudo -E -u "$docker_install_user" restic "${args[@]}"
runBackupOp restic "${args[@]}"
local rc=$?
resticEnvUnset
return $rc

View File

@ -16,14 +16,14 @@ resticInitLocation()
runFileOp chown -R "$docker_install_user":"$docker_install_user" "$RESTIC_REPOSITORY"
fi
if sudo -E -u "$docker_install_user" restic snapshots --json --no-lock >/dev/null 2>&1; then
if runBackupOp restic snapshots --json --no-lock >/dev/null 2>&1; then
isNotice "$(resticLocationName "$idx") already initialized at $RESTIC_REPOSITORY"
resticEnvUnset
return 0
fi
isNotice "Initializing $(resticLocationName "$idx") at $RESTIC_REPOSITORY"
if sudo -E -u "$docker_install_user" restic init; then
if runBackupOp restic init; then
isSuccessful "$(resticLocationName "$idx") initialized"
else
isError "Failed to initialize $(resticLocationName "$idx")"

View File

@ -20,7 +20,7 @@ resticRestoreSnapshot()
[[ -n "$include_path" ]] && args+=(--include "$include_path")
isNotice "Restoring ${snapshot_id:0:8} from $(resticLocationName "$idx")$target_dir"
sudo -E -u "$docker_install_user" restic "${args[@]}"
runBackupOp restic "${args[@]}"
local rc=$?
resticEnvUnset
return $rc

View File

@ -12,7 +12,7 @@ resticSnapshotsJson()
[[ -n "$app_filter" ]] && args+=(--tag "app=$app_filter")
[[ -n "$host_filter" ]] && args+=(--host "$host_filter")
sudo -E -u "$docker_install_user" restic "${args[@]}" 2>/dev/null
runBackupOp restic "${args[@]}" 2>/dev/null
local rc=$?
resticEnvUnset
return $rc
@ -26,7 +26,7 @@ resticSnapshotLatestId()
resticEnvExport "$idx" || return 1
local id
id=$(sudo -E -u "$docker_install_user" restic snapshots \
id=$(runBackupOp restic snapshots \
--tag "app=$app_name" --host "$host" \
--latest 1 --json --no-lock 2>/dev/null | \
grep -o '"short_id":"[^"]*"' | head -1 | cut -d'"' -f4)
@ -40,7 +40,7 @@ resticSnapshotListFiles()
local snapshot_id="$2"
resticEnvExport "$idx" || return 1
sudo -E -u "$docker_install_user" restic ls --json --no-lock "$snapshot_id" 2>/dev/null
runBackupOp restic ls --json --no-lock "$snapshot_id" 2>/dev/null
local rc=$?
resticEnvUnset
return $rc

View File

@ -77,6 +77,15 @@ runInstallWrite() {
runAsManager tee "${append_flag[@]}" "$dest" >/dev/null
}
# Backup-engine command (borg/restic/kopia) run AS the dedicated backup user
# ($docker_install_user), with the environment preserved (-E) so the repo
# password and BORG_/RESTIC_/KOPIA_ env vars reach the tool. Never root — the
# scoped sudoers lets the manager drop to this user. Single funnel so the
# backup subsystem's privilege drop has one audit point.
runBackupOp() {
sudo -E -u "$docker_install_user" "$@"
}
# Genuine system-administration command (ufw/systemctl/apt/sysctl/useradd, /etc
# edits). Needs real root in both modes; funnelled through one place so it can
# later be confined to a scoped sudoers allowlist.

View File

@ -10,7 +10,7 @@ getLibrePortalWebUIUrls()
local db_file="${db_file:-database.db}"
# Check if sqlite3 is available and database exists
if command -v sudo sqlite3 &> /dev/null && [ -f "$docker_dir/$db_file" ]; then
if command -v sqlite3 &> /dev/null && [ -f "$docker_dir/$db_file" ]; then
# Check if LibrePortal app is installed
local libreportal_check=$(runInstallOp sqlite3 "$docker_dir/$db_file" "SELECT name FROM apps WHERE name = 'libreportal' AND status = 1;" 2>/dev/null)