diff --git a/docs/TEMPERATURE_MONITORING.md b/docs/TEMPERATURE_MONITORING.md index 4f727e2e04..bc1063438a 100644 --- a/docs/TEMPERATURE_MONITORING.md +++ b/docs/TEMPERATURE_MONITORING.md @@ -9,13 +9,22 @@ If you are upgrading from older releases that used `pulse-sensor-proxy`, see the ## Recommended: Pulse Agent (Proxmox) -The unified agent runs on each Proxmox host and reports temperatures locally with no SSH keys needed. +The unified agent runs on each Proxmox host and reports temperatures locally +with no SSH keys needed. First complete the +[private-file preparation](UNIFIED_AGENT.md#private-file-installation-linux-macos-and-nas) +on that host: protect the Pulse agent token, download the installer from your +Pulse server over verified HTTPS, and inspect it. Then use the same Pulse +address and add the Proxmox collector flag: ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-proxmox +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-proxmox ``` +The Pulse agent token is not a Proxmox API token. Keep the secret out of +command arguments and leave command execution disabled for monitoring. + Notes: - Install `lm-sensors` on each host (`apt install lm-sensors && sensors-detect --auto`). - Temperatures appear automatically once the agent reports. @@ -104,11 +113,25 @@ ssh -i /path/to/key root@node "cat /sys/class/thermal/thermal_zone0/temp" ## Legacy Cleanup (If Upgrading) -If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container) with the supported cleanup helper: +If you still have the old sensor proxy installed from prior releases, remove it +from each **Proxmox host** (not the Pulse container) with the supported cleanup +helper. In that host's administrative shell, download it to a private directory: ```bash -curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ - sudo bash -s -- --uninstall --purge --local-only +umask 077 +mkdir -p "$HOME/.config/pulse" +chmod 700 "$HOME/.config/pulse" +curl --fail --silent --show-error --connect-timeout 10 --max-time 60 \ + --output "$HOME/.config/pulse/sensor-proxy-uninstall.sh" \ + https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh +``` + +Stop if the download fails and inspect the saved script before running it. +Do not pipe a web response into a privileged shell: + +```bash +bash "$HOME/.config/pulse/sensor-proxy-uninstall.sh" \ + --uninstall --purge --local-only ``` `--local-only` avoids cluster SSH entirely; run the command once on every @@ -122,8 +145,8 @@ remote portion fail after local cleanup completes. If you also want to remove the old `pulse-monitor@pam` API user and tokens before re-adding the node, include `--remove-proxmox-access`: ```bash -curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ - sudo bash -s -- --uninstall --purge --remove-proxmox-access --local-only +bash "$HOME/.config/pulse/sensor-proxy-uninstall.sh" \ + --uninstall --purge --remove-proxmox-access --local-only ``` Reinstalling or upgrading the Pulse container does **not** remove the sensor proxy from the host — they are separate installations. If you skip this cleanup, the selfheal timer will keep running and may generate recurring `TASK ERROR` entries in the Proxmox task log. diff --git a/docs/UNIFIED_AGENT.md b/docs/UNIFIED_AGENT.md index ff7435ca9d..4ef1f23ae1 100644 --- a/docs/UNIFIED_AGENT.md +++ b/docs/UNIFIED_AGENT.md @@ -27,18 +27,25 @@ preventing duplicate root-SSH sensor polling between cluster peers. ## Quick Start -Generate an installation command in the UI: +Choose the host profile and create a monitoring token in the UI: **Settings → Infrastructure → Install on a host** Choose a target profile in that screen when you want explicit install flags for Docker, Kubernetes, Proxmox VE, or Proxmox Backup Server. -The generated command is not tied to a single machine. For a Proxmox VE -cluster, one API connection already provides cluster-wide inventory; the agent -is per host, so run the same generated command on each cluster node where you -want agent-provided telemetry (temperatures, SMART, host identity). Each agent -registers itself and attaches to its own cluster member. +Keep the token out of shell commands, history, URLs and screenshots. Use the +private-file installation steps below; the command arguments contain only the +file's path. A Pulse agent token is not a Proxmox or PBS API token. Monitoring +does not require command execution: leave that option off unless you intend +to grant it. See [Agent Security](AGENT_SECURITY.md) before choosing a root +host agent instead of an API connection or the opt-in Linux safe profile. -The same generated command is also the supported v5-to-v6 agent upgrade path. +For a Proxmox VE cluster, one API connection already provides cluster-wide +inventory; the agent is per host. Repeat the private-file installation on each +cluster node where you want agent-provided telemetry (temperatures, SMART, host +identity), using the intended profile. Each agent registers itself and attaches +to its own cluster member. + +The same installer is also the supported v5-to-v6 agent upgrade path. Run it on the host that already has the v5 `pulse-agent` service to replace the binary and service configuration in place; do not uninstall the old service first unless you are intentionally removing that host from Pulse. @@ -51,24 +58,29 @@ agents initiate the connection. Prefer a stable DNS name for the primary URL so replacing the Pulse host does not require an agent migration. After importing the configuration on a Pulse server with a different address, -retarget each existing standard Linux agent from that agent machine: +retarget each existing standard Linux agent from that agent machine. Download +and inspect the installer from the **new** Pulse address using the HTTPS +preparation below; retargeting reuses the saved credential, so do not create +another token just for this operation: ```bash -curl -fsSL https://pulse.example.com:7655/install.sh | \ - sudo bash -s -- --retarget --url https://pulse.example.com:7655 +bash "$HOME/.config/pulse/agent-install.sh" \ + --retarget --url https://pulse.example.com ``` The retarget operation recovers the existing token, agent ID, enabled collectors, and other service options. It does not carry the old endpoint's TLS bypass, custom CA, or certificate fingerprint to the new address. Supply -`--cacert`, `--server-fingerprint`, or (only on a trusted network) -`--insecure` explicitly when the new endpoint requires it. The script must -come from the new server so it supports the retarget operation. A newly -generated full installation command from **Settings → Infrastructure → Install -on a host** remains the fallback. +`--cacert` or an independently verified `--server-fingerprint` explicitly +when the new agent endpoint requires it. The installer download itself must +use a trusted certificate or a separately verified CA file; an agent +fingerprint option does not verify that earlier download. The script must +come from the new server so it supports retargeting. If retargeting is not +supported on that host, use the private-file installation below with the +intended profile, rather than uninstalling the existing service first. -On Windows, run the full generated PowerShell installation command from the -new Pulse server as Administrator, including the desired collector options. +On Windows, use the PowerShell private-file installation below from the new +Pulse server as Administrator, including the desired collector options. Do not expect the configuration import itself to make agent-only machines appear at the new address. @@ -83,42 +95,96 @@ the agent has reported, and confirm the host-local version with This is the agent installer served by your Pulse server. It is separate from the top-level GitHub `install.sh`, which installs or updates the Pulse server itself. -### Linux (systemd) -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token -``` +### Private-file installation: Linux, macOS and NAS -### macOS -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token -``` +Use an administrative shell **on the host being monitored**, not the Pulse +server or an unrelated container. Linux/systemd, macOS, Synology and TrueNAS +use the same preparation. TrueNAS SCALE uses systemd; CORE uses rc.d. An API +connection is usually enough for TrueNAS inventory and usage; an agent is +optional for host-local data. + +1. In that shell, protect the file before opening the editor. Save only the + Pulse agent token, with no quotes or header. Use your own private directory + and regular files, not shared paths or symlinks: + + ```bash + umask 077 + mkdir -p "$HOME/.config/pulse" + chmod 700 "$HOME/.config/pulse" + touch "$HOME/.config/pulse/agent-token" + chmod 600 "$HOME/.config/pulse/agent-token" + vi "$HOME/.config/pulse/agent-token" + ``` + +2. Replace `https://pulse.example.com` with your Pulse server's HTTPS address + in both the download and installation commands. Download to a file: + + ```bash + curl --fail --silent --show-error --connect-timeout 10 --max-time 60 \ + --output "$HOME/.config/pulse/agent-install.sh" \ + https://pulse.example.com/install.sh + ``` + + **Stop if the download fails.** Inspect the saved script before executing + it. Do not bypass certificate verification or pipe an unchecked response + into a privileged shell. For a private CA, add curl's `--cacert` with the + separately verified CA file, and supply the installer's `--cacert` option + for the agent connection as well. Do not substitute GitHub's top-level + `install.sh`: that installs the Pulse server, not the agent. + +3. Install with the private token file, adding a profile from + [Installation Options](#installation-options) when needed: + + ```bash + bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" + ``` + +Keep the bootstrap token file private and remove that copy when no longer +needed; do not delete the installed agent's runtime credential. Check a fresh +report in Pulse and `pulse-agent --version` on the host. A started service or +hardware capacity alone does not prove that every requested metric is present. ### Windows (PowerShell, run as Administrator) + +Use a **new** setup directory under your own Windows profile; if it already +exists, choose another name. The following preparation grants access only to +your account and SYSTEM. Stop if directory creation or the ACL command fails: + ```powershell -irm http://:7655/install.ps1 | iex +$setupDir = Join-Path $env:USERPROFILE 'PulseAgentSetup' +New-Item -ItemType Directory -Path $setupDir -ErrorAction Stop | Out-Null +$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value +icacls.exe $setupDir /inheritance:r /grant:r "*${userSid}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' +if ($LASTEXITCODE -ne 0) { throw 'Could not protect the setup directory' } +$tokenFile = Join-Path $setupDir 'agent-token.txt' +New-Item -ItemType File -Path $tokenFile -ErrorAction Stop | Out-Null +Start-Process -FilePath notepad.exe -ArgumentList "`"$tokenFile`"" -Wait ``` -With environment variables: +In Notepad, save only the Pulse agent token, with no quotes or header. Never +paste it into a PowerShell command or assign a literal secret to an environment +variable. Replace the example HTTPS address in both commands below. Download +and inspect the script first; do not use `Invoke-Expression` on a web response: + ```powershell -$env:PULSE_URL="http://:7655" -$env:PULSE_TOKEN="" -irm http://:7655/install.ps1 | iex +$installerFile = Join-Path $setupDir 'install.ps1' +Invoke-WebRequest -Uri 'https://pulse.example.com/install.ps1' -OutFile $installerFile -ErrorAction Stop ``` -### Synology NAS -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token +Only after a successful download and inspection, run the saved script. Clear +an old `PULSE_TOKEN` environment value so it cannot override the file: + +```powershell +Remove-Item Env:PULSE_TOKEN -ErrorAction SilentlyContinue +& $installerFile -Url 'https://pulse.example.com' -TokenFile $tokenFile ``` -### TrueNAS SCALE/CORE -TrueNAS SCALE and TrueNAS CORE are both supported. The installer auto-detects the platform and configures the appropriate service manager (systemd for SCALE, rc.d for CORE). -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token -``` +Keep the setup directory private and remove the bootstrap copy when no longer +needed, not the installed service's token. Verify a fresh report and the +installed agent version; the remaining platform trust trade-offs are in +[Agent Security](AGENT_SECURITY.md). ## Features @@ -550,46 +616,58 @@ operators are comfortable with Proxmox-side guest probing. ## Installation Options +These commands use the protected token file and successfully downloaded, +inspected installer from [Private-file installation](#private-file-installation-linux-macos-and-nas). +Use the same HTTPS address in preparation and installation. The flags below +choose collectors; they do not grant command execution. + ### Simple Install (host + Docker auto-detect) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" ``` ### Proxmox VE Node (explicit profile) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-proxmox --proxmox-type pve +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-proxmox --proxmox-type pve ``` ### Proxmox Backup Server Node (explicit profile) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-proxmox --proxmox-type pbs +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-proxmox --proxmox-type pbs ``` ### Force Enable Docker (if auto-detection fails) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-docker +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-docker ``` ### Disable Docker (even if detected) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-docker=false +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-docker=false ``` ### Host + Kubernetes Monitoring ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-kubernetes +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-kubernetes ``` ### Docker Monitoring Only ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-host=false --enable-docker +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-host=false --enable-docker ``` ### Exclude Specific Disks from Monitoring @@ -693,9 +771,10 @@ installer path instead of relying on a plain-HTTP first hop. To disable auto-updates: ```bash -# During installation -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --disable-auto-update +# After private-file preparation and successful installer inspection +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --disable-auto-update # Or set environment variable PULSE_DISABLE_AUTO_UPDATE=true @@ -718,8 +797,11 @@ See [Centralized Agent Management](CENTRALIZED_MANAGEMENT.md) for supported keys ## Uninstall +Download and inspect the agent installer using the HTTPS preparation above +before running this on the agent host. Uninstallation needs no new token: + ```bash -curl -fsSL http://:7655/install.sh | bash -s -- --uninstall +bash "$HOME/.config/pulse/agent-install.sh" --uninstall ``` This removes: @@ -805,10 +887,13 @@ Unraid), `/tmp` and `/usr/local/bin` share that filesystem, so both the staged and installed copy must fit at once. If the check fails because `/tmp` is on a constrained root, point `TMPDIR` at a -directory on a data volume and re-run the installer: +directory on a data volume and re-run the already downloaded and inspected +agent installer from the private-file preparation above: ```bash -TMPDIR=/share/CACHEDEV1_DATA/tmp bash install.sh --url http://pulse --token +TMPDIR=/share/CACHEDEV1_DATA/tmp bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" ``` (`mktemp` honours `TMPDIR`, so this moves the staging copy off the RAM root. diff --git a/frontend-modern/public/docs/TEMPERATURE_MONITORING.md b/frontend-modern/public/docs/TEMPERATURE_MONITORING.md index 4f727e2e04..bc1063438a 100644 --- a/frontend-modern/public/docs/TEMPERATURE_MONITORING.md +++ b/frontend-modern/public/docs/TEMPERATURE_MONITORING.md @@ -9,13 +9,22 @@ If you are upgrading from older releases that used `pulse-sensor-proxy`, see the ## Recommended: Pulse Agent (Proxmox) -The unified agent runs on each Proxmox host and reports temperatures locally with no SSH keys needed. +The unified agent runs on each Proxmox host and reports temperatures locally +with no SSH keys needed. First complete the +[private-file preparation](UNIFIED_AGENT.md#private-file-installation-linux-macos-and-nas) +on that host: protect the Pulse agent token, download the installer from your +Pulse server over verified HTTPS, and inspect it. Then use the same Pulse +address and add the Proxmox collector flag: ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-proxmox +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-proxmox ``` +The Pulse agent token is not a Proxmox API token. Keep the secret out of +command arguments and leave command execution disabled for monitoring. + Notes: - Install `lm-sensors` on each host (`apt install lm-sensors && sensors-detect --auto`). - Temperatures appear automatically once the agent reports. @@ -104,11 +113,25 @@ ssh -i /path/to/key root@node "cat /sys/class/thermal/thermal_zone0/temp" ## Legacy Cleanup (If Upgrading) -If you still have the old sensor proxy installed from prior releases, remove it from each **Proxmox host** (not the Pulse container) with the supported cleanup helper: +If you still have the old sensor proxy installed from prior releases, remove it +from each **Proxmox host** (not the Pulse container) with the supported cleanup +helper. In that host's administrative shell, download it to a private directory: ```bash -curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ - sudo bash -s -- --uninstall --purge --local-only +umask 077 +mkdir -p "$HOME/.config/pulse" +chmod 700 "$HOME/.config/pulse" +curl --fail --silent --show-error --connect-timeout 10 --max-time 60 \ + --output "$HOME/.config/pulse/sensor-proxy-uninstall.sh" \ + https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh +``` + +Stop if the download fails and inspect the saved script before running it. +Do not pipe a web response into a privileged shell: + +```bash +bash "$HOME/.config/pulse/sensor-proxy-uninstall.sh" \ + --uninstall --purge --local-only ``` `--local-only` avoids cluster SSH entirely; run the command once on every @@ -122,8 +145,8 @@ remote portion fail after local cleanup completes. If you also want to remove the old `pulse-monitor@pam` API user and tokens before re-adding the node, include `--remove-proxmox-access`: ```bash -curl -fsSL https://raw.githubusercontent.com/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh | \ - sudo bash -s -- --uninstall --purge --remove-proxmox-access --local-only +bash "$HOME/.config/pulse/sensor-proxy-uninstall.sh" \ + --uninstall --purge --remove-proxmox-access --local-only ``` Reinstalling or upgrading the Pulse container does **not** remove the sensor proxy from the host — they are separate installations. If you skip this cleanup, the selfheal timer will keep running and may generate recurring `TASK ERROR` entries in the Proxmox task log. diff --git a/frontend-modern/public/docs/UNIFIED_AGENT.md b/frontend-modern/public/docs/UNIFIED_AGENT.md index ff7435ca9d..4ef1f23ae1 100644 --- a/frontend-modern/public/docs/UNIFIED_AGENT.md +++ b/frontend-modern/public/docs/UNIFIED_AGENT.md @@ -27,18 +27,25 @@ preventing duplicate root-SSH sensor polling between cluster peers. ## Quick Start -Generate an installation command in the UI: +Choose the host profile and create a monitoring token in the UI: **Settings → Infrastructure → Install on a host** Choose a target profile in that screen when you want explicit install flags for Docker, Kubernetes, Proxmox VE, or Proxmox Backup Server. -The generated command is not tied to a single machine. For a Proxmox VE -cluster, one API connection already provides cluster-wide inventory; the agent -is per host, so run the same generated command on each cluster node where you -want agent-provided telemetry (temperatures, SMART, host identity). Each agent -registers itself and attaches to its own cluster member. +Keep the token out of shell commands, history, URLs and screenshots. Use the +private-file installation steps below; the command arguments contain only the +file's path. A Pulse agent token is not a Proxmox or PBS API token. Monitoring +does not require command execution: leave that option off unless you intend +to grant it. See [Agent Security](AGENT_SECURITY.md) before choosing a root +host agent instead of an API connection or the opt-in Linux safe profile. -The same generated command is also the supported v5-to-v6 agent upgrade path. +For a Proxmox VE cluster, one API connection already provides cluster-wide +inventory; the agent is per host. Repeat the private-file installation on each +cluster node where you want agent-provided telemetry (temperatures, SMART, host +identity), using the intended profile. Each agent registers itself and attaches +to its own cluster member. + +The same installer is also the supported v5-to-v6 agent upgrade path. Run it on the host that already has the v5 `pulse-agent` service to replace the binary and service configuration in place; do not uninstall the old service first unless you are intentionally removing that host from Pulse. @@ -51,24 +58,29 @@ agents initiate the connection. Prefer a stable DNS name for the primary URL so replacing the Pulse host does not require an agent migration. After importing the configuration on a Pulse server with a different address, -retarget each existing standard Linux agent from that agent machine: +retarget each existing standard Linux agent from that agent machine. Download +and inspect the installer from the **new** Pulse address using the HTTPS +preparation below; retargeting reuses the saved credential, so do not create +another token just for this operation: ```bash -curl -fsSL https://pulse.example.com:7655/install.sh | \ - sudo bash -s -- --retarget --url https://pulse.example.com:7655 +bash "$HOME/.config/pulse/agent-install.sh" \ + --retarget --url https://pulse.example.com ``` The retarget operation recovers the existing token, agent ID, enabled collectors, and other service options. It does not carry the old endpoint's TLS bypass, custom CA, or certificate fingerprint to the new address. Supply -`--cacert`, `--server-fingerprint`, or (only on a trusted network) -`--insecure` explicitly when the new endpoint requires it. The script must -come from the new server so it supports the retarget operation. A newly -generated full installation command from **Settings → Infrastructure → Install -on a host** remains the fallback. +`--cacert` or an independently verified `--server-fingerprint` explicitly +when the new agent endpoint requires it. The installer download itself must +use a trusted certificate or a separately verified CA file; an agent +fingerprint option does not verify that earlier download. The script must +come from the new server so it supports retargeting. If retargeting is not +supported on that host, use the private-file installation below with the +intended profile, rather than uninstalling the existing service first. -On Windows, run the full generated PowerShell installation command from the -new Pulse server as Administrator, including the desired collector options. +On Windows, use the PowerShell private-file installation below from the new +Pulse server as Administrator, including the desired collector options. Do not expect the configuration import itself to make agent-only machines appear at the new address. @@ -83,42 +95,96 @@ the agent has reported, and confirm the host-local version with This is the agent installer served by your Pulse server. It is separate from the top-level GitHub `install.sh`, which installs or updates the Pulse server itself. -### Linux (systemd) -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token -``` +### Private-file installation: Linux, macOS and NAS -### macOS -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token -``` +Use an administrative shell **on the host being monitored**, not the Pulse +server or an unrelated container. Linux/systemd, macOS, Synology and TrueNAS +use the same preparation. TrueNAS SCALE uses systemd; CORE uses rc.d. An API +connection is usually enough for TrueNAS inventory and usage; an agent is +optional for host-local data. + +1. In that shell, protect the file before opening the editor. Save only the + Pulse agent token, with no quotes or header. Use your own private directory + and regular files, not shared paths or symlinks: + + ```bash + umask 077 + mkdir -p "$HOME/.config/pulse" + chmod 700 "$HOME/.config/pulse" + touch "$HOME/.config/pulse/agent-token" + chmod 600 "$HOME/.config/pulse/agent-token" + vi "$HOME/.config/pulse/agent-token" + ``` + +2. Replace `https://pulse.example.com` with your Pulse server's HTTPS address + in both the download and installation commands. Download to a file: + + ```bash + curl --fail --silent --show-error --connect-timeout 10 --max-time 60 \ + --output "$HOME/.config/pulse/agent-install.sh" \ + https://pulse.example.com/install.sh + ``` + + **Stop if the download fails.** Inspect the saved script before executing + it. Do not bypass certificate verification or pipe an unchecked response + into a privileged shell. For a private CA, add curl's `--cacert` with the + separately verified CA file, and supply the installer's `--cacert` option + for the agent connection as well. Do not substitute GitHub's top-level + `install.sh`: that installs the Pulse server, not the agent. + +3. Install with the private token file, adding a profile from + [Installation Options](#installation-options) when needed: + + ```bash + bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" + ``` + +Keep the bootstrap token file private and remove that copy when no longer +needed; do not delete the installed agent's runtime credential. Check a fresh +report in Pulse and `pulse-agent --version` on the host. A started service or +hardware capacity alone does not prove that every requested metric is present. ### Windows (PowerShell, run as Administrator) + +Use a **new** setup directory under your own Windows profile; if it already +exists, choose another name. The following preparation grants access only to +your account and SYSTEM. Stop if directory creation or the ACL command fails: + ```powershell -irm http://:7655/install.ps1 | iex +$setupDir = Join-Path $env:USERPROFILE 'PulseAgentSetup' +New-Item -ItemType Directory -Path $setupDir -ErrorAction Stop | Out-Null +$userSid = [Security.Principal.WindowsIdentity]::GetCurrent().User.Value +icacls.exe $setupDir /inheritance:r /grant:r "*${userSid}:(OI)(CI)F" '*S-1-5-18:(OI)(CI)F' +if ($LASTEXITCODE -ne 0) { throw 'Could not protect the setup directory' } +$tokenFile = Join-Path $setupDir 'agent-token.txt' +New-Item -ItemType File -Path $tokenFile -ErrorAction Stop | Out-Null +Start-Process -FilePath notepad.exe -ArgumentList "`"$tokenFile`"" -Wait ``` -With environment variables: +In Notepad, save only the Pulse agent token, with no quotes or header. Never +paste it into a PowerShell command or assign a literal secret to an environment +variable. Replace the example HTTPS address in both commands below. Download +and inspect the script first; do not use `Invoke-Expression` on a web response: + ```powershell -$env:PULSE_URL="http://:7655" -$env:PULSE_TOKEN="" -irm http://:7655/install.ps1 | iex +$installerFile = Join-Path $setupDir 'install.ps1' +Invoke-WebRequest -Uri 'https://pulse.example.com/install.ps1' -OutFile $installerFile -ErrorAction Stop ``` -### Synology NAS -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token +Only after a successful download and inspection, run the saved script. Clear +an old `PULSE_TOKEN` environment value so it cannot override the file: + +```powershell +Remove-Item Env:PULSE_TOKEN -ErrorAction SilentlyContinue +& $installerFile -Url 'https://pulse.example.com' -TokenFile $tokenFile ``` -### TrueNAS SCALE/CORE -TrueNAS SCALE and TrueNAS CORE are both supported. The installer auto-detects the platform and configures the appropriate service manager (systemd for SCALE, rc.d for CORE). -```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token -``` +Keep the setup directory private and remove the bootstrap copy when no longer +needed, not the installed service's token. Verify a fresh report and the +installed agent version; the remaining platform trust trade-offs are in +[Agent Security](AGENT_SECURITY.md). ## Features @@ -550,46 +616,58 @@ operators are comfortable with Proxmox-side guest probing. ## Installation Options +These commands use the protected token file and successfully downloaded, +inspected installer from [Private-file installation](#private-file-installation-linux-macos-and-nas). +Use the same HTTPS address in preparation and installation. The flags below +choose collectors; they do not grant command execution. + ### Simple Install (host + Docker auto-detect) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" ``` ### Proxmox VE Node (explicit profile) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-proxmox --proxmox-type pve +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-proxmox --proxmox-type pve ``` ### Proxmox Backup Server Node (explicit profile) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-proxmox --proxmox-type pbs +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-proxmox --proxmox-type pbs ``` ### Force Enable Docker (if auto-detection fails) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-docker +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-docker ``` ### Disable Docker (even if detected) ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-docker=false +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-docker=false ``` ### Host + Kubernetes Monitoring ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-kubernetes +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-kubernetes ``` ### Docker Monitoring Only ```bash -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --enable-host=false --enable-docker +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --enable-host=false --enable-docker ``` ### Exclude Specific Disks from Monitoring @@ -693,9 +771,10 @@ installer path instead of relying on a plain-HTTP first hop. To disable auto-updates: ```bash -# During installation -curl -fsSL http://:7655/install.sh | \ - bash -s -- --url http://:7655 --token --disable-auto-update +# After private-file preparation and successful installer inspection +bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" --disable-auto-update # Or set environment variable PULSE_DISABLE_AUTO_UPDATE=true @@ -718,8 +797,11 @@ See [Centralized Agent Management](CENTRALIZED_MANAGEMENT.md) for supported keys ## Uninstall +Download and inspect the agent installer using the HTTPS preparation above +before running this on the agent host. Uninstallation needs no new token: + ```bash -curl -fsSL http://:7655/install.sh | bash -s -- --uninstall +bash "$HOME/.config/pulse/agent-install.sh" --uninstall ``` This removes: @@ -805,10 +887,13 @@ Unraid), `/tmp` and `/usr/local/bin` share that filesystem, so both the staged and installed copy must fit at once. If the check fails because `/tmp` is on a constrained root, point `TMPDIR` at a -directory on a data volume and re-run the installer: +directory on a data volume and re-run the already downloaded and inspected +agent installer from the private-file preparation above: ```bash -TMPDIR=/share/CACHEDEV1_DATA/tmp bash install.sh --url http://pulse --token +TMPDIR=/share/CACHEDEV1_DATA/tmp bash "$HOME/.config/pulse/agent-install.sh" \ + --url https://pulse.example.com \ + --token-file "$HOME/.config/pulse/agent-token" ``` (`mktemp` honours `TMPDIR`, so this moves the staging copy off the RAM root. diff --git a/scripts/tests/test_agent_docs.py b/scripts/tests/test_agent_docs.py new file mode 100644 index 0000000000..dedb46cfa2 --- /dev/null +++ b/scripts/tests/test_agent_docs.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python3 +"""Exercise documented agent setup without installing a service or real token. + +TLS listeners are guest-local: run with pulse-worker-source-proof. Windows +checks cover the documented source contract, not native ACL/service execution. +""" + +import json +import os +from pathlib import Path +import re +import shutil +import stat +import subprocess +import tempfile +import unittest + +from test_pbs_docs import certificate, fixture_environment, server + + +ROOT = Path(__file__).resolve().parents[2] +NAMES = ("UNIFIED_AGENT.md", "TEMPERATURE_MONITORING.md") +TOKEN = "synthetic-agent-docs-secret" +CLEANUP_PATH = "/rcourtman/Pulse/main/scripts/uninstall-sensor-proxy.sh" + + +def blocks(name, language="bash"): + text = re.sub(r"^ {1,3}", "", (ROOT / "docs" / name).read_text(), flags=re.MULTILINE) + return re.findall(r"```" + language + r"\n(.*?)```", text, re.DOTALL) + + +def recipe(name, needle): + found = [block for block in blocks(name) if needle in block] + if len(found) != 1: + raise AssertionError(f"expected one {name} recipe containing {needle!r}") + return found[0] + + +class AgentDocsTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.temporary = tempfile.TemporaryDirectory() + cls.cert, cls.key = certificate(Path(cls.temporary.name), "agent-docs") + cls.other_cert, _ = certificate(Path(cls.temporary.name), "untrusted") + + @classmethod + def tearDownClass(cls): + cls.temporary.cleanup() + + def test_mirrors_and_no_secret_or_unchecked_bootstrap_commands(self): + for name in NAMES: + with self.subTest(name=name): + doc = (ROOT / "docs" / name).read_bytes() + self.assertEqual(doc, (ROOT / "frontend-modern/public/docs" / name).read_bytes()) + shell = "\n".join(blocks(name)) + self.assertNotRegex(shell, r"--token(?:\s|=)|PULSE_TOKEN=|curl[^\n]*http://") + self.assertNotRegex(shell, r"curl[^`]*\|\s*(?:sudo\s+)?bash") + self.assertIn("Stop if the download fails", doc.decode()) + windows = "\n".join(blocks(NAMES[0], "powershell")) + self.assertNotRegex(windows, r"(?i)\biex\b|\birm\b|Invoke-Expression|http://|\$env:PULSE_TOKEN\s*=") + + def test_changed_unix_recipes_parse(self): + count = 0 + for name in NAMES: + for block in blocks(name): + if any(word in block for word in ("agent-install.sh", "agent-token", "sensor-proxy-uninstall.sh")): + with self.subTest(name=name, command=block): + subprocess.run(["bash", "-n", "-c", block], check=True, capture_output=True) + count += 1 + self.assertEqual(count, 18) + + def test_private_file_preparation_preserves_existing_token(self): + command = recipe(NAMES[0], 'vi "$HOME/.config/pulse/agent-token"') + with tempfile.TemporaryDirectory() as temporary: + home = Path(temporary) + tools = home / "tools" + tools.mkdir() + editor = tools / "vi" + editor.write_text('#!/bin/sh\n[ "$#" = 1 ] && [ -f "$1" ]\n') + editor.chmod(0o700) + env = fixture_environment(home) + env["PATH"] = f"{tools}:{os.environ['PATH']}" + token = home / ".config/pulse/agent-token" + for existing in (False, True): + if existing: + token.write_text(TOKEN + "\n") + token.chmod(0o644) + token.parent.chmod(0o755) + result = subprocess.run(["bash", "-eu", "-c", command], env=env, capture_output=True) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(stat.S_IMODE(token.stat().st_mode), 0o600) + self.assertEqual(stat.S_IMODE(token.parent.stat().st_mode), 0o700) + self.assertEqual(token.read_text(), TOKEN + "\n" if existing else "") + self.assertNotIn(TOKEN.encode(), result.stdout + result.stderr) + + def test_windows_file_handoff_matches_installer_contract(self): + windows = "\n".join(blocks(NAMES[0], "powershell")) + for expected in ("-ErrorAction Stop", "/inheritance:r", "*${userSid}:(OI)(CI)F", + "*S-1-5-18:(OI)(CI)F", "$LASTEXITCODE -ne 0", + "-FilePath notepad.exe", "-Wait", "-OutFile $installerFile", + "Remove-Item Env:PULSE_TOKEN", "-TokenFile $tokenFile"): + self.assertIn(expected, windows) + source = (ROOT / "scripts/install.ps1").read_text() + self.assertIn('[string]$TokenFile = $env:PULSE_TOKEN_FILE,', source) + self.assertIn('if ([string]::IsNullOrWhiteSpace($Token) -and -not [string]::IsNullOrWhiteSpace($TokenFile))', source) + self.assertIn('(Get-Content -Path $resolvedTokenFile -Raw -ErrorAction Stop).Trim()', source) + + def download(self, home, command, port, ca=None, hostname="localhost"): + private = home / ".config/pulse" + private.mkdir(parents=True, exist_ok=True, mode=0o700) + tools = home / "tools" + tools.mkdir(exist_ok=True) + recorder = tools / "curl" + recorder.write_text( + "#!/usr/bin/env python3\nimport json, os, sys\nfrom pathlib import Path\n" + "Path(os.environ['CURL_RECEIPT']).write_text(json.dumps(sys.argv[1:]))\n" + "os.execv(os.environ['REAL_CURL'], [os.environ['REAL_CURL'], *sys.argv[1:]])\n" + ) + recorder.chmod(0o700) + env = fixture_environment(home) + env.update(PATH=f"{tools}:{os.environ['PATH']}", REAL_CURL=shutil.which("curl"), + CURL_RECEIPT=str(home / "curl-argv.json")) + command = command.replace("https://pulse.example.com", f"https://{hostname}:{port}") + command = command.replace("https://raw.githubusercontent.com", f"https://{hostname}:{port}") + if ca is not None: + command = command.replace("curl ", f'curl --cacert "{ca}" ', 1) + result = subprocess.run(["bash", "-eu", "-c", command], env=env, capture_output=True, timeout=20) + argv = json.loads((home / "curl-argv.json").read_text()) + self.assertNotIn(TOKEN, " ".join(argv)) + self.assertNotIn(TOKEN.encode(), result.stdout + result.stderr) + for argument in ("--connect-timeout", "--max-time", "--fail", "--output"): + self.assertIn(argument, argv) + self.assertNotIn("--insecure", argv) + self.assertNotIn("-k", argv) + return result + + def test_downloads_require_verified_tls_and_success(self): + for name, path, needle, output in ( + (NAMES[0], "/install.sh", '--output "$HOME/.config/pulse/agent-install.sh"', "agent-install.sh"), + (NAMES[1], CLEANUP_PATH, '--output "$HOME/.config/pulse/sensor-proxy-uninstall.sh"', "sensor-proxy-uninstall.sh"), + ): + for status, ca, hostname, expected in ( + (200, self.cert, "localhost", 0), + (403, self.cert, "localhost", 22), + (200, None, "localhost", 60), + (200, self.other_cert, "localhost", 60), + (200, self.cert, "127.0.0.1", 60), + ): + with self.subTest(name=name, status=status, expected=expected): + with tempfile.TemporaryDirectory() as temporary: + with server(self.cert, self.key, status, b"# harmless installer fixture\n", path) as (port, requests): + home = Path(temporary) + result = self.download(home, recipe(name, needle), port, ca, hostname) + self.assertEqual(result.returncode, expected, result.stderr.decode()) + if expected == 0: + self.assertEqual((home / ".config/pulse" / output).read_bytes(), b"# harmless installer fixture\n") + if expected == 60: + self.assertEqual(requests, []) + for requested_path, headers in requests: + self.assertEqual(requested_path, path) + self.assertNotIn("Authorization", headers) + + def test_every_profile_passes_only_private_file_not_token_arguments(self): + profiles = [] + for name in NAMES: + profiles.extend(block for block in blocks(name) if 'bash "$HOME/.config/pulse/agent-install.sh"' in block and "--token-file" in block) + self.assertEqual(len(profiles), 11) + fixture = '''#!/bin/bash +python3 - "$@" <<'PY' +import json, os, stat, sys +from pathlib import Path +args = sys.argv[1:] +token = Path(args[args.index('--token-file') + 1]) +assert args.count('--token-file') == 1 +assert token.read_text().strip() == os.environ['EXPECTED_TOKEN'] +assert stat.S_IMODE(token.stat().st_mode) == 0o600 +assert os.environ['EXPECTED_TOKEN'] not in ' '.join(args) +assert '--token' not in args +Path(os.environ['INSTALL_RECEIPT']).write_text(json.dumps(args)) +PY +''' + with tempfile.TemporaryDirectory() as temporary: + home = Path(temporary) + private = home / ".config/pulse" + private.mkdir(parents=True, mode=0o700) + (private / "agent-install.sh").write_text(fixture) + token = private / "agent-token" + token.write_text(TOKEN + "\n") + token.chmod(0o600) + env = fixture_environment(home) + env.update(EXPECTED_TOKEN=TOKEN, INSTALL_RECEIPT=str(home / "installer-argv.json")) + for command in profiles: + with self.subTest(command=command): + result = subprocess.run(["bash", "-eu", "-c", command], env=env, capture_output=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + args = json.loads((home / "installer-argv.json").read_text()) + self.assertEqual(args[:4], ["--url", "https://pulse.example.com", "--token-file", str(token)]) + self.assertNotIn(TOKEN.encode(), result.stdout + result.stderr) + expected_profiles = [[], ["--enable-proxmox", "--proxmox-type", "pve"], + ["--enable-proxmox", "--proxmox-type", "pbs"], + ["--enable-docker"], ["--enable-docker=false"], + ["--enable-kubernetes"], ["--enable-host=false", "--enable-docker"], + ["--disable-auto-update"], ["--enable-proxmox"]] + actual_profiles = [re.findall(r"--(?:enable-[a-z]+(?:=false)?|disable-auto-update|proxmox-type\s+(?:pve|pbs))", p) for p in profiles] + for expected in expected_profiles: + # Compare the actual argv tokens, retaining the type's value. + self.assertIn(" ".join(expected), [" ".join(a) for a in actual_profiles]) + source = (ROOT / "scripts/install.sh").read_text() + self.assertIn('--token-file) TOKEN_FILE_PATH="$2"; shift 2 ;;', source) + self.assertIn('read_collector_token_file_safely "$TOKEN_FILE_PATH" true', source) + + def test_retarget_uninstall_and_cleanup_use_saved_script_without_new_token(self): + expected = (["--retarget", "--url", "https://pulse.example.com"], ["--uninstall"], + ["--uninstall", "--purge", "--local-only"], + ["--uninstall", "--purge", "--remove-proxmox-access", "--local-only"]) + commands = [recipe(NAMES[0], "--retarget --url"), recipe(NAMES[0], 'agent-install.sh" --uninstall'), + recipe(NAMES[1], "--uninstall --purge --local-only"), + recipe(NAMES[1], "--uninstall --purge --remove-proxmox-access")] + with tempfile.TemporaryDirectory() as temporary: + home = Path(temporary) + private = home / ".config/pulse" + private.mkdir(parents=True, mode=0o700) + fixture = '#!/bin/bash\npython3 - "$@" <<\'PY\'\nimport json, os, sys\nfrom pathlib import Path\nPath(os.environ["INSTALL_RECEIPT"]).write_text(json.dumps(sys.argv[1:]))\nPY\n' + for name in ("agent-install.sh", "sensor-proxy-uninstall.sh"): + (private / name).write_text(fixture) + env = fixture_environment(home) + env["INSTALL_RECEIPT"] = str(home / "argv.json") + for command, args in zip(commands, expected): + result = subprocess.run(["bash", "-eu", "-c", command], env=env, capture_output=True, timeout=10) + self.assertEqual(result.returncode, 0, result.stderr.decode()) + self.assertEqual(json.loads((home / "argv.json").read_text()), args) + + +if __name__ == "__main__": + unittest.main() diff --git a/scripts/tests/test_pbs_docs.py b/scripts/tests/test_pbs_docs.py index 03ce29234a..fc19a7c2fd 100644 --- a/scripts/tests/test_pbs_docs.py +++ b/scripts/tests/test_pbs_docs.py @@ -63,7 +63,7 @@ def certificate(directory, name): @contextmanager -def server(cert, key, status=200, installer=None): +def server(cert, key, status=200, installer=None, installer_path="/install.sh"): requests = [] class Handler(BaseHTTPRequestHandler): @@ -71,7 +71,7 @@ def server(cert, key, status=200, installer=None): requests.append((self.path, dict(self.headers))) self.send_response(status) self.end_headers() - body = installer if self.path == "/install.sh" else b'{"data":[]}\n' + body = installer if self.path == installer_path else b'{"data":[]}\n' self.wfile.write(body or b"fixture error\n") def log_message(self, *_args):