Install an agent on a one-off PC from a single command
Some checks failed
CI / backend (push) Has been cancelled
CI / naming (push) Has been cancelled
CI / gates (push) Has been cancelled
CI / frontend (push) Has been cancelled
CI / lean-build (push) Has been cancelled

An admin generates a short-lived token in Settings > Endpoints > Endpoints and
copies the command it produces. A tech runs that one line in an elevated
PowerShell on a PC with no agent:

    powershell -ExecutionPolicy Bypass -c "irm https://<server>/shopdb/api/
    endpoints/siteagent/bootstrap.ps1?t=<token> | iex"

The script refuses to run unelevated, refuses a PC that already has the agent
(a managed bay gets it from GE-Enforce), downloads the package the existing
builder produces, checks both binaries against the hashes the server baked in,
installs with NO shared key, and reports what happened. The PC then asks to
enroll and waits for an admin to approve it, which the script watches for
locally for two minutes before exiting.

- New endpointbootstraptokens table and migration
  endpoints0024bootstraptokens: hash only, expiry, use count, revoke.
- Admin routes to generate, list and revoke tokens; the secret is shown once.
- Two token-only routes: the script, and the package (which spends a use).
  Anything wrong with the token answers 404 rather than confirming it exists,
  both are rate limited per address and both are audited with the caller.
- The package builder is now shared rather than duplicated, and a bootstrap
  package can never carry a collector key.
This commit is contained in:
cproudlock
2026-09-15 19:33:00 -04:00
parent 408cf08628
commit ca57b7f728
14 changed files with 1718 additions and 31 deletions

View File

@@ -763,3 +763,74 @@ Read needs `endpoints.fleet`; writes need the admin role and are audited.
`{"assetids": [...]}`. An explicit id is queued when this is the PC's only
blocker and is reported in `retried`; `{"next": n}` never picks such a PC, and
a PC with any other blocker as well is still skipped.
### 9.8 One-line bootstrap install
For a ONE-OFF PC. A managed bay keeps getting the agent from GE-Enforce; this
is the path for a machine nobody is going to author a manifest entry for.
An admin mints a short-lived token in Settings > Endpoints > Build site agent >
One-line install and hands a technician one command, which is run in an
ELEVATED PowerShell on a PC with no agent:
```
powershell -ExecutionPolicy Bypass -c "irm <baseurl>/api/endpoints/siteagent/bootstrap.ps1?t=<token> | iex"
```
The token is only an authorization to FETCH the installer. The package it
serves carries no shared key, whatever `plugin.endpoints.sharedkeyallowed`
says, so the PC enrolls by the normal 9.2 path and waits in Pending until an
admin approves it.
Admin routes (admin role, audited):
- `POST /api/endpoints/siteagent/bootstrap/tokens` `{"hours": 24, "maxuses": 25}`
(`hours` 1-168, `maxuses` 1-500). Returns `{"secret", "command", "token"}`.
The secret is in the response exactly once; only its sha256 and a 12-character
`tokenprefix` are stored.
- `GET /api/endpoints/siteagent/bootstrap/tokens` - prefix, created, expires,
`usecount` / `maxuses`, `revokedat`, `lastusedat`, `lastusedip` and a `state`
of `active`, `expired`, `exhausted` or `revoked`. Never a secret.
- `POST /api/endpoints/siteagent/bootstrap/tokens/<tokenid>/revoke`
Token-authenticated routes. No login; the token is the whole authorization and
travels in the query string as `t`, because a PC with no agent has neither a
credential nor a user session to put in a header:
| Route | Spends a use | Returns |
|---|---|---|
| `GET /api/endpoints/siteagent/bootstrap.ps1?t=...` | no | `text/plain` PowerShell, generated per token |
| `GET /api/endpoints/siteagent/bootstrap/package?t=...` | yes | the keyless agent package ZIP |
Both answer **404** for a token that is unknown, malformed, expired, revoked or
exhausted, so the URL confirms nothing about which tokens exist. Both are
rate-limited per IP (fixed window, `ENDPOINTS_BOOTSTRAP_RATELIMIT_MAX` in
`ENDPOINTS_BOOTSTRAP_RATELIMIT_WINDOW_SECONDS`, 429 `RATE_LIMITED` over
budget) and both write an audit row against `EndpointBootstrapToken` carrying
the caller's IP. Fetching the script deliberately does not spend a use: a
retried paste must not burn the download budget.
The generated script:
- refuses to run unelevated (exit 1) and says to reopen PowerShell as
administrator;
- refuses when the agent service already exists (exit 2) and says a managed bay
should get the agent from GE-Enforce, unless `SHOPDB_BOOTSTRAP_FORCE` is set
to anything but `0` in the same session;
- downloads the package to a temp directory over TLS with normal chain
validation (nothing relaxes certificate checking), expands it, and verifies
`shopdbagent.exe` and `shopdb-endpoint-tray.exe` against the SHA-256 values
the server baked in at generation time. A mismatch, a missing binary, or a
binary the server never staged refuses the install (exit 3);
- runs the package's `install.ps1` with no key, then deletes the temp
directory;
- prints the installed version and service state, and exits 4 when the service
is not Running;
- prints "This PC is waiting for approval - an admin approves it in Settings >
Endpoints > Enrollment", then watches
`%ProgramData%\ShopDB\EndpointAgent\credential\credential.bin` for up to two
minutes and prints "approved and enrolled" if the credential lands. This is a
LOCAL file watch, not a server poll: there is no public enrollment-status
route and adding one would hand an anonymous caller a way to probe the fleet.
Exit code 0 on install success even when the PC is still pending.