- deploy_agent.sh download now requires auth in EVERY mode (agent token via header/?token=, or a dashboard session). It was gated on strict mode only, so in grace mode the served installer published the live fleet token to the internet - accept AGENT_TOKEN_PREVIOUS alongside AGENT_TOKEN so a rotation can roll through the fleet; record agent_token_current per node to know when the previous token can be dropped - /api/agent-token (dashboard-auth) so the UI can build the install command - installer: fall back to the default server URL when an inherited one is unreachable (stale Tailscale address stranded a fresh install), and derive the version banner from the embedded agent instead of a hardcoded string - CPU thermal alerts to Rocket.Chat at 85C with hysteresis clearing at 75C - telemetry_history: 1-minute vitals samples, 7-day TTL (~37 MB fleet-wide), plus /api/history and /api/history-bulk for dashboard sparklines - server watches its own disk (85%) after the 2026-07-23 full-disk outage that killed mongod; per-heartbeat telemetry logging now opt-in via VERBOSE_TELEMETRY - /api/logs returns the newest 25 slim entries per client instead of the full history (2.2 MB every 3s was most of the server's egress); gzip middleware - agent speed probe right-sized to 4/2 MB once a day - server file listing/delete endpoints, search clearing, WAN IP capture Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
220 lines
12 KiB
Markdown
220 lines
12 KiB
Markdown
# SeekRight Pulse RMM — Production Deployment Map
|
||
|
||
This VM (`vm3-mint`, LAN `192.168.1.201`, Tailscale `100.79.183.41`) is the
|
||
**production server** for the SeekRight Pulse RMM system. Field agents and the
|
||
public dashboard both talk to services running here.
|
||
|
||
## FIELD-SYSTEM SAFETY IS THE TOP PRIORITY
|
||
|
||
The agents run on ~26 production toll-plaza machines across India with no easy
|
||
physical access. A broken agent rollout can strand the whole fleet. Every
|
||
change MUST follow these rules:
|
||
|
||
1. **Backward compatible always.** Old agents must keep working against a new
|
||
backend, and new agents against an old backend. Additive changes only:
|
||
new heartbeat-response keys (old agents ignore them) and new Optional
|
||
telemetry fields (old backends… see rule 2). Never rename/remove existing
|
||
API fields, endpoints, or whitelist command keys agents depend on.
|
||
2. **A telemetry field takes three places**: agent payload (deploy_agent.sh),
|
||
`TelemetryPayload` in central_api_prototype.py, and the UI. Pydantic
|
||
silently DROPS unknown fields — a missing model field fails invisibly.
|
||
3. **Agent code is fail-soft.** Anything added to the agent must swallow its
|
||
own errors and never break the heartbeat loop. Follow the existing
|
||
try/except-print pattern. No new inbound ports, no shells for data the
|
||
Python stdlib/psutil can read, outbound connections to our server (plus
|
||
the existing ipify/Rocket.Chat calls) only.
|
||
4. **Canary before fleet.** Update NH-8 (or one expendable node) first,
|
||
verify version + token_ok + features on its card, then batch the rest.
|
||
Each update = ~5 min agent downtime + Rocket.Chat down/up alert pair.
|
||
5. **Whitelist discipline.** Agents execute ONLY commands in commands.json.
|
||
Keep new commands read-only unless explicitly required; remember the file
|
||
is served to ALL agents (no per-client commands) and read per request.
|
||
6. **Token injection is sacred.** The server replaces exactly the assignment
|
||
`AGENT_TOKEN="__AGENT_TOKEN__"` in the served installer (count=1) and the
|
||
installer's sentinel check uses a split placeholder (`"__AGENT_""TOKEN__"`)
|
||
so injection can't rewrite it. Broke once (2026-07-16, agents got empty
|
||
tokens); don't reintroduce.
|
||
7. **Never flip strict auth** (`set_strict.py`) until every system that
|
||
matters shows `agent_token_ok: true` — strict mode locks tokenless agents
|
||
out AND blocks them from downloading the installer to fix themselves.
|
||
8. **Verify before rollout**: `bash -n deploy_agent.sh`, extract the embedded
|
||
agent (between the heredoc markers) and `ast.parse` it, and exercise new
|
||
functions against a mock where feasible.
|
||
|
||
## Agent version history
|
||
|
||
- **legacy / pre-3.2** — no token, no version reporting, old two-call poll
|
||
(`/api/telemetry` + `/api/get-command`). Still on: HYDTOT pair + offline nodes.
|
||
- **3.2-auth** — X-Agent-Token auth, single `/api/heartbeat` call, sends
|
||
cpu_temp + agent_version. Deployed fleet-wide 2026-07-16.
|
||
- **3.4-net** (current template, NOT yet fleet-deployed) — dashboard-configured
|
||
SHIFT path for video fetch, SHIFT folder search, 6-hourly link-speed probe
|
||
vs our own server, local interface IPs in every heartbeat (`import socket`
|
||
is stdlib IP discovery — NOT a websocket; no packets sent, nothing listens).
|
||
|
||
## Architecture
|
||
|
||
```
|
||
Internet
|
||
│
|
||
▼
|
||
Synology reverse proxy (takeleapindia.synology.me / 106.51.70.111)
|
||
├── https://rmm.seekright.com → this VM : vite port (currently 4173)
|
||
└── https://rmm-backend.seekright.com → this VM : 8000
|
||
│
|
||
Field agents (X-Agent-Token) ───────────────────┤
|
||
▼
|
||
FastAPI backend (uvicorn :8000, 4 workers)
|
||
│
|
||
MongoDB localhost:27017, db `rmm_db`
|
||
```
|
||
|
||
## Components
|
||
|
||
### Backend — `/opt/rmm-backend`
|
||
- FastAPI app `central_api_prototype.py`, run by **systemd `rmm-backend.service`**
|
||
(User=root): `venv/bin/uvicorn central_api_prototype:app --host 0.0.0.0 --port 8000 --workers 4`
|
||
- Env: systemd `EnvironmentFile=/opt/rmm-backend/.env` injects vars **at service
|
||
start**. The `APP_ENV` / `.env.production` / `.env.development` logic in the
|
||
code is mostly vestigial here: values from `.env` are already in the process
|
||
environment and `load_dotenv` never overrides them. **Editing `.env` requires
|
||
a service restart to take effect.**
|
||
- Data: MongoDB `rmm_db` (`clients`, `logs`, `config` collections). Alerts go to
|
||
Rocket.Chat via `ROCKETCHAT_WEBHOOK_URL`.
|
||
- `commands.json` = whitelist of commands agents may run. Read from disk **per
|
||
request** (no restart needed). Also editable live from the dashboard config
|
||
editor (`/api` load/save endpoints) — meaning production edits land in the
|
||
working tree and will conflict with `git pull` (see gotchas).
|
||
- Agent auth: agents send `X-Agent-Token` (value `AGENT_TOKEN` in `.env`). Mode
|
||
lives in Mongo `config._id=agent_auth`: `grace` (default — tokenless legacy
|
||
agents still accepted) or `strict` (reject without token). Flip with
|
||
`set_strict.py` only after every field agent has been updated
|
||
(`deploy_agent.sh` / the `update_agent` command).
|
||
- Dashboard login: `POST /api/login`, credentials `DASHBOARD_USERNAME` /
|
||
`DASHBOARD_PASSWORD` from `.env`, JWT signed with `JWT_SECRET_KEY`.
|
||
|
||
### Frontend — `/opt/rmm-ui`
|
||
- React + Vite, run by **systemd `rmm-frontend.service`** (User=root):
|
||
`npm run dev -- --mode production` (a vite **dev server**, not a static build).
|
||
- Port is set in `vite.config.js` (`server.port`, currently **4173** — must
|
||
match the Synology reverse-proxy upstream);
|
||
`allowedHosts: ["rmm.seekright.com"]` must include the public hostname or
|
||
vite rejects proxied requests.
|
||
- Vite watches files: pulled code changes hot-reload automatically, and a
|
||
`vite.config.js` change makes vite restart itself — BUT an in-process
|
||
restart keeps whatever port vite already bound (including an auto-bumped
|
||
one like 4174 after a port collision). To change ports for real:
|
||
`sudo systemctl restart rmm-frontend.service`.
|
||
- API base URL comes from `/opt/rmm-ui/.env.production`
|
||
(`VITE_API_BASE_URL=https://rmm-backend.seekright.com`).
|
||
|
||
### Reverse proxy — Synology (NOT on this VM)
|
||
- No nginx/apache runs on this VM. TLS + routing for both public hostnames is
|
||
done on the Synology at `takeleapindia.synology.me` (106.51.70.111).
|
||
- **If the vite port changes in `vite.config.js`, the Synology reverse-proxy
|
||
upstream port must be updated to match** — otherwise the site 502s.
|
||
This exact thing happened 2026-07-16 (commit `dc604a4` moved 4173 → 6173).
|
||
|
||
### Field agents (remote machines)
|
||
- Run `/opt/seekright-agent/client_agent_prototype.py` on each monitored node;
|
||
poll `GET /api/get-command` and push `POST /api/telemetry`.
|
||
- Installed/updated via `deploy_agent.sh` (served by the backend; in strict
|
||
mode downloading it requires a valid agent token).
|
||
|
||
## Agent token rotation (and why the download is authenticated)
|
||
|
||
The served installer has the LIVE agent token injected into it. Until
|
||
2026-08-03 that download was only gated in strict mode, so in grace mode
|
||
`https://rmm-backend.seekright.com/deploy_agent.sh` handed the fleet token to
|
||
anyone on the internet. It is now authenticated in EVERY mode (agent token via
|
||
`X-Agent-Token` header or `?token=`, or a logged-in dashboard session).
|
||
|
||
To rotate the token:
|
||
1. `.env`: move the current value to `AGENT_TOKEN_PREVIOUS=`, set a new
|
||
`AGENT_TOKEN=` (`python3 -c "import secrets; print(secrets.token_urlsafe(32))"`).
|
||
Both are accepted while both are set — agents authenticate the self-update
|
||
with the old token and receive the new one.
|
||
2. Restart the backend, then run `update_agent` across the fleet.
|
||
3. When every active node shows `agent_token_ok: true` AND is on the new token,
|
||
delete `AGENT_TOKEN_PREVIOUS` from `.env` and restart. Only then flip strict
|
||
(`set_strict.py`) — that is what actually closes the agent endpoints.
|
||
|
||
Note: grace mode means the agent endpoints accept UNAUTHENTICATED requests
|
||
today. Rotating the token stops the leak, but strict mode is what enforces it.
|
||
|
||
## Installing the agent on a NEW system
|
||
|
||
```bash
|
||
curl -fsSL "https://rmm-backend.seekright.com/deploy_agent.sh" -o /tmp/deploy_agent.sh
|
||
sudo bash /tmp/deploy_agent.sh <CLIENT_ID> # e.g. KRBOT-Narwana
|
||
```
|
||
|
||
- ALWAYS pass a CLIENT_ID on a fresh install — the fallback is the machine's
|
||
hostname (that's how the stale `hamsadmin-MS-7E07` client entry happened).
|
||
- ALWAYS download from the server, never copy deploy_agent.sh out of the repo:
|
||
the server injects the live agent token at download time.
|
||
- Re-running with no args is a safe in-place upgrade (keeps the node's
|
||
identity) — the `update_agent` fleet command relies on this.
|
||
- In strict auth mode the download itself needs the token:
|
||
append `?token=<AGENT_TOKEN from .env>` to the URL.
|
||
- After install: the node appears in the dashboard in ~30s; set its SHIFT
|
||
folder path in the UI card so video fetch/search works.
|
||
|
||
## How to deploy
|
||
|
||
Backend:
|
||
```bash
|
||
cd /opt/rmm-backend
|
||
git status # working tree should be clean — see gotchas
|
||
git pull
|
||
sudo systemctl restart rmm-backend.service
|
||
systemctl status rmm-backend.service --no-pager -n 20 # check for crash loop
|
||
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8000/docs # expect 200
|
||
```
|
||
|
||
Frontend:
|
||
```bash
|
||
cd /opt/rmm-ui
|
||
git pull # vite hot-reloads; restarts itself on config change
|
||
# only if deps changed: npm install && sudo systemctl restart rmm-frontend.service
|
||
curl -s -o /dev/null -w '%{http_code}\n' -H 'Host: rmm.seekright.com' http://localhost:4173/
|
||
curl -sk -o /dev/null -w '%{http_code}\n' https://rmm.seekright.com/ # via Synology
|
||
```
|
||
|
||
## Gotchas / history
|
||
|
||
- **Don't hand-edit tracked files on this server** (`commands.json`, `.env`
|
||
are tracked in git). Local edits block `git pull`. Commit changes to the repo
|
||
instead. Dashboard edits to `commands.json` also dirty the tree — push them
|
||
upstream after changing them.
|
||
- 2026-07-16: pull of `dc604a4` changed the vite port 4173 → 6173 and broke
|
||
https://rmm.seekright.com (Synology still forwarded to 4173). Resolved by
|
||
reverting `vite.config.js` to port 4173 in the working tree. That revert is
|
||
a LOCAL change to a tracked file — commit and push it (or the next pull
|
||
will conflict and re-break the site by pulling 6173 back in).
|
||
- The backend service must be restarted after every backend pull — uvicorn has
|
||
no auto-reload in the unit. A long-running process can silently serve
|
||
weeks-old code (happened Jul 6 → Jul 16).
|
||
- Adding a telemetry field takes THREE places: agent payload (in
|
||
`deploy_agent.sh`), `TelemetryPayload` in `central_api_prototype.py`, and the
|
||
UI. Pydantic silently DROPS any field missing from `TelemetryPayload` —
|
||
agents sent `cpu_temp` for a while with the backend discarding it and no
|
||
error anywhere (found 2026-07-16).
|
||
- Ports on this VM: 8000 backend, 6173 rmm-ui vite, 5173 auditor-portal vite
|
||
(localhost only), 7514 + others auditor portal, 4430–4433 MeshCentral,
|
||
Grafana also runs here. Check `ss -tlnp` before assigning a new port.
|
||
|
||
- 2026-07-23 INCIDENT: the server's own disk hit 100% → mongod fatally aborted
|
||
("Writing to log file failed") and stayed down ~22h (no Restart= in its unit)
|
||
→ dashboard showed zero systems while agents kept heartbeating into a dead DB.
|
||
Mitigations now in code: per-heartbeat telemetry stdout dumps are opt-in
|
||
(`VERBOSE_TELEMETRY=true` in .env to re-enable) and the backend Rocket.Chat
|
||
alerts when its own `/` passes 85%. Recommended systemd hardening: a mongod
|
||
override with `Restart=on-failure`, and `SystemMaxUse=2G` in journald.conf.
|
||
|
||
## Other services on this VM (not part of RMM deploys)
|
||
|
||
- `meshcentral.service` — MeshCentral remote management (node, ports 4430–4433)
|
||
- `grafana` — monitoring dashboards
|
||
- `/opt/auditor-portal` — separate project (its own vite/node dev servers)
|