#!/bin/bash # Run a command in a user namespace whose id maps let it restore file ownership. # # restic-userns-exec restic restore --target … # # Restore runs as the backup user, which has no CAP_CHOWN, so it cannot put back # the uid a file had when it was backed up. A user namespace solves that — but # only if every id involved is mapped, and LibrePortal needs three mappings at # once while `unshare` accepts exactly ONE range per option. # # That limit is why the previous one-line prefix silently did not work: # # unshare --map-root-user --map-users=SUB:SUB:N --map-groups=SUB:SUB:N # # mapped root and the subuid range, but never the backup user's own GID. App # data is written as :, so the group half of every # chown referred to an unmapped id, lchown returned EINVAL, and the file kept # the restoring user's ownership. Grafana's database came back 1002:1002 instead # of 231543:1002 and the app died with "attempt to write a readonly database" — # under a restore that reported success. # # newuidmap/newgidmap can write multi-range maps, so the maps become: # # uid: 0 <- caller (inner root: without this, capabilities are # dropped at exec and chown is not permitted) # SUB.. <- SUB.. (identity, so restic can ask for the stored # container uid by its real number) # gid: caller <- caller (identity: the group half of app-data chowns) # SUB.. <- SUB.. (identity) # # The caller's own UID is deliberately NOT identity-mapped: it is spent on inner # root. A file stored as : therefore fails its chown — and lands # owned by the caller anyway, because that is who inner root is outside. So the # one case this cannot map is the one case that needs no mapping. # # Anything unexpected — no subuid range, no newuidmap, a namespace that will not # start — falls back to running the command plainly, which is what happened # before this existed. set -u (( $# )) || { echo "restic-userns-exec: no command given" >&2; exit 2; } TMPDIR_MADE="" _plain() { [[ -n "$TMPDIR_MADE" ]] && rm -rf "$TMPDIR_MADE"; exec "$@"; } for _bin in unshare newuidmap newgidmap; do command -v "$_bin" >/dev/null 2>&1 || _plain "$@" done _usr=$(id -un 2>/dev/null) || _plain "$@" _u=$(id -u); _g=$(id -g) _uline=$(grep "^${_usr}:" /etc/subuid 2>/dev/null | head -1) _gline=$(grep "^${_usr}:" /etc/subgid 2>/dev/null | head -1) [[ -n "$_uline" && -n "$_gline" ]] || _plain "$@" _ustart="${_uline#*:}"; _ustart="${_ustart%%:*}"; _ucount="${_uline##*:}" _gstart="${_gline#*:}"; _gstart="${_gstart%%:*}"; _gcount="${_gline##*:}" [[ "$_ustart" =~ ^[0-9]+$ && "$_ucount" =~ ^[0-9]+$ ]] || _plain "$@" [[ "$_gstart" =~ ^[0-9]+$ && "$_gcount" =~ ^[0-9]+$ ]] || _plain "$@" (( _ucount > 0 && _gcount > 0 )) || _plain "$@" # The caller's own id has to sit outside its sub-range, or the two entries would # overlap on the outer side and the kernel rejects the whole map. (( _u < _ustart || _u >= _ustart + _ucount )) || _plain "$@" (( _g < _gstart || _g >= _gstart + _gcount )) || _plain "$@" TMPDIR_MADE=$(mktemp -d "${TMPDIR:-/tmp}/lp-userns.XXXXXX") || { TMPDIR_MADE=""; _plain "$@"; } _fifo="$TMPDIR_MADE/gate" mkfifo -m 600 "$_fifo" 2>/dev/null || _plain "$@" # Opened here and inherited there, rather than opened by path in the child: # until the map is written the child's uid is unmapped, so to the filesystem it # is nobody and could not open its own gate. exec 9<>"$_fifo" unshare --user bash -c 'read -r _ <&9; exec "$@"' _ "$@" & _child=$! # The namespace exists before unshare execs, but only just — retry briefly # rather than assume. A write that succeeds cannot be repeated, so stop there. _mapped=0 for _ in $(seq 1 100); do kill -0 "$_child" 2>/dev/null || break if newuidmap "$_child" 0 "$_u" 1 "$_ustart" "$_ustart" "$_ucount" 2>/dev/null; then _mapped=1; break fi sleep 0.02 done if (( _mapped )); then newgidmap "$_child" "$_g" "$_g" 1 "$_gstart" "$_gstart" "$_gcount" 2>/dev/null || _mapped=0 fi if (( ! _mapped )); then kill "$_child" 2>/dev/null wait "$_child" 2>/dev/null _plain "$@" fi printf 'go\n' >&9 wait "$_child" _rc=$? rm -rf "$TMPDIR_MADE" exit "$_rc"