diff --git a/docs/AGENT_SECURITY.md b/docs/AGENT_SECURITY.md index eecc89f110..20b5bf4cff 100644 --- a/docs/AGENT_SECURITY.md +++ b/docs/AGENT_SECURITY.md @@ -5,7 +5,7 @@ Pulse agents incorporate several security mechanisms to ensure that the code run **Start with the least privilege that answers your monitoring question.** For Proxmox VE, PBS, and PMG, that is usually no agent at all: API-only monitoring with a read-only token covers inventory, status, and metrics, and the -generated setup script creates a privilege-separated monitoring user for it +generated setup script creates a privilege-separated monitoring token for it (see [Proxmox Deployment Choices](#proxmox-deployment-choices)). Install a host agent only where you want data the platform API cannot provide, and on Linux consider the supported @@ -89,6 +89,14 @@ You do not need a Pulse agent on every Proxmox-related host just to see basic cluster inventory and utilization. Start with the least-privilege path that answers your monitoring question: +**API-only does not mean guest-agent-free.** VM guest filesystem and memory +collection can still send active requests on the channel used by freeze-enabled +backups. Read permissions and the absence of a host agent do not prove backup +safety. For an affected installation, follow the +[backup safety precaution](VM_DISK_MONITORING.md#backup-safety), including the +monitoring/alert outage while Pulse is stopped; do not add permissions or send +manual probes during a freeze or thaw. + | Goal | Recommended path | Host privilege needed? | |---|---|---| | PVE/PBS/PMG inventory, node status, VM/container status, storage usage, and normal Proxmox API metrics | Add the Proxmox connection with a read-only or narrowly scoped API token | No | diff --git a/docs/TROUBLESHOOTING.md b/docs/TROUBLESHOOTING.md index f05100da0a..6981ebc0b5 100644 --- a/docs/TROUBLESHOOTING.md +++ b/docs/TROUBLESHOOTING.md @@ -630,23 +630,43 @@ remove credentials, cookies, secret URLs and private host, network or personal information. See [Getting Help](#-getting-help) for safe evidence collection. ### Check Permissions (Proxmox) -If Pulse can't see VMs or storage, check the user permissions on Proxmox: -```bash -pveum user permissions @pam -``` -At minimum, ensure the user/token has read access for inventory and metrics: -- `Sys.Audit` -- `Datastore.Audit` +If Pulse cannot see VMs, storage or guest readings, start with the **user, +realm and token ID configured for the affected Pulse connection**, not a +guessed account or your administrator login. Use the existing connection +settings; do not dump the server configuration or expose the token secret. -For VM guest agent features on PVE 9+, prefer: +With privilege separation enabled, the token's effective access is the +**intersection of the user and token permissions**: both must allow the +required privilege on the affected resource path. Ask the Proxmox +administrator to inspect both sides, including inherited ACLs and their +propagation, for that VM, storage or node. An administrator session or +user-only permission listing does not prove the token has access. -- `VM.GuestAgent.Audit` — required for disk usage and guest info -- `VM.GuestAgent.FileRead` — required for accurate memory monitoring (excludes buff/cache) +Inspect the existing denial and compare it with the relevant read privileges: -For PVE 8 only, use `VM.Monitor` instead of the `VM.GuestAgent.*` privileges. +- Inventory and metrics: `Sys.Audit` and `Datastore.Audit`, on the relevant + node/storage scope. +- VM filesystem usage and guest information on PVE 9+: `VM.GuestAgent.Audit`. +- Guest memory through `/proc/meminfo` on PVE 9+: also + `VM.GuestAgent.FileRead`. +- On PVE 8, `VM.Monitor` is the legacy guest-agent fallback, not an extra + requirement for PVE 9+. -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. +The built-in `PVEAuditor` role cannot be modified. Where a read privilege is +actually missing, use the existing narrow custom role and matching scoped +user/token ACLs. **Do not disable privilege separation, grant Administrator or +add guest execution/write privileges** to diagnose missing data. Do not rerun +setup or replace a token as a permissions test: setup can rotate an existing +credential. Make any necessary access repair in your normal maintenance +window, outside backups, then observe normal polling without manual +guest-agent probes. `Sys.Modify` is not a read-only monitoring privilege; see +[pending-update access](#proxmox-pending-update-access) before considering it. + +Keep token secrets and full permission listings private. A report needs only +the relevant missing privilege and a redacted denial, not private identities, +resource paths or the complete ACL tree. Successful inventory does not prove +that guest readings are fresh, or that a guest has thawed. **Rocky Linux / RHEL VMs**: File-read restrictions in `/etc/sysconfig/qemu-ga` can explain missing guest memory; they do not by themselves establish why disk diff --git a/docs/VM_DISK_MONITORING.md b/docs/VM_DISK_MONITORING.md index 0ab7bbd28d..14ba35ea07 100644 --- a/docs/VM_DISK_MONITORING.md +++ b/docs/VM_DISK_MONITORING.md @@ -128,6 +128,12 @@ procedure; a reboot interrupts its workloads. Use the dedicated account and token actually configured for that Pulse connection, not a guessed account name. Review their effective privileges for the affected VM; a host-root diagnostic does not test the API token's access. +With privilege separation enabled, access is the intersection of user and token +permissions: both must allow the required read on the affected VM. Do not disable +privilege separation or recreate the token to diagnose a missing reading. See +[Check permissions](TROUBLESHOOTING.md#check-permissions-proxmox) for the scoped +inspection and repair boundary. A permitted read does not prove disk freshness, +responsiveness or thaw. | Collection | Proxmox VE 9+ | Proxmox VE 8 | | --- | --- | --- | diff --git a/frontend-modern/public/docs/AGENT_SECURITY.md b/frontend-modern/public/docs/AGENT_SECURITY.md index eecc89f110..20b5bf4cff 100644 --- a/frontend-modern/public/docs/AGENT_SECURITY.md +++ b/frontend-modern/public/docs/AGENT_SECURITY.md @@ -5,7 +5,7 @@ Pulse agents incorporate several security mechanisms to ensure that the code run **Start with the least privilege that answers your monitoring question.** For Proxmox VE, PBS, and PMG, that is usually no agent at all: API-only monitoring with a read-only token covers inventory, status, and metrics, and the -generated setup script creates a privilege-separated monitoring user for it +generated setup script creates a privilege-separated monitoring token for it (see [Proxmox Deployment Choices](#proxmox-deployment-choices)). Install a host agent only where you want data the platform API cannot provide, and on Linux consider the supported @@ -89,6 +89,14 @@ You do not need a Pulse agent on every Proxmox-related host just to see basic cluster inventory and utilization. Start with the least-privilege path that answers your monitoring question: +**API-only does not mean guest-agent-free.** VM guest filesystem and memory +collection can still send active requests on the channel used by freeze-enabled +backups. Read permissions and the absence of a host agent do not prove backup +safety. For an affected installation, follow the +[backup safety precaution](VM_DISK_MONITORING.md#backup-safety), including the +monitoring/alert outage while Pulse is stopped; do not add permissions or send +manual probes during a freeze or thaw. + | Goal | Recommended path | Host privilege needed? | |---|---|---| | PVE/PBS/PMG inventory, node status, VM/container status, storage usage, and normal Proxmox API metrics | Add the Proxmox connection with a read-only or narrowly scoped API token | No | diff --git a/frontend-modern/public/docs/TROUBLESHOOTING.md b/frontend-modern/public/docs/TROUBLESHOOTING.md index f05100da0a..6981ebc0b5 100644 --- a/frontend-modern/public/docs/TROUBLESHOOTING.md +++ b/frontend-modern/public/docs/TROUBLESHOOTING.md @@ -630,23 +630,43 @@ remove credentials, cookies, secret URLs and private host, network or personal information. See [Getting Help](#-getting-help) for safe evidence collection. ### Check Permissions (Proxmox) -If Pulse can't see VMs or storage, check the user permissions on Proxmox: -```bash -pveum user permissions @pam -``` -At minimum, ensure the user/token has read access for inventory and metrics: -- `Sys.Audit` -- `Datastore.Audit` +If Pulse cannot see VMs, storage or guest readings, start with the **user, +realm and token ID configured for the affected Pulse connection**, not a +guessed account or your administrator login. Use the existing connection +settings; do not dump the server configuration or expose the token secret. -For VM guest agent features on PVE 9+, prefer: +With privilege separation enabled, the token's effective access is the +**intersection of the user and token permissions**: both must allow the +required privilege on the affected resource path. Ask the Proxmox +administrator to inspect both sides, including inherited ACLs and their +propagation, for that VM, storage or node. An administrator session or +user-only permission listing does not prove the token has access. -- `VM.GuestAgent.Audit` — required for disk usage and guest info -- `VM.GuestAgent.FileRead` — required for accurate memory monitoring (excludes buff/cache) +Inspect the existing denial and compare it with the relevant read privileges: -For PVE 8 only, use `VM.Monitor` instead of the `VM.GuestAgent.*` privileges. +- Inventory and metrics: `Sys.Audit` and `Datastore.Audit`, on the relevant + node/storage scope. +- VM filesystem usage and guest information on PVE 9+: `VM.GuestAgent.Audit`. +- Guest memory through `/proc/meminfo` on PVE 9+: also + `VM.GuestAgent.FileRead`. +- On PVE 8, `VM.Monitor` is the legacy guest-agent fallback, not an extra + requirement for PVE 9+. -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. +The built-in `PVEAuditor` role cannot be modified. Where a read privilege is +actually missing, use the existing narrow custom role and matching scoped +user/token ACLs. **Do not disable privilege separation, grant Administrator or +add guest execution/write privileges** to diagnose missing data. Do not rerun +setup or replace a token as a permissions test: setup can rotate an existing +credential. Make any necessary access repair in your normal maintenance +window, outside backups, then observe normal polling without manual +guest-agent probes. `Sys.Modify` is not a read-only monitoring privilege; see +[pending-update access](#proxmox-pending-update-access) before considering it. + +Keep token secrets and full permission listings private. A report needs only +the relevant missing privilege and a redacted denial, not private identities, +resource paths or the complete ACL tree. Successful inventory does not prove +that guest readings are fresh, or that a guest has thawed. **Rocky Linux / RHEL VMs**: File-read restrictions in `/etc/sysconfig/qemu-ga` can explain missing guest memory; they do not by themselves establish why disk diff --git a/frontend-modern/public/docs/VM_DISK_MONITORING.md b/frontend-modern/public/docs/VM_DISK_MONITORING.md index 0ab7bbd28d..14ba35ea07 100644 --- a/frontend-modern/public/docs/VM_DISK_MONITORING.md +++ b/frontend-modern/public/docs/VM_DISK_MONITORING.md @@ -128,6 +128,12 @@ procedure; a reboot interrupts its workloads. Use the dedicated account and token actually configured for that Pulse connection, not a guessed account name. Review their effective privileges for the affected VM; a host-root diagnostic does not test the API token's access. +With privilege separation enabled, access is the intersection of user and token +permissions: both must allow the required read on the affected VM. Do not disable +privilege separation or recreate the token to diagnose a missing reading. See +[Check permissions](TROUBLESHOOTING.md#check-permissions-proxmox) for the scoped +inspection and repair boundary. A permitted read does not prove disk freshness, +responsiveness or thaw. | Collection | Proxmox VE 9+ | Proxmox VE 8 | | --- | --- | --- | diff --git a/frontend-modern/src/features/docs/__tests__/proxmoxPermissionsGuide.test.ts b/frontend-modern/src/features/docs/__tests__/proxmoxPermissionsGuide.test.ts new file mode 100644 index 0000000000..da97a6db80 --- /dev/null +++ b/frontend-modern/src/features/docs/__tests__/proxmoxPermissionsGuide.test.ts @@ -0,0 +1,124 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; +import { renderDocMarkdown } from '../docMarkdown'; + +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../../../..'); + +function renderGuide(name: string): HTMLElement { + const article = document.createElement('article'); + article.innerHTML = renderDocMarkdown( + readFileSync(path.join(repoRoot, 'frontend-modern/public/docs', `${name}.md`), 'utf8'), + name, + ); + return article; +} + +function section(article: HTMLElement, id: string): HTMLElement { + const heading = article.querySelector(`#${id}`); + expect(heading, `missing permission guide section ${id}`).not.toBeNull(); + return followingContent(heading!); +} + +function followingContent(heading: Element): HTMLElement { + const content = document.createElement('div'); + for (let element = heading.nextElementSibling; element; element = element.nextElementSibling) { + if (/^H[1-3]$/.test(element.tagName)) break; + content.appendChild(element.cloneNode(true)); + } + return content; +} + +const prose = (element: HTMLElement): string => + (element.textContent ?? '').replace(/\s+/g, ' ').trim(); + +describe('shipped Proxmox permission diagnosis', () => { + it('checks the configured user and token on the affected resource, not an administrator', () => { + const content = prose(section(renderGuide('TROUBLESHOOTING'), 'check-permissions-proxmox')); + expect(content).toContain( + 'user, realm and token ID configured for the affected Pulse connection', + ); + expect(content).toContain('intersection of the user and token permissions'); + expect(content).toContain( + 'both must allow the required privilege on the affected resource path', + ); + expect(content).toContain('inherited ACLs and their propagation'); + expect(content).toContain('administrator session or user-only permission listing'); + expect(content).toContain('does not prove the token has access'); + }); + + it('removes the literal placeholder command that redirects and overwrites local files', () => { + const content = section(renderGuide('TROUBLESHOOTING'), 'check-permissions-proxmox'); + // The old @pam snippet passes bash -n, but redirects from `user` + // into `@pam` instead of passing a user ID. Inspection needs no command. + expect(content.querySelector('pre')).toBeNull(); + expect(prose(content)).not.toContain('pveum user permissions'); + expect(prose(content)).toContain('Inspect the existing denial'); + }); + + it('preserves least privilege and avoids rotating credentials or probing a frozen guest', () => { + const content = prose(section(renderGuide('TROUBLESHOOTING'), 'check-permissions-proxmox')); + expect(content).toContain('Do not disable privilege separation'); + expect(content).toContain('grant Administrator'); + expect(content).toContain('add guest execution/write privileges'); + expect(content).toContain('Do not rerun setup or replace a token as a permissions test'); + expect(content).toContain('normal maintenance window, outside backups'); + expect(content).toContain('normal polling without manual guest-agent probes'); + expect(content).toContain('Keep token secrets and full permission listings private'); + expect(content).toContain('missing privilege and a redacted denial'); + expect(content).toContain('Sys.Modify is not a read-only monitoring privilege'); + }); + + it('distinguishes active guest reads from passive inventory before recommending permissions', () => { + const content = section(renderGuide('AGENT_SECURITY'), 'proxmox-deployment-choices'); + expect(prose(content)).toContain('API-only does not mean guest-agent-free'); + expect(prose(content)).toContain( + 'active requests on the channel used by freeze-enabled backups', + ); + expect(prose(content)).toContain( + 'Read permissions and the absence of a host agent do not prove backup safety', + ); + const warning = Array.from(content.querySelectorAll('p')).find((paragraph) => + paragraph.textContent?.includes('API-only does not mean guest-agent-free'), + ); + const table = content.querySelector('table'); + expect(table).not.toBeNull(); + expect(warning?.compareDocumentPosition(table!)).toBe(Node.DOCUMENT_POSITION_FOLLOWING); + expect(warning?.querySelector('a')?.getAttribute('href')).toBe( + '/docs/VM_DISK_MONITORING#backup-safety', + ); + }); + + it('keeps the disk-specific entry point token-aware without promising a fresh reading', () => { + const heading = Array.from(renderGuide('VM_DISK_MONITORING').querySelectorAll('h2')).find( + (element) => element.textContent === '⚙️ Permissions', + ); + expect(heading).toBeDefined(); + const content = followingContent(heading!); + const text = prose(content); + expect(text).toContain('intersection of user and token permissions'); + expect(text).toContain('on the affected VM'); + expect(text).toContain('Do not disable privilege separation or recreate the token'); + expect(text).toContain('does not prove disk freshness, responsiveness or thaw'); + expect(content.querySelector('a')?.getAttribute('href')).toBe( + '/docs/TROUBLESHOOTING#check-permissions-proxmox', + ); + }); + + it('matches the existing generated token and ACL contract without running setup', () => { + // Static binding only: neither this test nor the docs create a real token. + const setup = readFileSync( + path.join(repoRoot, 'internal/api/configapi/setup_script_render.go'), + 'utf8', + ); + expect(setup).toContain('--privsep 1'); + expect(setup).toContain('pveum aclmod / -user pulse-monitor@pve -role PVEAuditor'); + expect(setup).toContain('pveum aclmod / -token "$PULSE_TOKEN_ID" -role PVEAuditor'); + for (const name of ['TROUBLESHOOTING', 'VM_DISK_MONITORING', 'AGENT_SECURITY']) { + expect(readFileSync(path.join(repoRoot, 'docs', `${name}.md`))).toEqual( + readFileSync(path.join(repoRoot, 'frontend-modern/public/docs', `${name}.md`)), + ); + } + }); +});