diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index 3ed9d3b46..389f808bc 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -137,15 +137,16 @@ pveum user permissions @pam At minimum, ensure the user/token has read access for inventory and metrics: - `Sys.Audit` -- `VM.Monitor` - `Datastore.Audit` -For VM guest agent features on PVE 9+, also ensure: +For VM guest agent features on PVE 9+, prefer: - `VM.GuestAgent.Audit` — required for disk usage and guest info - `VM.GuestAgent.FileRead` — required for accurate memory monitoring (excludes buff/cache) -Note: The built-in `PVEAuditor` role cannot be modified. Create a custom role (e.g. `PulseMonitor`) with the above privileges added, and assign it to your Pulse API token. +For PVE 8 only, use `VM.Monitor` instead of the `VM.GuestAgent.*` privileges. + +Note: The built-in `PVEAuditor` role cannot be modified. Create a custom role (e.g. `PulseMonitor`) with the above privileges added, and assign it to your Pulse API token. After upgrading to PVE 9, add the `VM.GuestAgent.*` privileges and remove legacy `VM.Monitor` from the custom role. **Rocky Linux / RHEL VMs**: The default qemu-guest-agent configuration may block file-read RPCs (`guest-file-open`, `guest-file-read`, `guest-file-close`). If memory or disk data is missing for these VMs, check `/etc/sysconfig/qemu-ga` and ensure those operations are not blocked, then restart the agent. Refer to your distro's qemu-guest-agent documentation for the exact config syntax. diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 1c5d449fd..338b3c946 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -97,6 +97,11 @@ that binary, not separate customer-facing agent products. ## Shared Boundaries +PVE node setup shared boundaries that render or copy `PulseMonitor` +permissions must treat `VM.GuestAgent.Audit` plus `VM.GuestAgent.FileRead` as +the PVE 9+ primary contract, with `VM.Monitor` retained only as the legacy PVE +8 fallback when guest-agent privileges are unavailable. + `internal/dockeragent/` is lifecycle-adjacent for agent binary/update trust and owns the Docker / Podman collection module used by `pulse-agent`, but Docker runtime capability truth is monitoring-owned. Lifecycle consumers must @@ -3179,7 +3184,10 @@ presentation in the extracted node setup surface `useNodeModalState.ts`): the copied PVE permission snippet must stay aligned with the canonical backend setup script, including comma-joined privilege transport and non-destructive `PulseMonitor` role updates, instead -of shipping a stale local fork. +of shipping a stale local fork. PVE 9+ guest-agent privileges +`VM.GuestAgent.Audit` and `VM.GuestAgent.FileRead` are the primary setup +contract; legacy `VM.Monitor` may appear only as the PVE 8 fallback when the +guest-agent privilege probe is unavailable. That same node setup modal owner must also route Proxmox agent-install command generation through the canonical `NodesAPI.getAgentInstallCommand` client for both PVE and PBS, instead of mixing client-mediated and ad hoc raw POST diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 2b1ae21fa..e3cff853c 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -136,6 +136,12 @@ product API routes free of maintainer commercial analytics. ## Shared Boundaries +PVE setup API consumers, generated scripts, runtime setup, installer setup, and +browser manual guidance must share one `PulseMonitor` privilege contract: +prefer `VM.GuestAgent.Audit` plus `VM.GuestAgent.FileRead` on PVE 9+, and use +legacy `VM.Monitor` only as the PVE 8 fallback when guest-agent privileges are +unavailable. + Mobile onboarding QR/deep-link payload readiness is a shared backend/frontend contract. `internal/api/onboarding_handlers.go` owns the `409 onboarding_not_ready` diagnostic response when Remote Access, relay @@ -1440,7 +1446,11 @@ payload shape change when the portal presents compact client rows. `PVEDatastoreAdmin` ACLs to both `pulse-monitor@pve` and the concrete token id. PBS scripts must grant `Audit` to both `pulse-monitor@pbs` and the concrete token id. Browser/runtime setup callers must not fork this into - token-shared, token-unprivileged, or user-only ACL variants. + token-shared, token-unprivileged, or user-only ACL variants. For PVE, + `PulseMonitor` privilege selection must prefer `VM.GuestAgent.Audit` plus + `VM.GuestAgent.FileRead` on PVE 9+ and reserve `VM.Monitor` for the legacy + PVE 8 fallback, so API-generated scripts, runtime setup, installer setup, + and browser manual guidance stay on one privilege contract. 67. `internal/api/enterprise_extension_rbac_admin.go` shared with `organization-settings`: RBAC admin extension endpoints are both an organization settings control surface and a canonical API payload contract boundary. 68. `internal/api/licensing_bridge.go` shared with `cloud-paid`: commercial licensing bridge handlers carry both API payload contract and cloud-paid entitlement boundary ownership. 69. `internal/api/licensing_handlers.go` shared with `cloud-paid`: commercial licensing handlers carry both API payload contract and cloud-paid entitlement boundary ownership. diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index 2992c3932..b9f39566a 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -1733,15 +1733,19 @@ also validate the returned canonical `type`, normalized `host`, and live registration cannot drift onto a stale or mismatched bootstrap response. Install-time PVE auto-registration must also create privilege-separated Pulse-managed monitor tokens and mirror effective ACLs to the concrete token id -rather than relying on user-only grants or shared-token inheritance. That same -install-time token creation must extract the token secret deterministically: it -must request the machine-readable `pveum ... --output-format json` form first -and parse the `value` field, falling back to the legacy box-drawing table -layout only when an older pveum rejects the JSON flag — matching the hardened -web-setup render path (`internal/api/setup_script_render.go`) so token capture -does not silently fail or mis-parse when pveum's table formatting drifts across -versions/locales. `scripts/installtests/root_install_sh_test.go` is the owned -proof surface for that install-time extraction. A +rather than relying on user-only grants or shared-token inheritance. Its +`PulseMonitor` role setup must prefer `VM.GuestAgent.Audit` plus +`VM.GuestAgent.FileRead` when those PVE 9+ privileges are available, and fall +back to legacy `VM.Monitor` only when the guest-agent privilege probe is +unavailable. That same install-time token creation must extract the token +secret deterministically: it must request the machine-readable +`pveum ... --output-format json` form first and parse the `value` field, +falling back to the legacy box-drawing table layout only when an older pveum +rejects the JSON flag — matching the hardened web-setup render path +(`internal/api/setup_script_render.go`) so token capture does not silently fail +or mis-parse when pveum's table formatting drifts across versions/locales. +`scripts/installtests/root_install_sh_test.go` is the owned proof surface for +that install-time extraction. A non-empty `expires` field alone is not sufficient; the installer must reject bootstrap responses whose expiry is already in the past. That same bootstrap consumer must also fail closed unless the runtime-owned setup metadata is diff --git a/frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx b/frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx index b6671574c..e4ca4249e 100644 --- a/frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx +++ b/frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsx @@ -556,8 +556,8 @@ export const NodeModalSetupGuideSection: Component

Note: PVEAuditor gives read-only API access. PulseMonitor adds Sys.Audit - plus either VM.Monitor (PVE 8) or VM.GuestAgent.Audit (PVE 9+) for disk and - guest metrics. PVEDatastoreAdmin on /storage adds backup visibility. + plus VM.GuestAgent.Audit/FileRead (PVE 9+) or VM.Monitor (PVE 8) for disk + and guest metrics. PVEDatastoreAdmin on /storage adds backup visibility.

diff --git a/frontend-modern/src/components/Settings/__tests__/nodeModalModel.test.ts b/frontend-modern/src/components/Settings/__tests__/nodeModalModel.test.ts new file mode 100644 index 000000000..afea1c2fd --- /dev/null +++ b/frontend-modern/src/components/Settings/__tests__/nodeModalModel.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest'; +import { PVE_MANUAL_PERMISSION_COMMAND } from '../nodeModalModel'; + +describe('nodeModalModel', () => { + it('prefers PVE 9 guest-agent privileges before legacy VM.Monitor', () => { + expect(PVE_MANUAL_PERMISSION_COMMAND).toContain('VM.GuestAgent.Audit'); + expect(PVE_MANUAL_PERMISSION_COMMAND).toContain('VM.GuestAgent.FileRead'); + expect(PVE_MANUAL_PERMISSION_COMMAND).toContain('VM.Monitor'); + + const guestBranch = PVE_MANUAL_PERMISSION_COMMAND.indexOf( + 'if [ "$HAS_GUEST_AGENT_AUDIT" = true ]; then', + ); + const monitorBranch = PVE_MANUAL_PERMISSION_COMMAND.indexOf( + 'pveum role add PulseTmpVMMonitor -privs VM.Monitor', + ); + + expect(guestBranch).toBeGreaterThanOrEqual(0); + expect(monitorBranch).toBeGreaterThanOrEqual(0); + expect(guestBranch).toBeLessThan(monitorBranch); + }); +}); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 3bd18f49c..bbb5b9afd 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -64,6 +64,7 @@ import nodeModalAuthenticationSectionSource from '../NodeModalAuthenticationSect import nodeModalMonitoringSectionSource from '../NodeModalMonitoringSection.tsx?raw'; import nodeModalSetupGuideSectionSource from '../NodeModalSetupGuideSection.tsx?raw'; import nodeModalStatusFooterSource from '../NodeModalStatusFooter.tsx?raw'; +import nodeModalModelSource from '../nodeModalModel.ts?raw'; import nodeModalStateSource from '../useNodeModalState.ts?raw'; import availabilityTargetSlotSource from '../ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx?raw'; import trueNASCredentialSlotSource from '../ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx?raw'; @@ -1291,6 +1292,19 @@ describe('settings architecture guardrails', () => { expect(nodeModalSetupGuideSectionSource).toContain('Existing source repair'); expect(nodeModalSetupGuideSectionSource).toContain('Audit/Repair'); expect(nodeModalSetupGuideSectionSource).toContain('without rotating the current API token'); + expect(nodeModalSetupGuideSectionSource).toContain('VM.GuestAgent.Audit/FileRead'); + expect(nodeModalModelSource).toContain('HAS_GUEST_AGENT_AUDIT'); + expect(nodeModalModelSource).toContain('VM.GuestAgent.FileRead'); + expect(nodeModalModelSource).toContain('PulseTmpVMMonitor'); + const guestAgentBranch = nodeModalModelSource.indexOf( + 'if [ "$HAS_GUEST_AGENT_AUDIT" = true ]; then', + ); + const monitorFallback = nodeModalModelSource.indexOf( + 'pveum role add PulseTmpVMMonitor -privs VM.Monitor', + ); + expect(guestAgentBranch).toBeGreaterThanOrEqual(0); + expect(monitorFallback).toBeGreaterThanOrEqual(0); + expect(guestAgentBranch).toBeLessThan(monitorFallback); expect(nodeModalSetupGuideSectionSource).toContain('Docker inside Proxmox LXCs'); expect(nodeModalSetupGuideSectionSource).toContain( 'PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true', diff --git a/frontend-modern/src/components/Settings/nodeModalModel.ts b/frontend-modern/src/components/Settings/nodeModalModel.ts index f562ca638..eed0eed26 100644 --- a/frontend-modern/src/components/Settings/nodeModalModel.ts +++ b/frontend-modern/src/components/Settings/nodeModalModel.ts @@ -56,24 +56,31 @@ else fi fi -# VM guest agent / monitor privileges -VM_PRIV="" -if pveum role list 2>/dev/null | grep -q "VM.Monitor"; then - VM_PRIV="VM.Monitor" -elif pveum role list 2>/dev/null | grep -q "VM.GuestAgent.Audit"; then - VM_PRIV="VM.GuestAgent.Audit" -else - if pveum role add PulseTmpVMMonitor -privs VM.Monitor 2>/dev/null; then - VM_PRIV="VM.Monitor" - pveum role delete PulseTmpVMMonitor 2>/dev/null - elif pveum role add PulseTmpGuestAudit -privs VM.GuestAgent.Audit 2>/dev/null; then - VM_PRIV="VM.GuestAgent.Audit" - pveum role delete PulseTmpGuestAudit 2>/dev/null - fi +# VM guest agent (PVE 9+) / monitor (PVE 8) privileges +HAS_GUEST_AGENT_AUDIT=false +if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.Audit"; then + HAS_GUEST_AGENT_AUDIT=true +elif pveum role add PulseTmpGuestAudit -privs VM.GuestAgent.Audit 2>/dev/null; then + HAS_GUEST_AGENT_AUDIT=true + pveum role delete PulseTmpGuestAudit 2>/dev/null fi -if [ -n "$VM_PRIV" ]; then - EXTRA_PRIVS+=("$VM_PRIV") +if [ "$HAS_GUEST_AGENT_AUDIT" = true ]; then + EXTRA_PRIVS+=("VM.GuestAgent.Audit") + + if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.FileRead"; then + EXTRA_PRIVS+=("VM.GuestAgent.FileRead") + elif pveum role add PulseTmpGuestFileRead -privs VM.GuestAgent.FileRead 2>/dev/null; then + EXTRA_PRIVS+=("VM.GuestAgent.FileRead") + pveum role delete PulseTmpGuestFileRead 2>/dev/null + fi +else + if pveum role list 2>/dev/null | grep -q "VM.Monitor"; then + EXTRA_PRIVS+=("VM.Monitor") + elif pveum role add PulseTmpVMMonitor -privs VM.Monitor 2>/dev/null; then + EXTRA_PRIVS+=("VM.Monitor") + pveum role delete PulseTmpVMMonitor 2>/dev/null + fi fi if [ \${#EXTRA_PRIVS[@]} -gt 0 ]; then diff --git a/install.sh b/install.sh index 39652d7a1..58fd14e02 100755 --- a/install.sh +++ b/install.sh @@ -2314,14 +2314,6 @@ PY pveum role delete PulseSysAuditProbe >/dev/null 2>&1 || true fi - local has_vm_monitor=false - if pveum role list 2>/dev/null | grep -q "VM.Monitor"; then - has_vm_monitor=true - elif pveum role add PulseVmMonitorProbe -privs VM.Monitor >/dev/null 2>&1; then - has_vm_monitor=true - pveum role delete PulseVmMonitorProbe >/dev/null 2>&1 || true - fi - local has_guest_audit=false if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.Audit"; then has_guest_audit=true @@ -2330,10 +2322,25 @@ PY pveum role delete PulseGuestAuditProbe >/dev/null 2>&1 || true fi - if [[ "$has_vm_monitor" == true ]]; then - extra_privs+=("VM.Monitor") - elif [[ "$has_guest_audit" == true ]]; then + if [[ "$has_guest_audit" == true ]]; then extra_privs+=("VM.GuestAgent.Audit") + if pveum role list 2>/dev/null | grep -q "VM.GuestAgent.FileRead"; then + extra_privs+=("VM.GuestAgent.FileRead") + elif pveum role add PulseGuestFileReadProbe -privs VM.GuestAgent.FileRead >/dev/null 2>&1; then + extra_privs+=("VM.GuestAgent.FileRead") + pveum role delete PulseGuestFileReadProbe >/dev/null 2>&1 || true + fi + else + local has_vm_monitor=false + if pveum role list 2>/dev/null | grep -q "VM.Monitor"; then + has_vm_monitor=true + elif pveum role add PulseVmMonitorProbe -privs VM.Monitor >/dev/null 2>&1; then + has_vm_monitor=true + pveum role delete PulseVmMonitorProbe >/dev/null 2>&1 || true + fi + if [[ "$has_vm_monitor" == true ]]; then + extra_privs+=("VM.Monitor") + fi fi if [[ ${#extra_privs[@]} -gt 0 ]]; then diff --git a/scripts/installtests/root_install_sh_test.go b/scripts/installtests/root_install_sh_test.go index 1f2904f8d..59961dbcb 100644 --- a/scripts/installtests/root_install_sh_test.go +++ b/scripts/installtests/root_install_sh_test.go @@ -345,6 +345,10 @@ func TestRootInstallScriptAutoRegisterUsesSecureContractShape(t *testing.T) { `pveum aclmod /storage -token "$token_id" -role PVEDatastoreAdmin`, `priv_string="$(IFS=,; echo "${extra_privs[*]}")"`, `pveum role modify PulseMonitor -privs "$priv_string"`, + `pveum role add PulseGuestFileReadProbe -privs VM.GuestAgent.FileRead`, + `extra_privs+=("VM.GuestAgent.Audit")`, + `extra_privs+=("VM.GuestAgent.FileRead")`, + `extra_privs+=("VM.Monitor")`, `slug = re.sub(r"[^a-z0-9]+", "-", host)`, `print(f"pulse-{slug}")`, } @@ -356,6 +360,11 @@ func TestRootInstallScriptAutoRegisterUsesSecureContractShape(t *testing.T) { if strings.Contains(script, `local token_name="pulse-${pulse_host_slug}-$(date +%s)"`) { t.Fatalf("root install.sh preserved stale timestamp-suffixed Proxmox token naming") } + guestBranch := strings.Index(script, `if [[ "$has_guest_audit" == true ]]; then`) + monitorBranch := strings.Index(script, `if [[ "$has_vm_monitor" == true ]]; then`) + if guestBranch < 0 || monitorBranch < 0 || guestBranch > monitorBranch { + t.Fatalf("root install.sh must prefer VM.GuestAgent.* privileges before legacy VM.Monitor") + } forbidden := []string{ `local bootstrap_token=""`, `X-Setup-Token: $bootstrap_token`, diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index c879950e9..28b7782bb 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -2917,7 +2917,7 @@ class SubsystemLookupTest(unittest.TestCase): { "heading": "## Shared Boundaries", "path": "internal/api/access_control_handlers.go", - "line": 1146, + "line": 1152, "heading_line": 137, } ],