# felhom-agent sudoers allowlist — the NARROW host-root surface (slice 5 Phase B, doc 03 §3/§7; narrowed R-861). # # Install as a drop-in: /etc/sudoers.d/felhom-agent (mode 0440, root:root), validated with `visudo -cf`. It rides the # signed config bundle (R-840). The agent runs as the non-root `felhom-agent` user and shells out via `sudo -n` with # FIXED argument vectors (no shell). # # R-861 (agent v0.146.0) — EXACT PATTERNS, NOT GLOBS. A sudoers `*` in the ARGUMENTS also matches spaces, so # `pct set [0-9]* -onboot 1` matched `pct set 100 --dev0 /dev/sda -onboot 1` (a raw host disk for the guest), and # `mount --bind /mnt/*/felhom-data /mnt/felhom-drives/*` matched a `..` path onto /etc. Every argument list that varies # is now a sudo regular expression (`^...$`, sudo >= 1.9.10; Debian 13 / PVE 9 ship 1.9.16): one value per slot, a # fixed character set, no `..`, no extra argument. Lines with no variable part stay literal. The patterns are pinned # by the capability manifest (every real call must match: TestManifestCoveredBySudoers) and by injection cases that # must NOT match (configs/test_sudoers_patterns.py, and live with `sudo -l -U felhom-agent` on the demo boxes). # # R-861 — NO FILE THE AGENT WROTE IS INSTALLED WHERE ROOT READS IT. The `install` lines are gone: a mount unit, a # dnsmasq drop-in, the WireGuard config and the OOB sshd config + key go through `felhom-priv-apply`, a root wrapper # from the bundle that checks the CONTENT against the agent's own renderers; the guest pre-start hook and the shared # drive parent are FIXED files that come with the bundle itself; the agent binary is replaced only by an # operator-signed agent_update that `felhom-os-apply` verifies as root (`felhom-selfupdate-guarded apply` is no longer # here). The two remaining root runs of agent code (FELHOM_ESCROW, the guest hook) therefore run only a signed binary. # # Binary paths MUST match the agent config (privileged.systemctl/install/smartctl/lvs). A missing/declined entry # degrades the agent with a warning (SMART→UNKNOWN, mount→logged error), it does not crash; the capability probe # reports it to the hub. Cmnd_Alias FELHOM_MOUNT = \ /usr/local/sbin/felhom-priv-apply ^unit mnt-[A-Za-z0-9_.\\-]+\.(mount|automount)$, \ /usr/bin/systemctl daemon-reload, \ /usr/bin/systemctl ^enable --now -- mnt-[A-Za-z0-9_.\\-]+\.mount$, \ /usr/bin/systemctl ^disable -- mnt-[A-Za-z0-9_.\\-]+\.mount$, \ /usr/bin/systemctl ^stop -- mnt-[A-Za-z0-9_.\\-]+\.mount$ Cmnd_Alias FELHOM_DISK = \ /usr/sbin/smartctl ^-a -j /dev/(sd[a-z]+|nvme[0-9]+n[0-9]+|vd[a-z]+|hd[a-z]+)$, \ /usr/sbin/lvs ^--reportformat json --units b -o lv_name\,data_percent\,metadata_percent -- [A-Za-z0-9_.+-]+(/[A-Za-z0-9_.+-]+)?$, \ /usr/sbin/pvs --reportformat json --noheadings -o pv_name, \ /usr/sbin/zpool status -P # Provisioning back-half (slice 8A, doc 03 §6): populate a guest's bootstrap config mount # host-side (internal/provision). These are host-root ops the API token cannot do — a bind mount # is root@pam-only, and the chown maps the 0600 bootstrap.json to the unprivileged-LXC guest-root # (uid/gid 100000, spike gotcha 1). The host dir is AGENT-OWNED state under /var/lib/felhom-agent/ # (the wildcard only ever names a path the agent itself created), and the bootstrap file the agent # writes there is the only thing these touch. ':' is escaped per sudoers grammar. Cmnd_Alias FELHOM_PROVISION = \ /usr/bin/chown ^-R 100000\:100000 /var/lib/felhom-agent/guests/[0-9]+(/bootstrap)?$, \ /usr/sbin/pct ^set [0-9]+ -mp[0-9]+ /var/lib/felhom-agent/guests/[0-9]+/bootstrap\,mp\=/[A-Za-z0-9/_.-]+(\,ro\=1)?$, \ /usr/sbin/pct ^set [0-9]+ -onboot 1$ # Disk inspection + format (slice 8C + Impl-1). blkid/lsblk read the device's data-bearing evidence # (the agent decides data-bearing-ness from THIS, never the caller's claim). Format goes ONLY through # felhom-mkfs-guarded (Impl-1 Part B): raw mkfs.* is NO LONGER allowlisted, so even a bad agent cannot # mkfs the OS disk — the wrapper re-checks the catastrophic cases (system disk / LVM PV / foreign mount) # as root and refuses, and the agent's unclaimed-disk filter (claim.go) is the primary guard above it. Cmnd_Alias FELHOM_FORMAT = \ /usr/sbin/blkid ^-p -o export /dev/[^ ]+$, \ /usr/bin/lsblk ^-J -o NAME\,FSTYPE\,PTTYPE\,MOUNTPOINT /dev/[^ ]+$, \ /usr/local/sbin/felhom-mkfs-guarded ^/dev/[^ ]+ (ext4|xfs)$ # LAN split-horizon resolver (internal/lanresolver): the agent manages a host-side dnsmasq that # answers *. with each guest's live LAN IP. A felhom-*.conf drop-in reaches /etc/dnsmasq.d # only through felhom-priv-apply, which allows exactly the lines the resolver renders (R-861: a `dhcp-script=` would # run as root); the two `pct exec` reads are FIXED command vectors # (the guest's eth0 IPv4 + the controller's pulled controller.yaml for the domain) — NOT a general # `pct exec`. systemctl is scoped to the dnsmasq unit only. The agent never edits /etc/resolv.conf. Cmnd_Alias FELHOM_DNSMASQ = \ /usr/bin/apt-get install -y -q dnsmasq, \ /usr/local/sbin/felhom-priv-apply ^dnsmasq /tmp/felhom-resolver-[0-9]+\.conf felhom-[a-z0-9][a-z0-9._-]*\.conf$, \ /usr/bin/systemctl enable --now dnsmasq, \ /usr/bin/systemctl reload dnsmasq, \ /usr/bin/systemctl restart dnsmasq, \ /usr/bin/rm ^-f /etc/dnsmasq\.d/felhom-[a-z0-9][a-z0-9._-]*\.conf$, \ /usr/sbin/pct ^exec [0-9]+ -- ip -4 -o addr show dev eth0$, \ /usr/sbin/pct ^exec [0-9]+ -- docker exec felhom-controller cat /opt/docker/felhom-controller/controller\.yaml$ # Guest mountpoint lifecycle (intermediary-mount re-architecture + C1 net). The pre-start self-heal hook is a FIXED file # from the config bundle (/var/lib/vz/snippets/felhom-guest-hook.sh — R-861: the agent no longer installs it from /tmp; # Proxmox runs it as root at every guest start). The agent only registers it per guest, deletes a dead mountpoint slot # (the B3 C1 fix) and reboots a guest to activate binds — each with an exact vmid / slot. Cmnd_Alias FELHOM_GUESTHOOK = \ /usr/sbin/pct ^set [0-9]+ --hookscript local\:snippets/felhom-guest-hook\.sh$, \ /usr/sbin/pct ^set [0-9]+ --delete mp[0-9]+$, \ /usr/sbin/pct ^reboot [0-9]+$ # Intermediary mount model (the drive hot-swap re-architecture). The agent keeps a SHARED host parent # /mnt/felhom-drives (self-bind + make-shared) and binds/unbinds each drive's felhom-data namespace UNDERNEATH it so the # change propagates into the running guest live. The boot-persistence script + unit are FIXED files from the config # bundle (R-861: the agent no longer installs them from /tmp); the agent only enables the unit. A drive name is one # path segment that cannot start with a dot (no `..`); `lxc-info -n -p -H` resolves the guest init PID for the # GuestSeesMount check; `make-private` isolates the parent's peer group on FIRST setup only. Cmnd_Alias FELHOM_INTERMEDIARY = \ /usr/bin/mkdir -p /mnt/felhom-drives, \ /usr/bin/mkdir ^-p /mnt/felhom-drives/[A-Za-z0-9_-][A-Za-z0-9_.-]*$, \ /usr/bin/mkdir ^-p /mnt/[A-Za-z0-9_-][A-Za-z0-9_.-]*/felhom-data$, \ /usr/bin/chown ^100000\:100000 /mnt/[A-Za-z0-9_-][A-Za-z0-9_.-]*/felhom-data$, \ /usr/bin/mount --bind /mnt/felhom-drives /mnt/felhom-drives, \ /usr/bin/mount --make-shared /mnt/felhom-drives, \ /usr/bin/mount --make-private /mnt/felhom-drives, \ /usr/bin/mount ^--bind /mnt/[A-Za-z0-9_-][A-Za-z0-9_.-]*/felhom-data /mnt/felhom-drives/[A-Za-z0-9_-][A-Za-z0-9_.-]*$, \ /usr/bin/umount ^/mnt/felhom-drives/[A-Za-z0-9_-][A-Za-z0-9_.-]*$, \ /usr/bin/systemctl enable felhom-shared-parent.service, \ /usr/bin/lxc-info ^-n [0-9]+ -p -H$, \ /usr/sbin/pct ^set [0-9]+ -mp8 /mnt/felhom-drives\,mp\=/mnt/felhom-drives$ # Controller-swap / managed auto-update (Option A, non-root). The agent owns the in-guest controller # image SWAP (it survives the controller being killed mid-swap): read the baked image ref, check the # pre-pulled target is present, rewrite /etc/felhom-controller-image, restart the bootstrap unit, # health-check, roll back on failure. Each grant is bounded — NO general `pct exec` and NO `bash -c`: # cat — read the current image ref (read-only) # docker image inspect * — is the pre-pulled target present? (read-only) # docker inspect -f * — container running/health/image (read-only; `*` spans the -f template # + container across spaces, spike-confirmed) # systemctl restart — re-run the golden's bootstrap (the only state change) # tee — WRITE the ref; content is fed on STDIN (no shell, no interpolation), # the agent strict-validates the ref (controllerImageRe) before the write. # Validated GO: felhom.eu/documentation/audits/SPIKE-controllerswap-narrow-grants-2026-06-29.md. Cmnd_Alias FELHOM_CONTROLLERSWAP = \ /usr/sbin/pct ^exec [0-9]+ -- cat /etc/felhom-controller-image$, \ /usr/sbin/pct ^exec [0-9]+ -- docker image inspect gitea\.dooplex\.hu/admin/felhom-controller\:[0-9]+\.[0-9]+\.[0-9]+$, \ /usr/sbin/pct ^exec [0-9]+ -- docker inspect -f .+ (felhom-controller|cloudflared)$, \ /usr/sbin/pct ^exec [0-9]+ -- systemctl restart felhom-controller-bootstrap\.service$, \ /usr/sbin/pct ^exec [0-9]+ -- tee /etc/felhom-controller-image$ # Stale-lock recovery (F2-b, v0.49.0). A host reboot DURING a vzdump backup leaves the guest with a # `snapshot-delete`/`backup` lock + `onboot:1` then can't start it → the customer box stays DOWN. The # agent clears the STALE lock at startup (only when no vzdump is in-flight). `pct unlock` is the one op # with no API equivalent (snapshot-delete + start go through the API token); the agent fine-validates the # vmid (numeric) before exec — the `[0-9]*` is the coarse allowlist. Cmnd_Alias FELHOM_STALELOCK = \ /usr/sbin/pct ^unlock [0-9]+$ # Restore-test scratch teardown (F-LEAK, Campaign 8, v0.110.0). A restore-test whose restore FAILS # leaves a scratch guest the API token CANNOT destroy: `FelhomAgentGuest` is granted at /pool/felhom and # a guest joins that pool only when its restore COMPLETES, so a failed restore leaves a pool-less guest # out of reach (403 VM.Allocate) holding its disks until a human removes it. # # TWO API-SIDE FIXES WERE TRIED AND BOTH REFUTED LIVE on 2026-07-28, which is why this grant exists: # 1. Adopt the stranded guest into the pool, then retry. `PUT /pools/{pool}` ALSO requires # VM.Allocate on the VM being added — pool membership cannot bootstrap its own authority. # 2. Grant FelhomAgentGuest per-path at /vms/990000..990009. Durable for exactly one use per slot: # PVE's own destroy path calls `AccessControl::remove_vm_access($vmid)` (LXC.pm:906), which DELETES # every ACL at /vms/ (AccessControl.pm:1898). The grant is consumed by the operation it # authorises, so after ten teardowns the band is ungranted and the defect returns. # # WHY THIS IS THE TIGHTEST AVAILABLE FENCE, not a widening: sudo matches the vmid LITERALLY, so # `99000[0-9]` is exactly the ten-slot scratch band the restore-test picks from — nothing else. There is # no `[0-9]*` coarse allowlist here on purpose: unlike `pct unlock`, this op DESTROYS, so the band must # be in the policy and not merely validated in the agent. Even a compromised agent asking for # `pct destroy 9201` is refused by sudo itself. Unlike an ACL, a sudoers rule is not consumed by use. # The agent re-checks the band in code before exec (defence in depth); this is the outer fence. Cmnd_Alias FELHOM_SCRATCH_TEARDOWN = \ /usr/sbin/pct destroy 99000[0-9] --purge # Network storage / NAS (Part A1, SPIKE-nas-storage-2026-06-29). The agent mounts a customer NAS share # HOST-SIDE under /mnt/felhom-drives/ via a systemd .automount (+ .mount) pair so it propagates # into the guest through the existing shared bind (an unprivileged LXC cannot mount NFS/CIFS itself). # A NAS is NOT a drive — no durable-id, no SMART, no wipe; these grants only install/enable/remove the # unit pair. The agent fine-validates every value (share name, server, export, uid/gid, creds path) before # any unit is rendered (internal/storage/netmount.go ValidateNetworkMountSpec); the unit FILE reaches # /etc/systemd/system only through `felhom-priv-apply unit` (FELHOM_MOUNT), which requires nosuid,nodev on a network # share (R-861). The `.mount` enable/disable/stop reuse FELHOM_MOUNT; this alias adds the # `.automount` variants + the unit-file removal. The unit FILE name is the systemd-escaped mountpoint, # which always begins `mnt-felhom` (the mountpoint is /mnt/felhom-drives/), so the rm glob is scoped # to felhom mount units only. mkdir of the mountpoint reuses FELHOM_INTERMEDIARY's /mnt/felhom-drives/*. # CAMPAIGN-3 additions (loud, per the no-widening rule): # - `systemctl reset-failed -- mnt-felhom*`: F10 (CRITICAL) — a NAS automount that hit # mount-start-limit-hit during an outage was re-armable by NO platform path; the reassert now # reset-failed's the stuck unit before `enable --now` (which the start-limit otherwise refuses), # and RemoveNetworkMount clears failed-state residue (F2). Scoped to felhom mount units (the unit # name is the systemd-escaped mountpoint, always beginning `mnt-felhom`). reset-failed only clears # a unit's failed latch — it cannot start/stop/alter anything. # - `rmdir /mnt/felhom-drives/*`: F1 — remove the now-empty mountpoint dir a removed share leaves # behind (the campaign accumulated 10 stub-shaped leftovers). rmdir ONLY (never rm -rf): it refuses # a non-empty dir, so unexpected data is preserved, not destroyed — a fail-safe grant. Cmnd_Alias FELHOM_NETMOUNT = \ /usr/bin/systemctl ^enable --now -- mnt-[A-Za-z0-9_.\\-]+\.automount$, \ /usr/bin/systemctl ^disable -- mnt-[A-Za-z0-9_.\\-]+\.automount$, \ /usr/bin/systemctl ^stop -- mnt-[A-Za-z0-9_.\\-]+\.automount$, \ /usr/bin/systemctl ^reset-failed -- mnt-felhom[A-Za-z0-9_.\\-]*\.(mount|automount)$, \ /usr/bin/rmdir ^/mnt/felhom-drives/[A-Za-z0-9_-][A-Za-z0-9_.-]*$, \ /usr/bin/rm ^-f /etc/systemd/system/mnt-felhom[A-Za-z0-9_.\\-]*\.(mount|automount)$ # Offsite WG tunnel (S3, doc 06 §3.3). The agent manages wg-quick@wg-felhom as an agent-managed # host service (the dnsmasq/lanresolver shape): conf staged in the agent-owned StateDir (never # /tmp), installed 0600 to the FIXED destination by `felhom-priv-apply wg`, which refuses any key renderConf never # writes (R-861: PostUp/PreUp run as root under wg-quick), unit enable/restart/disable. The ONLY wg read # is `latest-handshakes` — `wg show dump` is FORBIDDEN everywhere (its interface line # carries the PRIVATE KEY; the S1 session-log incident). Source and destination are fixed in the wrapper. Cmnd_Alias FELHOM_WG = \ /usr/bin/apt-get install -y -q wireguard-tools, \ /usr/local/sbin/felhom-priv-apply wg, \ /usr/bin/systemctl enable --now wg-quick@wg-felhom, \ /usr/bin/systemctl restart wg-quick@wg-felhom, \ /usr/bin/systemctl disable --now wg-quick@wg-felhom, \ /usr/bin/wg show wg-felhom latest-handshakes # Agent self-update (TASK D1; R-861). The A/B flip (`felhom-selfupdate-guarded apply`) is NO LONGER the agent's: the # agent hands the operator-SIGNED agent_update to felhom-os-apply (FELHOM_OSAPPLY, mode agent_update), which verifies # the signature as root and only then runs the flip. Until v0.146.0 the agent passed the sha itself, so a compromised # agent could install any binary — the binary FELHOM_ESCROW and the guest hook run as root. `commit` (clear the pending # marker) and `rollback` (pending-guarded revert, normally run by felhom-agent-rollback.service) stay. Cmnd_Alias FELHOM_SELFUPDATE = \ /usr/local/sbin/felhom-selfupdate-guarded commit, \ /usr/local/sbin/felhom-selfupdate-guarded rollback # Dedicated OOB sshd (TASK H1). The agent manages felhom-sshd like wg-felhom/dnsmasq: it RENDERS the # config (Port from its claim) + the operator's authorized_keys, validates with `sshd -t`, and reloads # (never restart-on-change [SF-2]). Both files reach /etc/felhom-sshd only through felhom-priv-apply (R-861): the config # must be the ONE template with only the Port varying (an AuthorizedKeysFile the agent owns + `StrictModes no` would be # a root login), the key file one plain public key without options. `sshd -t/-T` are the validate/discover reads. The # systemctl verbs are SCOPED to felhom-sshd only. reset-failed precedes a deliberate restart [SF-5]. # NOTHING here can touch the stock sshd, :22, or /etc/ssh. Cmnd_Alias FELHOM_SSHD = \ /usr/local/sbin/felhom-priv-apply sshd-config, \ /usr/local/sbin/felhom-priv-apply sshd-key, \ /usr/sbin/sshd -t -f /var/lib/felhom-agent/felhom-sshd/sshd_config, \ /usr/sbin/sshd -t -f /etc/felhom-sshd/sshd_config, \ /usr/sbin/sshd -T -f /etc/felhom-sshd/sshd_config, \ /usr/bin/systemctl enable --now felhom-sshd, \ /usr/bin/systemctl reload felhom-sshd, \ /usr/bin/systemctl restart felhom-sshd, \ /usr/bin/systemctl reset-failed felhom-sshd, \ /usr/bin/wg show wg-felhom latest-handshakes # PBS DR tier apply (slice 2, SPIKE-pbs-tier-provisioning-2026-07-10 §2b). Storage-entry # lifecycle is /storage-ROOT-gated in the PVE API (spike Probe 1: create/modify/delete all check # Datastore.Allocate on /storage), so the agent token cannot do it — this wrapper is the pinned # vector. THE SET-ONLY LAW: the wrapper contains NO deletion path (entry deletion destroys the # client encryption key = un-decryptable backups); verbs are create/reconcile/grant only. The # token secret rides the wrapper's STDIN — sudo logs argv, so it must never appear here. The # agent fine-validates every field (charset + descriptor equality) before exec; these globs are # the coarse allowlist. # # `read` (R-39 leg b, agent v0.91.0) is the ONE added verb. It prints a token secret to stdout and # performs no mutation. It exists because the agent writes that file through this wrapper but could # never read it back (/etc/pve/priv is 0700 root:www-data), leaving its PBS verify loop permanently # blind to an `applied`-but-401 tier. It is NOT a general file-read: the wrapper pins the directory # and prefix-asserts the resolved path, and the id grammar admits no slash. The secret goes to # STDOUT, never argv — sudo logs argv. # E-2a: the backup-target storage shim. Creating a PVE storage needs Datastore.Allocate at /storage # and the grant needs Permissions.Modify -- the agent holds NEITHER by design (blast-radius # containment; Permissions.Modify would let it rewrite its own authority). Both live behind this # fixed-vocabulary root shim instead, exactly like the mkfs and pbs-apply wrappers. The wrapper has # NO storage-removal path, enforces is_mountpoint 1, and refuses a target on the root device. Cmnd_Alias FELHOM_BACKUPTARGET = \ /usr/local/sbin/felhom-backup-target-apply create *, \ /usr/local/sbin/felhom-backup-target-apply grant * Cmnd_Alias FELHOM_PBSDR = \ /usr/local/sbin/felhom-pbs-apply create *, \ /usr/local/sbin/felhom-pbs-apply reconcile *, \ /usr/local/sbin/felhom-pbs-apply grant *, \ /usr/local/sbin/felhom-pbs-apply read * # OOB nft belt (TASK H1). The STATIC table `inet felhom_oob` is installed once by host-install; the # agent mutates ONLY its two SETS — @operator_ips (the operator /32) + @ssh_port (the claimed port). # SET ELEMENTS ONLY [trap 4]: NO `nft add rule`, NO `nft -f`, NO `flush ruleset/table` — a rule grant # would let the agent firewall anything. The agent fine-validates every element (netip / int range) # before exec; the trailing wildcards are the coarse allowlist (values only). Cmnd_Alias FELHOM_OOB = \ /usr/sbin/nft list set inet felhom_oob operator_ips, \ /usr/sbin/nft list set inet felhom_oob ssh_port, \ /usr/sbin/nft flush set inet felhom_oob operator_ips, \ /usr/sbin/nft flush set inet felhom_oob ssh_port, \ /usr/sbin/nft ^add element inet felhom_oob operator_ips \{ [0-9.]+(/[0-9]+)? \}$, \ /usr/sbin/nft ^add element inet felhom_oob ssh_port \{ [0-9]+ \}$ # Escrow ceremony (controller-driven, TASK 2026-07-13; mechanics validated by # SPIKE-controller-escrow-2026-07-13). ONE fixed argv — sudoers matches the argument vector # byte-for-byte (spike §2.2): any alteration (value, extra flag, order, config path) is refused. # --config pinned: env_reset strips FELHOM_AGENT_CONFIG and the pin closes alternate-config # injection. R rides the subprocess stdout pipe only; sudo logs argv = secrets-free. The argv # MUST stay byte-identical to escrow.CeremonyArgs() (internal/escrow/ceremony.go) — the # capability manifest entry + TestManifestCoveredBySudoers lock the three copies together. Cmnd_Alias FELHOM_ESCROW = \ /usr/local/bin/felhom-agent --config /etc/felhom-agent/agent.json --selftest=escrow-create --upload --output=json # Node self-heal (CAMPAIGN-3 Part 6, F12-class defense in depth). The ONE fixed unit the appliance # watchdog may (re)start when a boot leaves networking down — the exact command the morning recovery # ran by hand after the F12 host loss. FIXED unit, no glob: this grant alone cannot harm — starting # networking.service is precisely what the boot should have done. The remedy is ALSO code-gated on # deployment_mode="appliance" (the Manager refuses to invoke it on a byo host); the sudoers grant is # the coarse floor, the mode gate is the fine one. Cmnd_Alias FELHOM_SELFHEAL = \ /usr/bin/systemctl start networking.service # Guest-network watchdog (internal/guestnet, R-54). The guest's DHCP client is unsupervised — when it # died on 2026-07-20 the box lost its address ~80 minutes later and went off the internet for 1h15m # (INCIDENT-guest-dhclient-killed-2026-07-20). Four FIXED read vectors plus ONE fixed heal vector; the # heal is the incident's own restored invocation, byte for byte. This is NOT a general `pct exec`: every # argument after the numeric vmid is a literal, so the grant cannot be widened by anything the guest or # the hub says. The address read is deliberately NOT duplicated here — it is already FELHOM_DNSMASQ's, # and the same command must not be granted twice under two names. # OS updates, guest fast lane (`11-os-updates.md` §5.4.1, agent v0.140.0). The ONLY entry: the root wrapper with # one plan file in the agent's own os/ dir. Every safety rule (no removal, no downgrade, no new or unlisted package, # Debian origin only, the box's own customer guest only) lives in the wrapper, red-proved per rule # (configs/test_felhom_os_apply.py). The agent gets NO apt grant of its own. Cmnd_Alias FELHOM_OSAPPLY = \ /usr/local/sbin/felhom-os-apply --plan /var/lib/felhom-agent/os/plan-*.json Cmnd_Alias FELHOM_GUESTNET = \ /usr/sbin/pct ^exec [0-9]+ -- ip route show default$, \ /usr/sbin/pct ^exec [0-9]+ -- cat /etc/network/interfaces$, \ /usr/sbin/pct ^exec [0-9]+ -- pgrep -x dhclient$, \ /usr/sbin/pct ^exec [0-9]+ -- dhclient -pf /run/dhclient\.eth0\.pid -lf /var/lib/dhcp/dhclient\.eth0\.leases eth0$ felhom-agent ALL=(root) NOPASSWD: FELHOM_MOUNT, FELHOM_DISK, FELHOM_PROVISION, FELHOM_FORMAT, FELHOM_DNSMASQ, FELHOM_GUESTHOOK, FELHOM_INTERMEDIARY, FELHOM_CONTROLLERSWAP, FELHOM_STALELOCK, FELHOM_NETMOUNT, FELHOM_WG, FELHOM_SELFUPDATE, FELHOM_SSHD, FELHOM_OOB, FELHOM_PBSDR, FELHOM_BACKUPTARGET, FELHOM_SELFHEAL, FELHOM_ESCROW, FELHOM_GUESTNET, FELHOM_SCRATCH_TEARDOWN, FELHOM_OSAPPLY