Keep agent setup tokens out of commands and unverified downloads

Use existing Unix and Windows token-file installers, verified HTTPS downloads and inspection before execution across setup, retarget, profile and cleanup examples. Exercise private files, all profile arguments, rejected TLS and failed downloads with synthetic guest-local fixtures.

Contract-Neutral: Documentation and executable documentation tests only; installer behaviour, privileges and runtime contracts are unchanged.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot] 2026-10-01 05:41:44 +01:00
parent 36418af66b
commit d5c187613a
6 changed files with 593 additions and 142 deletions

View file

@ -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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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.

View file

@ -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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-token>
```
### Private-file installation: Linux, macOS and NAS
### macOS
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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://<pulse-ip>: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://<pulse-ip>:7655"
$env:PULSE_TOKEN="<api-token>"
irm http://<pulse-ip>: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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>: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 <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.

View file

@ -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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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.

View file

@ -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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-token>
```
### Private-file installation: Linux, macOS and NAS
### macOS
```bash
curl -fsSL http://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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://<pulse-ip>: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://<pulse-ip>:7655"
$env:PULSE_TOKEN="<api-token>"
irm http://<pulse-ip>: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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <api-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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>:7655/install.sh | \
bash -s -- --url http://<pulse-ip>:7655 --token <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://<pulse-ip>: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 <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.

View file

@ -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()

View file

@ -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):