backup: give borg and kopia the same ownership mapping as restic
The user-namespace prefix that lets an unprivileged restore put back a file's original owner was only wired into restic. borg extract and kopia snapshot restore run as the same backup user with the same lack of CAP_CHOWN, so both lost <container-uid>:<backup-user> exactly the way restic did — an app whose data comes back owned by the backup user cannot write it, which is how grafana kept dying with "attempt to write a readonly database". borg is quieter about it than restic: it does not print an "ignoring error" line at all, so there was nothing to notice. Move the prefix to engine_dispatch.sh as backupUsernsPrefix — it was never restic-specific — and use it from all three engines. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
17316b7fe2
commit
226ebe1717
@ -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 <container-uid>:<backup-user>
|
||||
# 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
|
||||
|
||||
@ -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 <container-uid>:<backup-user> 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"
|
||||
|
||||
@ -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 <container-uid>:<backup-user> 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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 "$@"; }
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user