This commit is contained in:
+92
-3
@@ -1,11 +1,12 @@
|
||||
# Agent API authentication
|
||||
|
||||
Reference for the per-device signature gate on the agent-facing HTTP
|
||||
API. Three endpoints are gated:
|
||||
API. Four endpoints are gated:
|
||||
|
||||
- `POST /api/heartbeat`
|
||||
- `POST /api/sysinfo`
|
||||
- `POST /api/unattended-password`
|
||||
- `POST /api/agent/exec-result` — managed-only (no legacy/unsigned path)
|
||||
|
||||
For the operator workflow — turning it on, the dashboard toggle, what
|
||||
happens when a managed agent is uninstalled — see the matching section
|
||||
@@ -40,6 +41,7 @@ Every subsequent request HTTP API (port 21114)
|
||||
with sk ───── POST /api/heartbeat ─────► against peer.pk
|
||||
───── POST /api/sysinfo ─────► (when peer.managed=1)
|
||||
───── POST /api/unattended-password ─────►
|
||||
───── POST /api/agent/exec-result ─────► (always required)
|
||||
```
|
||||
|
||||
The same secret key signs both the rendezvous identity proof and the
|
||||
@@ -212,6 +214,86 @@ unsigned request, and it self-resolves on the next sync tick.
|
||||
| Admin row shows **Unsigned** for a peer running hello-agent | Agent hasn't completed its first signed POST yet (keypair race), or it's running a build |
|
||||
| | that pre-dates the signing patch — check `vendor/rustdesk/src/hbbs_http/sign.rs` is present. |
|
||||
|
||||
## Remote PowerShell exec
|
||||
|
||||
Layered on top of the signature gate. An admin in the dashboard sends a
|
||||
script to a peer; the agent runs it as its service account; output and
|
||||
exit code come back into the dashboard within ~1 s (the heartbeat
|
||||
interval).
|
||||
|
||||
### Three independent gates
|
||||
|
||||
A dispatch must pass **all three** server-side checks before a row is
|
||||
queued — the agent never sees a script it shouldn't have:
|
||||
|
||||
1. **`AuthedUser.is_admin`** — only admins can dispatch.
|
||||
2. **`peer.managed = 1`** — the same flag the signed-API gate uses.
|
||||
This means TOFU has already promoted the peer (or an admin explicitly
|
||||
flipped it). Stock RustDesk clients are uninvited.
|
||||
3. **Strategy `enable-remote-exec = "Y"`** — the resolved strategy for
|
||||
the peer must explicitly opt in. Defaults to off. Set it on a strategy,
|
||||
assign the strategy to the peer (or its group / owner), exec is now
|
||||
live for that scope. *Server-side only — the key is never pushed to
|
||||
the client.* See [STRATEGIES.md](STRATEGIES.md).
|
||||
|
||||
### Wire path
|
||||
|
||||
```
|
||||
Admin UI ──POST /admin/pages/devices/:id/exec──► Server inserts exec_history(status='queued')
|
||||
│
|
||||
▼
|
||||
Agent's next heartbeat reply carries `exec: [{cmd_id, script, max_secs, max_bytes}]`;
|
||||
the server flips the row to 'running' atomically (exec_pop_queued_for_peer).
|
||||
│
|
||||
▼
|
||||
Agent runs `powershell.exe -NoProfile -NonInteractive -ExecutionPolicy Bypass -Command -`,
|
||||
writes the script to stdin, captures stdout+stderr up to 1 MiB, kills on 5-minute wall clock.
|
||||
│
|
||||
▼
|
||||
Admin UI ◄──poll /admin/pages/devices/:id/exec/:cmd_id/poll── Server ◄──POST /api/agent/exec-result (signed)── Agent
|
||||
```
|
||||
|
||||
### Limits
|
||||
|
||||
| Setting | Default | Where |
|
||||
|----------------|--------:|------------------------------------------------------------|
|
||||
| Script size | 32 KiB | `src/api/admin/pages/exec.rs::MAX_SCRIPT_BYTES` |
|
||||
| Wall-clock | 300 s | `src/api/heartbeat.rs::EXEC_MAX_SECS` (sent to agent) |
|
||||
| Output capture | 1 MiB | `src/api/heartbeat.rs::EXEC_MAX_BYTES` (sent to agent) |
|
||||
| In-flight/peer | 1 | `exec_in_flight_count > 0` blocks new dispatch |
|
||||
|
||||
The agent enforces wall-clock and output-capture locally — server caps
|
||||
are advisory unless you also harden the agent. If you don't trust your
|
||||
own agent build, the server caps still bound storage and replay-cost.
|
||||
|
||||
### Result POST authentication
|
||||
|
||||
`POST /api/agent/exec-result` is the only agent endpoint that **always**
|
||||
requires a signature, even when the peer happens to be `managed=0`.
|
||||
There's no legacy compatibility story for exec — if the agent can't
|
||||
sign, the result POST is rejected outright and the row sits in `running`
|
||||
until an admin notices. Reason: an attacker who can spoof `(id, uuid)`
|
||||
shouldn't be able to forge "I executed your command and here's the
|
||||
output" for a device they don't actually control.
|
||||
|
||||
### Operational notes
|
||||
|
||||
- **The dispatch row stays `running` until the agent posts a result.**
|
||||
If the agent crashes mid-script there's no automatic timeout cleanup
|
||||
yet (planned: a hourly task that flips long-stuck `running` rows to
|
||||
`errored`). Admins can dispatch a fresh command after the in-flight
|
||||
one ages past 5 minutes by waiting; the in-flight check is wall-clock
|
||||
based on `issued_at`.
|
||||
- **Output may contain secrets.** A `Get-Content` of a credential file
|
||||
goes straight into the `exec_history` table and the admin UI. The
|
||||
current schema has no per-row access control beyond "is_admin"; if
|
||||
you need finer scoping, audit log retention plus your `users` table
|
||||
ACL is the only knob.
|
||||
- **No interactive REPL yet.** Each dispatch is one shot: write script,
|
||||
run, read result. Multi-command sessions or interactive prompts
|
||||
(Read-Host, sudo-style passwords) will hang and time out. This is by
|
||||
design for v1 — Option B in the original architecture discussion.
|
||||
|
||||
## File map
|
||||
|
||||
Server:
|
||||
@@ -220,24 +302,31 @@ Server:
|
||||
|-------------------------------------------|------------------------------------------------------------------|
|
||||
| `src/api/device_auth.rs` | The verifier (extractor + replay cache + TOFU promote). |
|
||||
| `src/api/heartbeat.rs`, `src/api/sysinfo.rs`, `src/api/unattended.rs` | Wired to call `verify` then `enforce_managed_for_id`. |
|
||||
| `src/api/agent_exec.rs` | `POST /api/agent/exec-result` (sig-required, no legacy path). |
|
||||
| `src/api/peers.rs::set_managed` | `PUT /api/peers/:id/managed` admin endpoint. |
|
||||
| `src/api/admin/pages/devices.rs::toggle_managed` | Dashboard action handler. |
|
||||
| `src/api/admin/pages/exec.rs` | Per-device exec page (form + history + HTMX poll fragment). |
|
||||
| `src/api/strategy/mod.rs::allows_remote_exec` | Resolves the per-peer strategy and reads `enable-remote-exec`. |
|
||||
| `src/database.rs::M2_SOFT_ALTERS` | `ALTER TABLE peer ADD COLUMN managed`. |
|
||||
| `src/database.rs::M5_SCHEMA` | `CREATE TABLE exec_history` + indexes. |
|
||||
| `src/database.rs::peer_get_auth, peer_set_managed` | DB helpers (untyped `sqlx::query` so they survive the no-DB-migrated dev build). |
|
||||
| `src/database.rs::exec_create, exec_pop_queued_for_peer, exec_finish, exec_get_by_cmd_id, exec_in_flight_count, exec_list_for_peer` | Exec lifecycle helpers. |
|
||||
|
||||
Agent — hello-agent vendor tree:
|
||||
|
||||
| Path | Purpose |
|
||||
|------------------------------------------------------------|---------------------------------------------------------------|
|
||||
| `vendor/rustdesk/src/hbbs_http/sign.rs` | The signer. |
|
||||
| `vendor/rustdesk/src/hbbs_http/sync.rs` (call sites) | Heartbeat + sysinfo POSTs now sign. |
|
||||
| `vendor/rustdesk/src/hbbs_http/sync.rs` (call sites + `EXEC_SENDER`) | Heartbeat + sysinfo POSTs sign; heartbeat reply forwards queued `exec` requests to the broadcast channel. |
|
||||
| `vendor/rustdesk/src/common.rs::post_request_, parse_simple_header` | Header parser now accepts `\n`-separated `Name: Value` pairs (backward-compatible). |
|
||||
| `vendor/rustdesk/src/lib.rs` | `pub mod hbbs_http` — required so hello-agent can reach both `::sign` and `::sync::exec_signal_receiver`. |
|
||||
|
||||
Agent — hello-agent crate (outside the vendor tree):
|
||||
|
||||
| Path | Purpose |
|
||||
|-------------------------------------|-----------------------------------------------------------------------------------------|
|
||||
| `src/unattended_password.rs::try_report` | Reports the per-boot password to `/api/unattended-password`; now signs the POST. |
|
||||
| `src/unattended_password.rs::try_report` | Reports the per-boot password to `/api/unattended-password`; signs the POST. |
|
||||
| `src/exec.rs` | PowerShell runner. Subscribes to the sync layer's broadcast channel, spawns `powershell.exe`, captures stdout/stderr with caps, signs and POSTs the result to `/api/agent/exec-result`. Started from `run_server()` in main.rs. |
|
||||
|
||||
## Out of scope
|
||||
|
||||
|
||||
@@ -303,6 +303,47 @@ keys and what each one does.
|
||||
|
||||
---
|
||||
|
||||
## Remote PowerShell exec (per-peer, strategy-gated)
|
||||
|
||||
Admins can dispatch a PowerShell script to a managed device from the
|
||||
dashboard's **Run command…** action (Devices page row menu, or directly
|
||||
via `/admin/pages/devices/:peer_id/exec`). The agent runs the script as
|
||||
its service account — typically LocalSystem on Windows — and the
|
||||
output streams back into the dashboard within ~1 s.
|
||||
|
||||
This feature is **disabled by default**. To enable it for a peer (or
|
||||
fleet):
|
||||
|
||||
1. Edit (or create) a strategy on the **Strategies** page with the JSON:
|
||||
```json
|
||||
{ "enable-remote-exec": "Y" }
|
||||
```
|
||||
(mix with whatever other strategy options you already push)
|
||||
2. Assign that strategy to the peer, its device group, or its owner.
|
||||
3. The peer's `Auth` column must show **Signed** — exec is refused on
|
||||
`peer.managed=0` peers. See [AGENT-API-AUTH.md](AGENT-API-AUTH.md).
|
||||
|
||||
All three gates (admin role, managed=1, strategy opt-in) are enforced
|
||||
server-side at dispatch time. The strategy key is never pushed to the
|
||||
client — it's checked on the server only and serves purely as the
|
||||
authorization toggle.
|
||||
|
||||
Caps (defaults; live in `src/api/heartbeat.rs` and
|
||||
`src/api/admin/pages/exec.rs`):
|
||||
|
||||
- Script size: **32 KiB** per dispatch.
|
||||
- Wall clock: **5 minutes** per command; the agent kills the process
|
||||
on timeout and marks the row `timed_out`.
|
||||
- Output capture: **1 MiB** combined stdout+stderr; further bytes are
|
||||
drained and discarded, the row gets `truncated=true`.
|
||||
- One in-flight exec per peer at a time.
|
||||
|
||||
See [AGENT-API-AUTH.md](AGENT-API-AUTH.md) for the wire format,
|
||||
authentication, and threat model. Result POSTs are mandatory-signed —
|
||||
there's no legacy/unsigned path for the exec result endpoint.
|
||||
|
||||
---
|
||||
|
||||
## Agent API signing (per-peer)
|
||||
|
||||
`POST /api/heartbeat`, `POST /api/sysinfo`, and
|
||||
|
||||
@@ -98,6 +98,7 @@ window) are listed for completeness but are normally per-user.
|
||||
| `disable-change-permanent-password` | `Y`/`N` | Prevent the user changing the permanent password |
|
||||
| `disable-change-id` | `Y`/`N` | Prevent the user changing the device ID |
|
||||
| `disable-unlock-pin` | `Y`/`N` | Disable the unlock PIN feature |
|
||||
| `enable-remote-exec` | `Y`/`N` | Allow admins to dispatch PowerShell scripts to this peer via the dashboard's **Run command** action. Server-side only — the value is checked at dispatch time, never pushed to the client. See [AGENT-API-AUTH.md](AGENT-API-AUTH.md) for the auth model. Off by default; only effective on `peer.managed=1` peers. |
|
||||
|
||||
### Network & connectivity
|
||||
|
||||
|
||||
Reference in New Issue
Block a user