Skip to content

ADR-0362: pve01's one privileged path is sudo-over-SSH — the claude@pam API token stays audit-only, and Prime Directive 1's documented command is corrected

Status: Accepted Date: 2026-08-08

Closes Issue #2123 under Epic #886 (vulnscan). Amends Prime Directive 1 in ~/.claude/CLAUDE.md (ADR-0049) and adds a conformance clause to the Documentation Standard. Builds on ADR-0336, which added the pitlab-snapshot helper and the ZFS fallback.

Prime Directive 1 makes a pre-change snapshot mandatory before any configuration change to a pve01 guest. It offered two ways to take one. Measured live on 2026-08-08, both failed as written:

Documented pathWhat actually happensEvidence
PROXMOX_API_TOKEN (claude@pam), described as “API: … or Proxmox REST API via curl”Permission check failed (/vms/109, VM.Snapshot)GET /access/permissions returns Datastore.Audit, Mapping.Audit, Pool.Audit, SDN.Audit, Sys.Audit, VM.Audit, VM.GuestAgent.Audit on / and every sub-path — the built-in PVEAuditor role, nothing else
ssh pve01 pvesh create /nodes/pve01/qemu/<vmid>/snapshot …ipcc_send_rec[N] failed / Unable to load access control list, exit 255the agent’s SSH session lands as unprivileged pit (uid 1001), which cannot open the pmxcfs socket

The working form is ssh pve01 **sudo** pvesh …pit holds passwordless sudo -n on pve01 — and it was used successfully during the run that raised this Issue (snapshot pre-sqlite3-cve-20260803 created, verified and deleted).

The failure mode this creates is worse than an ordinary doc error. Two attempts at a mandatory safety step fail before the third succeeds. Under time pressure, or in an unattended run, that is exactly the point at which the snapshot gets skipped and the directive is quietly violated — and the estate has already paid for a missing rollback point once, when plex01 was patched unprotected and “rolled back” to nothing (Issue #2175, ADR-0336).

The corpus already knew. pve01-pvesh-qm-require-sudo-from-agent-ssh.md had documented the sudo requirement for weeks, and its closing line read: “The snapshot commands documented in the global CLAUDE.md RULE 1 assume root and must be run via sudo from an agent session.” A reference note observing that a prime directive is wrong is not a fix. Nothing routed that observation back to the directive, so the directive stayed wrong.

1. claude@pam stays audit-only. The REST API is a read path and is documented as one. Rejecting the alternative — granting the token VM.Snapshot/VM.PowerMgmt on /vms — keeps the privileged-access register’s least-privilege position intact and matches the claude@pbs Audit-token precedent (ADR-0212). A credential that cannot mutate cannot be misused to mutate, and there is a perfectly good privileged path already.

2. ssh pve01 sudo <cmd> is the single privileged path to pve01, stated as a rule rather than left to be rediscovered. Every pvesh/qm/pct invocation in the directive now carries sudo, including the ones outside the snapshot steps (the guest stop/start around a ZFS rollback). The escalation is explicit and logged by sudo rather than implicit in a credential, which is the better audit posture as well as the working one.

3. Both dead ends are named in the directive, not just the right answer. Prime Directive 1 now says why bare pvesh fails (Unable to load access control list, exit 255) and why the token fails (Permission check failed (…, VM.Snapshot)), with an explicit instruction not to “fix” the latter by widening the token. Naming a wrong path is what stops it being retried: a reader who recognises the error message they just got stops looking for a third option and uses the one that works.

4. pitlab-snapshot remains the sanctioned form and needed no change. It was already built on ssh pve01 sudo pvesh (ADR-0336) and never used the API token, which is why the automation path kept working while the documented manual path did not. Verified end-to-end on 2026-08-08 across both mechanisms and the true-negative — see Consequences.

5. The Documentation Standard gains a conformance clause: a command on a mandatory path is verified executable as written, by the identity that will run it. This is the generalisation, and it is the part that stops the next recurrence. The defect here was not that someone mistyped sudo; it was that a command sat in the highest-privilege document in the estate for months with nobody ever having executed it in the form written, by the user who would execute it. The clause is deliberately narrow — mandatory-path docs (prime directives, runbooks for a required step), not every fenced block — because that is where an unrunnable command converts into a skipped control rather than a moment of mild irritation.

  • Grant claude@pam the VM.Snapshot + VM.PowerMgmt privileges on /vms. Rejected. It widens a credential that currently cannot break anything, to reach a capability a working path already provides. The token’s value is precisely that a leak of it is a disclosure problem and not a control problem.
  • Document both paths and let the caller choose. Rejected — this was the state, and the ambiguity is the defect. Two documented ways, neither verified, produced two failures before a third undocumented one worked. One unambiguous path is the deliverable.
  • Fix only the reference note, not the directive. Rejected as the failure that already happened: the note was correct and had no effect, because the directive is what gets read at the moment of action. Correcting the note again would have re-run the same experiment.
  • Add a sudo-inserting shell alias/wrapper on control01 so the documented bare form works. Rejected: it makes the docs true by making the system lie, is invisible to anyone reading the doc from elsewhere, and breaks the moment a command runs outside that shell (Ansible, cron, a script). Documenting the real command is cheaper and more honest.
  • Make the Documentation Standard clause universal — every fenced command verified. Attractive and rejected as unenforceable: most fenced blocks are illustrative, parameterised, or destructive-by-design, and a clause nobody can meet is a clause that gets ignored everywhere including where it matters. Scoping it to mandatory paths keeps it a real requirement.
  • Prime Directive 1 is executable as written for the first time. Proven 2026-08-08 across all three outcomes rather than only the happy one: pitlab-snapshot take/list/delete on iventoy01 (CT 107) exercised the PVE path, on control01 (CT 121) exercised the ZFS root-dataset fallback (rpool/data/subvol-121-disk-0@pre-…), and on bun3d (VM 114) correctly produced the loud true negativeFAIL: … There is NO rollback point. Do not proceed with the change. All snapshots taken for the proof were deleted and their absence re-verified.
  • A fourth guest class is now named in the directive: no snapshot handle at either layer. ADR-0336 documented the bind-mount class (control01, runner01, runner02). bun3d (VM 114) is a different cause with the same consequence — its disks live on arch01-vms, which PVE cannot snapshot, and its rootfs is not ZFS-backed, so the fallback has nothing to fall back to. The correct behaviour there is to stop, and the directive now says so with a live example rather than as a hypothetical.
  • The claude@pam token’s read-only status is now an ADR-backed decision rather than an unexamined default. A future session that hits Permission check failed has a documented answer instead of an invitation to widen the grant.
  • The Documentation Standard’s new clause creates work the first time it is applied to an existing mandatory doc — someone must actually run the commands. That is the point, and the cost is bounded because the scope is small.
  • The routing gap is the residual risk, and it is stated rather than solved. A reference note observing that a prime directive is wrong had no mechanism to reach the directive. This ADR fixes the instance; nothing yet guarantees the next such observation is routed. Named here so it is a known coupling — the practical mitigation today is that /wrapup’s ADR-reconciliation check reads new reference notes.
  • ADR-0336 — the pitlab-snapshot helper and the PD1 ZFS fallback
  • ADR-0212 — the PBS least-privilege Audit-token precedent this follows
  • ADR-0049 — ~/.claude is version-controlled, so a directive change is a reviewable commit
  • Privileged access register — the token’s scope and why it needs no tightening
  • pve01 pvesh/qm require sudo — the reference note that knew first
  • Documentation Standard — the mandatory-path command clause added here