41 Setup Guide
Mathew Storm edited this page 2026-05-26 15:14:28 -04:00

Agent Setup Guide

Install the Storm Pulse agent on a fresh VPS. The agent itself runs as your unprivileged admin user — no stormpulse system user, no root daemon, no sudo at runtime. A few one-time setup steps still need sudo (apt install, creating a project directory under /opt); those are clearly marked. Step 1 runs on the dashboard server; every other step runs on the VPS.


Quick install

The happy path. Fill in 4 variables, paste two blocks, answer six wizard prompts in between.

1. Issue an enrollment token

Open the dashboard → Servers → your host → Issue enrollment token. The side panel gives you two things to copy:

  • Enroll command — the complete rootless command, with your agent-id and one-time enrollment token pre-substituted. Paste it verbatim in Block A.
  • Pulse token — long-lived. Paste it into the init wizard in phase 3.

Copy both before dismissing the panel — the warning "shown only once" is real.

2. Block A — install and enroll

On the VPS, paste this to install pipx and the agent:

sudo apt update && sudo apt install -y pipx
pipx ensurepath
export PATH="$HOME/.local/bin:$PATH"
pipx install git+https://git.stormdevelopments.ca/official-public/storm-pulse.git

Then paste the Enroll command you copied from the dashboard in step 1. It already contains your agent-id and enrollment token — nothing to substitute.

Here's an example of what it will look like:

stormpulse enroll \
    --creds-dir ~/.config/stormpulse \
    https://stormdevelopments.ca/api/enroll/ \
    garage-alpha \
    d650d061-3daf-45c8-a066-a3b91696f5cf

3. Run the init wizard

cd /opt/your-project   # wherever your project lives (e.g. /opt/garage)
stormpulse init --user

Six prompts. Most auto-detect — accept defaults unless wrong:

  • Pulse token — paste the value from step 1.
  • Dashboard WebSocket URL — auto-derived, confirm.
  • Project directory — defaults to the current dir.
  • Compose file — auto-detected.
  • Service name — parsed from the compose file.
  • .env file — auto-detected, or skip.

4. Block B — start and verify

systemctl --user daemon-reload
systemctl --user enable --now stormpulse
journalctl --user -u stormpulse -f

Expect "Connected to dashboard" and periodic heartbeat/metrics sends in the log tail.

5. Watch the dashboard turn green

The server's status dot flips to green within seconds. CPU, memory, and disk metrics start streaming. You're done.


Detail: when something breaks

Same flow as the TL;DR, broken apart with explanation and failure modes per step.

1. Generate an enrollment token

Dashboard UI (preferred). Servers → your host → Issue enrollment token. Copy the Pulse token and Enrollment token from the side panel. The hostname becomes the agent-id and must match the Server record exactly.

CLI fallback. If you don't have dashboard access, run on the dashboard server:

HOST_NAME="your-host-name"

# bare-metal install:
python manage.py create_enrollment_token "$HOST_NAME"

# docker install:
docker compose --env-file .env -f docker/docker-compose.prod.yml \
  exec web python manage.py create_enrollment_token "$HOST_NAME"

2. Install pipx

sudo apt update && sudo apt install -y pipx
pipx ensurepath
export PATH="$HOME/.local/bin:$PATH"

ensurepath edits your shell rc file for future shells; export fixes the current shell. You need both — pasting pipx install ... in this shell without the export gives command not found.

3. Install the agent

pipx install git+https://git.stormdevelopments.ca/official-public/storm-pulse.git
stormpulse --version   # expect: a version number

If command not found, re-run the export PATH=... line from step 2.

4. Enroll

Paste the Enroll command from the dashboard panel (step 1 above in the TL;DR). It looks roughly like:

stormpulse enroll \
    https://stormdevelopments.ca/api/enroll/ \
    YOUR_AGENT_ID \
    YOUR_ENROLLMENT_TOKEN

— but with real values pre-filled by the dashboard. If you used the CLI fallback above, paste that output instead.

Writes credentials to ~/.config/stormpulse/. The private key never leaves this machine — see Security Architecture for the cert/CSR mechanics. Pass --creds-dir <path> if you want them somewhere else.

Don't reach for sudo to work around agent errors. The binary lives at ~/.local/bin/stormpulse and sudo's secure_path strips that — you'll get command not found. The agent is rootless; sudo isn't the fix for any agent problem.

5. Init

cd "$PROJECT_DIR"
stormpulse init --user

Wizard prompts — see the TL;DR step 3 list. Writes ~/.config/stormpulse/stormpulse.toml and ~/.config/systemd/user/stormpulse.service. Use --force to overwrite existing files. Edit the toml later to customize commands.

--user forces user-mode install (a user systemd unit). Without it, init tries to auto-detect mode by probing $XDG_RUNTIME_DIR/docker.sock. If rootless dockerd isn't running yet, the probe misses and init falls back to system mode, which then errors with "System-mode install needs root". Always pass --user on hardened boxes — it's deterministic.

If the wizard refuses with "Rerun without sudo for user mode", drop sudo and try again.

6. Git remote (optional)

Skip if the project has no git repo, or your user can already git pull it (SSH keys set up).

Otherwise, set an HTTPS remote with a Forgejo token:

GIT_TOKEN="forgejo-read-only-token"   # Settings → Applications
REPO_PATH="ORG/REPO.git"

git -C "$PROJECT_DIR" remote set-url origin \
  "https://${USER}:${GIT_TOKEN}@git.stormdevelopments.ca/${REPO_PATH}"

For a public repo, omit ${USER}:${GIT_TOKEN}@.

7. Start

sudo loginctl enable-linger $USER   # only if "Linger=no"
systemctl --user daemon-reload
systemctl --user enable --now stormpulse
journalctl --user -u stormpulse -f

enable-linger lets the user unit survive your logout. Playbook 001 sets it for admin users — check with loginctl show-user $USER | grep Linger.

The agent connects outbound only — no inbound ports needed. If your egress is restrictive, allow HTTPS (port 443) to the dashboard.

8. Container log shipping (optional)

stormpulse logging init

Detects running containers and writes one [[log_groups]] block per container to ~/.config/stormpulse/stormpulse.toml. See Log Shipping for parser choices and the full reference.


Host service logs

On hardened boxes, Caddy and fail2ban run on the host, not in containers — their logs are files on disk. Add [[log_groups]] entries to ~/.config/stormpulse/stormpulse.toml:

[[log_groups]]
name = "caddy"
source_type = "file"
path = "/var/log/caddy/access.log"
parser = "caddy_json"

[[log_groups]]
name = "fail2ban"
source_type = "file"
path = "/var/log/fail2ban.log"
parser = "stormpulse"

[[log_groups]]
name = "ufw"
source_type = "file"
path = "/var/log/ufw.log"
parser = "stormpulse"

Restart after editing:

systemctl --user restart stormpulse

The agent runs as your user, so it can only read files your user can read. Playbook 001 puts admin users in the adm group, which grants read on most /var/log/*. If a group is missing in the dashboard, that's the first thing to check.

Group names matter for tab routing: caddy, fail2ban, ufwNetwork tab; system, audit, kernel, journaldServer; everything else → Activity. (ADR storm-pulse CORE-003.)


Verify end-to-end

  1. Agent logsjournalctl --user -u stormpulse -f shows "Connected to dashboard" plus heartbeat/metrics sends.
  2. Dashboard — server status dot is green; CPU/memory/disk metrics appear.
  3. Test command — trigger a git_pull from the dashboard. The agent log shows it executing; the result lands back in the dashboard.

Manual enrollment (rare)

Only when the VPS can't reach the dashboard during setup. The private key must be generated on this VPS — never imported.

mkdir -p ~/.config/stormpulse && chmod 700 ~/.config/stormpulse
cd ~/.config/stormpulse

openssl ecparam -genkey -name prime256v1 -noout -out agent-key.pem
chmod 600 agent-key.pem

AGENT_ID="your-host-name"
openssl req -new -key agent-key.pem -out /tmp/agent.csr -subj "/CN=$AGENT_ID"

Send /tmp/agent.csr to the CA operator. They return agent.pem (signed cert) and ca.pem (CA cert). Drop both in ~/.config/stormpulse/ at mode 0644. Place the HMAC shared secret as raw bytes at ~/.config/stormpulse/hmac.key at mode 0600. Then skip to detail step 5 (Init).


Troubleshooting

Symptom Check
stormpulse: command not found export PATH="$HOME/.local/bin:$PATH" — detail step 2
Wizard says "Rerun without sudo for user mode" You ran init as root. Drop sudo.
Init says "System-mode install needs root" Missing --user flag. Auto-detection probes for rootless dockerd; if it's not running, init falls back to system mode. Always pass --user on hardened boxes.
Init says "Directory not found: /opt/foo" at the project-dir prompt Open another shell. sudo mkdir -p /opt/foo && sudo chown $USER:$USER /opt/foo. Then re-enter the path at the still-open prompt. (Fixed in 0.1.9: wizard offers to create the dir; if the parent needs root, prints the exact sudo line.)
"Connection refused" in logs Dashboard down, or wrong WebSocket URL
"SSL: CERTIFICATE_VERIFY_FAILED" Wrong CA, or cert not signed by this CA
"Auth setup error" ~/.config/stormpulse/hmac.key missing/empty/unreadable
Status stays "disconnected" pulse_token mismatch with the Server record
Commands fail with "not_found" docker ps doesn't work for your user — rootless dockerd not up
Metrics work but commands don't HMAC key mismatch between dashboard and agent
Agent dies on logout loginctl show-user $USER should show Linger=yes — detail step 7

Update

pipx upgrade storm-pulse-agent
systemctl --user restart stormpulse
journalctl --user -u stormpulse -f

If pipx upgrade finds no new version but you want main:

pipx install --force git+https://git.stormdevelopments.ca/official-public/storm-pulse.git
systemctl --user restart stormpulse