diff --git a/scripts/backup/engine/borg_restore.sh b/scripts/backup/engine/borg_restore.sh index e5f91b4..2016045 100644 --- a/scripts/backup/engine/borg_restore.sh +++ b/scripts/backup/engine/borg_restore.sh @@ -16,13 +16,22 @@ borgRestoreSnapshot() runFileOp mkdir -p "$target_dir" isNotice "Restoring $snapshot_id from $(resticLocationName "$idx") → $target_dir" + + # Same reason restic needs it: extraction runs as the backup user, which has + # no CAP_CHOWN, so without the namespace every : + # file comes back owned by the backup user and the app cannot write its own + # data. borg is quieter about it than restic — it does not even print an + # "ignoring error" line — so this went unnoticed for longer. + local ns_prefix=() + mapfile -t ns_prefix < <(backupUsernsPrefix) + local rc if [[ -n "$include_path" ]]; then local stripped="${include_path#/}" - ( cd "$target_dir" && runBackupOp borg extract "::$snapshot_id" "$stripped" ) + ( cd "$target_dir" && runBackupOp "${ns_prefix[@]}" borg extract "::$snapshot_id" "$stripped" ) rc=$? else - ( cd "$target_dir" && runBackupOp borg extract "::$snapshot_id" ) + ( cd "$target_dir" && runBackupOp "${ns_prefix[@]}" borg extract "::$snapshot_id" ) rc=$? fi borgEnvUnset diff --git a/scripts/backup/engine/engine_dispatch.sh b/scripts/backup/engine/engine_dispatch.sh index f834c43..53fb303 100644 --- a/scripts/backup/engine/engine_dispatch.sh +++ b/scripts/backup/engine/engine_dispatch.sh @@ -97,6 +97,29 @@ engineDumpFile() { local i="$1"; shift; engineDispatch "$(engineForL # has no adapter on purpose. A missing adapter is therefore a quiet "no" rather # than engineDispatch's error — callers fall back to restoring in place, which # is correct for borg and merely conservative elsewhere. +# Prefix that lets an unprivileged restore put back the ownership a file had +# when it was backed up. Engine-neutral: restic, borg and kopia all extract as +# the backup user, and all three lose : without it. +# +# The mapping itself lives in backup/engine/restic-userns-exec, because it needs +# three id ranges at once and `unshare` takes one per option. The checks here +# only decide whether to reach for it; the helper re-checks and falls back to +# running the command plainly if anything is missing. +backupUsernsPrefix() +{ + local usr="${docker_install_user:-dockerinstall}" + local helper="${install_scripts_dir%/}/backup/engine/restic-userns-exec" + [[ -r "$helper" ]] || return 0 + command -v unshare >/dev/null 2>&1 || return 0 + command -v newuidmap >/dev/null 2>&1 || return 0 + command -v newgidmap >/dev/null 2>&1 || return 0 + # No subuid range (rooted mode, or a hand-rolled account) — nothing to map, + # so leave the call exactly as it was rather than guess. + grep -q "^${usr}:" /etc/subuid 2>/dev/null || return 0 + grep -q "^${usr}:" /etc/subgid 2>/dev/null || return 0 + printf '%s\n' bash "$helper" +} + engineSnapshotPaths() { local i="$1"; shift local fn; fn="$(engineForLocation "$i")SnapshotPaths" diff --git a/scripts/backup/engine/kopia_restore.sh b/scripts/backup/engine/kopia_restore.sh index 0eca47a..254ff80 100644 --- a/scripts/backup/engine/kopia_restore.sh +++ b/scripts/backup/engine/kopia_restore.sh @@ -24,7 +24,13 @@ kopiaRestoreSnapshot() final_target="$target_dir/${include_path#/}" runFileOp mkdir -p "$final_target" fi - runBackupOp kopia snapshot restore "$snapshot_id" "$final_target" + # Restore runs as the backup user with no CAP_CHOWN; without the namespace + # every : file comes back owned by the backup + # user and the app cannot write its own data. See restic-userns-exec. + local ns_prefix=() + mapfile -t ns_prefix < <(backupUsernsPrefix) + + runBackupOp "${ns_prefix[@]}" kopia snapshot restore "$snapshot_id" "$final_target" local rc=$? kopiaEnvUnset return $rc diff --git a/scripts/backup/engine/restic_restore.sh b/scripts/backup/engine/restic_restore.sh index 7344027..1c1c661 100644 --- a/scripts/backup/engine/restic_restore.sh +++ b/scripts/backup/engine/restic_restore.sh @@ -25,28 +25,6 @@ # inner root, which IS that user on the host, so those files already land with # exactly the right owner. resticRestoreErrorsAreBenign below is what keeps that # from being reported as a failed restore. -_resticUsernsPrefix() -{ - local usr="${docker_install_user:-dockerinstall}" - - # The mapping itself lives in restic-userns-exec, because it needs three id - # ranges at once and `unshare` takes one per option — see that file. The - # checks here only decide whether to reach for it at all; it re-checks and - # falls back to running the command plainly if anything is missing. - local helper="${install_scripts_dir%/}/backup/engine/restic-userns-exec" - [[ -r "$helper" ]] || return 0 - command -v unshare >/dev/null 2>&1 || return 0 - command -v newuidmap >/dev/null 2>&1 || return 0 - command -v newgidmap >/dev/null 2>&1 || return 0 - - # No subuid range (rooted mode, or a hand-rolled account) — nothing to map, - # so leave the call exactly as it was rather than guess. - grep -q "^${usr}:" /etc/subuid 2>/dev/null || return 0 - grep -q "^${usr}:" /etc/subgid 2>/dev/null || return 0 - - printf '%s\n' bash "$helper" -} - # True when every error restic reported is the expected "cannot map the backup # user's own uid" one described above. Anything else — a missing pack, a full # disk, a permission problem on the target — must still fail the restore. @@ -82,7 +60,7 @@ resticRestoreSnapshot() isNotice "Restoring ${snapshot_id:0:8} from $(resticLocationName "$idx") → $target_dir" local ns_prefix=() - mapfile -t ns_prefix < <(_resticUsernsPrefix) + mapfile -t ns_prefix < <(backupUsernsPrefix) # Output is captured (not streamed) so the benign-error check below can read # it; it is echoed straight back afterwards, so the operator sees the same diff --git a/scripts/source/files/arrays/function_manifest.sh b/scripts/source/files/arrays/function_manifest.sh index e0c3858..1198868 100644 --- a/scripts/source/files/arrays/function_manifest.sh +++ b/scripts/source/files/arrays/function_manifest.sh @@ -254,6 +254,7 @@ declare -gA LP_FN_MAP=( [backupSshKeyRefreshUi]="backup/locations/location_ssh.sh" [backupSshKeySet]="backup/locations/location_ssh.sh" [backupSystemConfig]="backup/system/backup_system.sh" + [backupUsernsPrefix]="backup/engine/engine_dispatch.sh" [backupVerifySnapshot]="backup/verify/backup_verify.sh" [_bookstackArtisan]="bookstack/scripts/bookstack_auth.sh" [bookstack_install_post_start]="bookstack/scripts/bookstack_install_hooks.sh" @@ -904,7 +905,6 @@ declare -gA LP_FN_MAP=( [resticSnapshotPaths]="backup/engine/restic_snapshots.sh" [resticSnapshotsJson]="backup/engine/restic_snapshots.sh" [resticSystemSnapshotsJson]="backup/engine/restic_snapshots.sh" - [_resticUsernsPrefix]="backup/engine/restic_restore.sh" [restoreAppRunHook]="restore/restore_app_hooks.sh" [restoreAppStart]="restore/restore_app_start.sh" [restoreDbRehydratePreStart]="backup/db/backup_db.sh" @@ -1493,6 +1493,7 @@ declare -gA LP_FN_ROOT=( [backupSshKeyRefreshUi]="scripts" [backupSshKeySet]="scripts" [backupSystemConfig]="scripts" + [backupUsernsPrefix]="scripts" [backupVerifySnapshot]="scripts" [_bookstackArtisan]="containers" [bookstack_install_post_start]="containers" @@ -2143,7 +2144,6 @@ declare -gA LP_FN_ROOT=( [resticSnapshotPaths]="scripts" [resticSnapshotsJson]="scripts" [resticSystemSnapshotsJson]="scripts" - [_resticUsernsPrefix]="scripts" [restoreAppRunHook]="scripts" [restoreAppStart]="scripts" [restoreDbRehydratePreStart]="scripts" @@ -2770,6 +2770,7 @@ backupSshKeyPublic() { unset -f backupSshKeyPublic; __lpAutoload "${install_scri backupSshKeyRefreshUi() { unset -f backupSshKeyRefreshUi; __lpAutoload "${install_scripts_dir}backup/locations/location_ssh.sh"; backupSshKeyRefreshUi "$@"; } backupSshKeySet() { unset -f backupSshKeySet; __lpAutoload "${install_scripts_dir}backup/locations/location_ssh.sh"; backupSshKeySet "$@"; } backupSystemConfig() { unset -f backupSystemConfig; __lpAutoload "${install_scripts_dir}backup/system/backup_system.sh"; backupSystemConfig "$@"; } +backupUsernsPrefix() { unset -f backupUsernsPrefix; __lpAutoload "${install_scripts_dir}backup/engine/engine_dispatch.sh"; backupUsernsPrefix "$@"; } backupVerifySnapshot() { unset -f backupVerifySnapshot; __lpAutoload "${install_scripts_dir}backup/verify/backup_verify.sh"; backupVerifySnapshot "$@"; } _bookstackArtisan() { unset -f _bookstackArtisan; __lpAutoload "${install_containers_dir}bookstack/scripts/bookstack_auth.sh"; _bookstackArtisan "$@"; } bookstack_install_post_start() { unset -f bookstack_install_post_start; __lpAutoload "${install_containers_dir}bookstack/scripts/bookstack_install_hooks.sh"; bookstack_install_post_start "$@"; } @@ -3420,7 +3421,6 @@ resticSnapshotListFiles() { unset -f resticSnapshotListFiles; __lpAutoload "${in resticSnapshotPaths() { unset -f resticSnapshotPaths; __lpAutoload "${install_scripts_dir}backup/engine/restic_snapshots.sh"; resticSnapshotPaths "$@"; } resticSnapshotsJson() { unset -f resticSnapshotsJson; __lpAutoload "${install_scripts_dir}backup/engine/restic_snapshots.sh"; resticSnapshotsJson "$@"; } resticSystemSnapshotsJson() { unset -f resticSystemSnapshotsJson; __lpAutoload "${install_scripts_dir}backup/engine/restic_snapshots.sh"; resticSystemSnapshotsJson "$@"; } -_resticUsernsPrefix() { unset -f _resticUsernsPrefix; __lpAutoload "${install_scripts_dir}backup/engine/restic_restore.sh"; _resticUsernsPrefix "$@"; } restoreAppRunHook() { unset -f restoreAppRunHook; __lpAutoload "${install_scripts_dir}restore/restore_app_hooks.sh"; restoreAppRunHook "$@"; } restoreAppStart() { unset -f restoreAppStart; __lpAutoload "${install_scripts_dir}restore/restore_app_start.sh"; restoreAppStart "$@"; } restoreDbRehydratePreStart() { unset -f restoreDbRehydratePreStart; __lpAutoload "${install_scripts_dir}backup/db/backup_db.sh"; restoreDbRehydratePreStart "$@"; }