Commit Graph

4 Commits

Author SHA1 Message Date
cproudlock
987f5c0d0f Stop the one-line install handing a standard user a way in
The bootstrap command staged its script into C:\Windows\Temp and then ran it
elevated. That directory grants Users create-file with CREATOR OWNER full
control, so somebody without admin rights can pre-plant the exact path the
elevated run is about to execute. It now stages under Program Files, creates the
directory with an explicit DACL when it is missing, and refuses to run a staged
file that is not owned by an admin principal. A failed hardening is loud rather
than silent: the run stops instead of carrying on with a file it could not
protect.

The token moved out of the query string into a header. In a query string it
lands verbatim in the reverse proxy and IIS logs, which is a credential written
to a file nobody thinks of as a secret store. The query form is still accepted,
because bays provisioned before this change are holding install commands that
use it, and it logs that it was used - without logging the token - so a site can
see who still needs a re-issued command before turning it off with
plugin.endpoints.bootstrapquerytoken.

Two smaller things this also closes:

A request with no token at all answered 400 naming the header, which told an
anonymous caller the state of that switch and contradicted the module's own
promise that anything unusable answers 404. No token is now 404. The 400 is kept
for a token genuinely presented in the query form while the query form is off,
which is a caller who needs to know their command is stale.

The rollout documentation was republishing the vulnerable one-liner verbatim, so
a tech following the runbook reinstated exactly what this fixes. Both documents
now carry the current command.

Also corrects the operator-facing copy for the token, which named a query
argument and a header that do not exist: it is ?t= and X-Endpoint-Bootstrap-Token,
per bootstrap.py's own constants. Anybody following the old text got a 404 with
no reason given.

Both switches added in this range are now declared with the other plugin
settings and reachable from their settings page. Read through Setting.get but
never declared, they were rows that did not exist until someone wrote one by
hand, which on an air-gapped box means the feature was unreachable.
2026-09-18 13:30:08 -04:00
cproudlock
066a19052b Act on the wj architectural review: races, limits, pool and contract gaps
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
Findings F2, F4, F5, F6, F7, F8, F9, F10, F11 and F13 from
wj-architectural-review.md. F1/F1a (the non-sargable LOWER pattern) and F3
(the warranty sync holding a pooled connection) are being handled separately
and are untouched here.

- A collector check-in that raced another for the same PC lost everything:
  the unique index on the application fingerprint raised, nothing caught it,
  and the whole check-in rolled back, discarding state, events and hardware,
  answering the agent with form copy meant for a person. The insert now runs
  in a savepoint, reconciles what the other transaction already wrote, one row
  per savepoint so a second collision skips a row rather than the batch, and
  the check-in commits with a warning. This is the shape the delete side of
  the same function already had.
- Enrollment is deliberately unauthenticated, so it now carries a per-address
  rate limit and a cap on pending requests, and the cap is a setting an admin
  can see and change. A flood can no longer bury the approval queue or grow
  the table without bound.
- The fleet alert poll no longer formats debug strings on every request.
- BusinessUnit is on the contract surface (0.34.0), so the endpoints plugin
  stops reaching into a core table with raw SQL.
- A test walks every asset type descriptor and proves the endpoint it names
  really refuses a caller without the declared permission.
- The plugin settings routes now need a permission; they were telling anyone
  which modules a site runs and where its components live.
- The connection pool is sized explicitly with a short timeout, and the
  arithmetic against the server's connection ceiling is written next to the
  worker count instead of being left to defaults nobody chose.
- Fleet alerts carry both the correct lowercase keys and the old camelCase
  pair. Trays older than 1.7.0 match keys case-sensitively and would have gone
  quiet on every PC not yet upgraded; the old pair goes when they are.
- The dev server proxy points at localhost again, with environment variables
  for the remote-box workflow, and one developer's address is out of the
  server certificate page.
2026-09-16 08:57:04 -04:00
cproudlock
2557d1967d Make the one-line install survive Defender, and approval leave a PC that works
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
All three found testing the bootstrap installer on Windows.

- The published command used the irm-pipe-iex form, which Microsoft Defender
  detects as Trojan:Win32/Commando.A!ml and kills before it runs, every time,
  on a default install. The command now downloads the script to a file, runs
  it, and removes it in a finally block, which Defender does not object to.
  The UI also offers the script as a plain download for a tech who would
  rather save it.
- Approving a request only linked the asset, so a PC whose pending reason was
  "unknown hostname" - which is every PC installed this way - kept getting 403
  on every call afterwards, because its computer record had no hostname to
  match the credential against. Approval now stamps the hostname when the
  record has none, refuses with a 409 naming both when the record has a
  different one, and overwrites only when the admin confirms. The Credentials
  tab offers that confirmation.
- A hostname that resolves to no record now says so instead of reading as an
  impostor, while still refusing the request.
- While waiting for approval the agent retried on the 15-minute telemetry
  cadence, so a PC approved immediately enrolled a quarter of an hour later
  and the installer always ended on "still pending". It now retries about
  every minute for the first ten minutes, then every five to thirty minutes,
  then every fifteen, with jitter. A rate limit or server error still uses the
  slower backoff.
2026-09-15 20:46:04 -04:00
cproudlock
ca57b7f728 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.
2026-09-15 19:33:00 -04:00