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"
|
runFileOp mkdir -p "$target_dir"
|
||||||
|
|
||||||
isNotice "Restoring $snapshot_id from $(resticLocationName "$idx") → $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
|
local rc
|
||||||
if [[ -n "$include_path" ]]; then
|
if [[ -n "$include_path" ]]; then
|
||||||
local stripped="${include_path#/}"
|
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=$?
|
rc=$?
|
||||||
else
|
else
|
||||||
( cd "$target_dir" && runBackupOp borg extract "::$snapshot_id" )
|
( cd "$target_dir" && runBackupOp "${ns_prefix[@]}" borg extract "::$snapshot_id" )
|
||||||
rc=$?
|
rc=$?
|
||||||
fi
|
fi
|
||||||
borgEnvUnset
|
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
|
# 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
|
# than engineDispatch's error — callers fall back to restoring in place, which
|
||||||
# is correct for borg and merely conservative elsewhere.
|
# 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() {
|
engineSnapshotPaths() {
|
||||||
local i="$1"; shift
|
local i="$1"; shift
|
||||||
local fn; fn="$(engineForLocation "$i")SnapshotPaths"
|
local fn; fn="$(engineForLocation "$i")SnapshotPaths"
|
||||||
|
|||||||
@ -24,7 +24,13 @@ kopiaRestoreSnapshot()
|
|||||||
final_target="$target_dir/${include_path#/}"
|
final_target="$target_dir/${include_path#/}"
|
||||||
runFileOp mkdir -p "$final_target"
|
runFileOp mkdir -p "$final_target"
|
||||||
fi
|
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=$?
|
local rc=$?
|
||||||
kopiaEnvUnset
|
kopiaEnvUnset
|
||||||
return $rc
|
return $rc
|
||||||
|
|||||||
@ -25,28 +25,6 @@
|
|||||||
# inner root, which IS that user on the host, so those files already land with
|
# 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
|
# exactly the right owner. resticRestoreErrorsAreBenign below is what keeps that
|
||||||
# from being reported as a failed restore.
|
# 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
|
# 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
|
# 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.
|
# 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"
|
isNotice "Restoring ${snapshot_id:0:8} from $(resticLocationName "$idx") → $target_dir"
|
||||||
|
|
||||||
local ns_prefix=()
|
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
|
# 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
|
# 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"
|
[backupSshKeyRefreshUi]="backup/locations/location_ssh.sh"
|
||||||
[backupSshKeySet]="backup/locations/location_ssh.sh"
|
[backupSshKeySet]="backup/locations/location_ssh.sh"
|
||||||
[backupSystemConfig]="backup/system/backup_system.sh"
|
[backupSystemConfig]="backup/system/backup_system.sh"
|
||||||
|
[backupUsernsPrefix]="backup/engine/engine_dispatch.sh"
|
||||||
[backupVerifySnapshot]="backup/verify/backup_verify.sh"
|
[backupVerifySnapshot]="backup/verify/backup_verify.sh"
|
||||||
[_bookstackArtisan]="bookstack/scripts/bookstack_auth.sh"
|
[_bookstackArtisan]="bookstack/scripts/bookstack_auth.sh"
|
||||||
[bookstack_install_post_start]="bookstack/scripts/bookstack_install_hooks.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"
|
[resticSnapshotPaths]="backup/engine/restic_snapshots.sh"
|
||||||
[resticSnapshotsJson]="backup/engine/restic_snapshots.sh"
|
[resticSnapshotsJson]="backup/engine/restic_snapshots.sh"
|
||||||
[resticSystemSnapshotsJson]="backup/engine/restic_snapshots.sh"
|
[resticSystemSnapshotsJson]="backup/engine/restic_snapshots.sh"
|
||||||
[_resticUsernsPrefix]="backup/engine/restic_restore.sh"
|
|
||||||
[restoreAppRunHook]="restore/restore_app_hooks.sh"
|
[restoreAppRunHook]="restore/restore_app_hooks.sh"
|
||||||
[restoreAppStart]="restore/restore_app_start.sh"
|
[restoreAppStart]="restore/restore_app_start.sh"
|
||||||
[restoreDbRehydratePreStart]="backup/db/backup_db.sh"
|
[restoreDbRehydratePreStart]="backup/db/backup_db.sh"
|
||||||
@ -1493,6 +1493,7 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[backupSshKeyRefreshUi]="scripts"
|
[backupSshKeyRefreshUi]="scripts"
|
||||||
[backupSshKeySet]="scripts"
|
[backupSshKeySet]="scripts"
|
||||||
[backupSystemConfig]="scripts"
|
[backupSystemConfig]="scripts"
|
||||||
|
[backupUsernsPrefix]="scripts"
|
||||||
[backupVerifySnapshot]="scripts"
|
[backupVerifySnapshot]="scripts"
|
||||||
[_bookstackArtisan]="containers"
|
[_bookstackArtisan]="containers"
|
||||||
[bookstack_install_post_start]="containers"
|
[bookstack_install_post_start]="containers"
|
||||||
@ -2143,7 +2144,6 @@ declare -gA LP_FN_ROOT=(
|
|||||||
[resticSnapshotPaths]="scripts"
|
[resticSnapshotPaths]="scripts"
|
||||||
[resticSnapshotsJson]="scripts"
|
[resticSnapshotsJson]="scripts"
|
||||||
[resticSystemSnapshotsJson]="scripts"
|
[resticSystemSnapshotsJson]="scripts"
|
||||||
[_resticUsernsPrefix]="scripts"
|
|
||||||
[restoreAppRunHook]="scripts"
|
[restoreAppRunHook]="scripts"
|
||||||
[restoreAppStart]="scripts"
|
[restoreAppStart]="scripts"
|
||||||
[restoreDbRehydratePreStart]="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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
_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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
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 "$@"; }
|
restoreDbRehydratePreStart() { unset -f restoreDbRehydratePreStart; __lpAutoload "${install_scripts_dir}backup/db/backup_db.sh"; restoreDbRehydratePreStart "$@"; }
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user