#!/bin/bash #=============================================================================== # felhom-backup-target-apply — the ONLY path the felhom-agent sudoers permits for creating the # whole-guest backup TARGET storage and granting the agent access to it (E-2a). # # WHY A WRAPPER AT ALL. Creating a PVE storage needs `Datastore.Allocate` at `/storage`, and the ACL # grant needs `Permissions.Modify`. The agent holds NEITHER by design — its token is scoped per # storage path for blast-radius containment, and `Permissions.Modify` would let it rewrite its own # authority. Widening the PVE role to make the move possible would trade the entire containment model # for one feature. So the privileged half lives here: a minimal, auditable root shim with a fixed # vocabulary, exactly like felhom-mkfs-guarded and felhom-pbs-apply. # # THE NO-DELETE LAW (inherited from felhom-pbs-apply, same reasoning class). This wrapper contains NO # storage-removal path of any kind. `pvesm remove` on a dir storage does not delete the archives, but # it DOES silently orphan a configured backup tier, and a "cleanup" verb here would be reachable by # any bug in the agent. Retiring a target is a deliberate operator op, not this tool. Grep-assertable; # do not add one. # # THE TWO LAWS E-1 PAID FOR ON LIVE HARDWARE, both enforced here rather than trusted to the caller: # # F-1 the storage path must BE the drive's own mountpoint. A subdirectory fails the agent's # exactMount check, so the target reports `disconnected` FOREVER and its durable id degrades # off the filesystem UUID. Enforced: `mountpoint -q` must pass on the exact path given. # # F-2 --is_mountpoint 1 is not optional. Without it, an unplugged or late-mounting drive leaves a # bare directory on the ROOT filesystem and vzdump writes the whole-guest backup onto the # system drive — the exact device the whole change exists to escape — while PVE reports the # storage `active` and advertises the root filesystem's free space. Proven live: the unguarded # form had already created dump/ on pve-root. Hardcoded below; not a caller-supplied flag. # # Ops (all non-secret; nothing here touches a credential, so nothing arrives on stdin): # create # Create a `dir` storage with content=backup at , is_mountpoint 1. # IDEMPOTENT: an existing entry with the SAME path is accepted (re-run safe, and the # installer re-run path depends on it). An existing entry with a DIFFERENT path is REFUSED # — silently repointing a live backup target is the failure this whole arc closes. # grant # The dual grant: FelhomAgentStore on /storage/ to the agent user AND token (privsep # intersection — a token's rights are the intersection, so granting one is granting neither). # Without it every backup 403s on first run (E-1 finding F-3, found by the first real backup). #=============================================================================== set -euo pipefail die() { echo "felhom-backup-target-apply: REFUSED: $*" >&2; exit 1; } op="${1:-}"; id="${2:-}" [[ -n "$op" && -n "$id" ]] || die "usage: felhom-backup-target-apply [mountpoint]" # Storage id: PVE grammar, conservative. Also the ACL path component — no slashes possible. [[ "$id" =~ ^[A-Za-z][A-Za-z0-9_.-]{0,27}$ ]] || die "bad storage id ($id)" STORECFG=/etc/pve/storage.cfg # current_path_of — the configured `path` of dir storage , or "" when absent/not-a-dir. current_path_of() { awk -v want="dir: $1" ' $0 == want { found=1; next } found && /^[a-z]+: / { exit } found && $1 == "path" { print $2; exit } ' "$STORECFG" 2>/dev/null || true } case "$op" in create) [[ $# -eq 3 ]] || die "create takes " mp="$3" # Absolute, normalized, no traversal, no shell metacharacters. The value reaches pvesm and the # filesystem, so it is validated here rather than assumed well-formed. [[ "$mp" = /* ]] || die "mountpoint must be absolute ($mp)" [[ "$mp" != *".."* ]] || die "mountpoint must not contain .. ($mp)" [[ "$mp" =~ ^[A-Za-z0-9/_.-]+$ ]] || die "mountpoint has unexpected characters ($mp)" [[ "$mp" != "/" ]] || die "refusing / as a backup target" # F-1 + F-2, checked as one: the path must BE a mountpoint right now. A bare directory here is # precisely the silent-retarget shape, and is_mountpoint would make PVE refuse it later anyway — # better to refuse now, with a reason, than to create a storage that can never activate. mountpoint -q "$mp" || die "$mp is not a mountpoint — the backup target must be the drive's OWN mountpoint (F-1), and an unmounted path would silently retarget onto the system drive (F-2)" # Never the system disk: a target on the root filesystem is not drive-loss protection, it is the # thing we are escaping. The root device and the candidate's device are compared, not their paths. root_dev="$(findmnt -no SOURCE / 2>/dev/null || true)" mp_dev="$(findmnt -no SOURCE "$mp" 2>/dev/null || true)" [[ -n "$mp_dev" ]] || die "could not resolve the backing device of $mp" [[ "$mp_dev" != "$root_dev" ]] || die "$mp is backed by the ROOT device ($root_dev) — a backup target there protects against corruption only, never drive loss" existing="$(current_path_of "$id")" if [[ -n "$existing" ]]; then if [[ "$existing" == "$mp" ]]; then echo "felhom-backup-target-apply: storage $id already exists at $mp — nothing to do (idempotent)" >&2 exit 0 fi die "storage $id already exists at $existing — refusing to repoint it at $mp (a live backup target is never silently moved)" fi # is_mountpoint 1 is HARDCODED (F-2). content=backup only: this storage exists for vzdump archives # and must never become a place guests are allocated on. pvesm add dir "$id" --path "$mp" --content backup --is_mountpoint 1 >&2 echo "felhom-backup-target-apply: created dir storage $id at $mp (content=backup, is_mountpoint 1)" >&2 ;; grant) [[ $# -eq 2 ]] || die "grant takes only " # BOTH, always. A privsep token's rights are the intersection of the user's and the token's ACLs, # so granting one of the two grants nothing usable. pveum acl modify "/storage/$id" --users felhom-agent@pve --roles FelhomAgentStore >&2 pveum acl modify "/storage/$id" --tokens 'felhom-agent@pve!agent' --roles FelhomAgentStore >&2 echo "felhom-backup-target-apply: granted FelhomAgentStore on /storage/$id (user + token)" >&2 ;; *) die "unknown op ($op)" ;; esac