Build the plugin set a real site runs, and write down what shipped
The lean-build job proved the delete-a-plugin guarantee with two plugins and four hand-picked marker components. It now builds a real twelve-plugin set, and the assertions follow a rule instead of a hunch: one component from EVERY omitted plugin must be absent, one from EVERY chosen plugin must be present, and the backend tree is checked both ways. An omitted set that grows without its check growing is how a leak ships. CHANGELOG had not been touched across the whole range, so the largest security-behaviour change in the product - per-PC endpoint credentials, the shared-key refusal for enrolled PCs, server pinning, agent 1.20.1 and tray 1.7.1 - had no release record. An [Unreleased] section now carries it, written from the commits rather than from memory. CLAUDE.md and the agent quick-reference stated that ADR-016 bans storing credentials in the registry. It does not, and this fleet depends on HKLM for its collector key and server pins: the next person following that text removes them and silently breaks reporting and pinning on every un-enrolled PC. Corrected, along with two other statements that were false on this branch - the database is MySQL, not PostgreSQL, and column names are lowercase concatenated, not snake_case, which is the opposite of what the gate enforces. The wiki drafts are updated against the code as it stands, each old finding given a verdict with a file and line rather than left as an assertion from a week ago, and a new page records the ADR conformance result - including that the ADR set itself is not on this branch, so nothing here enforces it except the naming and style script. They are also scrubbed of the things that must not reach a published wiki: the site name, the internal git server, developer home paths and the assistant tooling. They now pass the repository's own publishability patterns, which is what the scrub is for.
This commit is contained in:
181
CHANGELOG.md
181
CHANGELOG.md
@@ -702,6 +702,187 @@ ADR-007 and ADR-002.
|
||||
`plugins/tools/frontend/labelPages.js`, where the column contract and the
|
||||
two-sided page order are covered by tests.
|
||||
|
||||
### Added
|
||||
|
||||
- **Every PC gets its own endpoint credential, and enrolls to get it.** The
|
||||
fleet authenticated with one shared collector key: every bay held the same
|
||||
secret, one of them leaking meant reissuing all of them, and a PC could post
|
||||
a check-in naming any asset it liked. A PC now enrolls once and is
|
||||
authenticated as itself thereafter. `POST /api/endpoints/enroll` issues a
|
||||
credential to a PC that matches exactly one computer on hostname and BIOS
|
||||
serial and holds none yet; anything else - unknown hostname, a duplicate, a
|
||||
serial that disagrees, a PC already enrolled - is recorded as a request an
|
||||
administrator approves or rejects, and the agent's persisted nonce is what
|
||||
binds an approved request back to the PC that asked. Enroll never refuses, so
|
||||
a PC installed with no key at all still turns up in the queue instead of
|
||||
vanishing. One auth helper serves every agent route: with
|
||||
`X-Endpoint-Credential` the asset comes from the credential, so a request
|
||||
naming a different PC is refused with `ASSET_MISMATCH` rather than believed,
|
||||
and a revoked credential answers `CREDENTIAL_INVALID` so the agent
|
||||
re-enrolls rather than going quiet. The core collector takes the credential
|
||||
for the computers, printers and endpoints collectors through two hooks on
|
||||
`shopdb.api` (contract 0.33.0).
|
||||
|
||||
Settings > Endpoints > Enrollment is where this is run. The Migration tab
|
||||
shows readiness counts, each PC's state with the reasons it is blocked,
|
||||
"Upgrade selected" and "Upgrade next 25", and live wave progress; a wave
|
||||
packages the agent once and queues the existing per-PC upgrade. The
|
||||
Credentials tab lists enrolled PCs with revoke, pending requests with
|
||||
approve and reject, and the two switches. The fleet list gained a Credential
|
||||
column and filter, and a PC's detail page shows its credential state, with
|
||||
Revoke and Allow re-enrollment for an administrator. Migration 0023 creates
|
||||
`endpointcredentials`, `endpointenrollrequests` and
|
||||
`endpointmigrationwaves`.
|
||||
|
||||
- **The agent pins the server it talks to, and the registry owns the pins.**
|
||||
An air-gapped site runs its own certificate, so "trusted chain" says very
|
||||
little; the agent now checks the server's SubjectPublicKeyInfo SHA-256
|
||||
against a pinned list before it will send a credential anywhere, and a
|
||||
credential is only ever sent back to the server that issued it. Pins live in
|
||||
`HKLM\SOFTWARE\GE\ShopDB` under `ServerPins`, with `appsettings.json` as the
|
||||
fallback. The registry is AUTHORITATIVE whenever that value exists, even
|
||||
when it is empty: the two used to be merged, so removing a pin from HKLM
|
||||
never took effect anywhere. Two pins are carried at once, current and next,
|
||||
so a certificate can be rotated without a flag day.
|
||||
`Set-ShopDBServerPins.ps1` is the GE-Enforce entry point (`-Clear` deletes
|
||||
the value, `-None` writes an empty one) and `Get-ShopDBServerPin.ps1` reads
|
||||
what a PC has. `GET /settings/server-cert` returns the pin to put in that
|
||||
entry, the Server Certificate page shows it, and the page also compares the
|
||||
certificate the site actually presents at its public address with the
|
||||
certificate file, warning when the two disagree.
|
||||
|
||||
- **An agent on a one-off PC from a single command.** A PC outside GE-Enforce -
|
||||
a one-off, a loaner, something rebuilt in a hurry - had no path to an agent
|
||||
short of doing the whole managed install by hand. An administrator now
|
||||
generates a short-lived token in Settings > Endpoints > Endpoints and hands a
|
||||
technician one elevated PowerShell line. The script refuses to run
|
||||
unelevated, refuses a PC that already has an agent (a managed bay gets its
|
||||
agent from GE-Enforce), checks both binaries against hashes the server baked
|
||||
into the package, and installs with NO shared key - the PC then asks to
|
||||
enroll and the script watches locally for two minutes for an administrator to
|
||||
approve it. The token routes answer 404 to anything wrong rather than
|
||||
confirming a token exists, are rate limited per address and are audited with
|
||||
the caller, and a bootstrap package can never carry a collector key.
|
||||
Migration `0024_endpoint_bootstrap_tokens` adds the token table, which stores
|
||||
a hash, an expiry and a use count.
|
||||
|
||||
- **A kiosk admits a caller by where it is on the network, not only by its
|
||||
token.** The device token assumes a handful of fixed screens provisioned
|
||||
once, and that is not how these kiosks are used: most people open the page on
|
||||
their own PC, there is no login, and per-browser provisioning would mean
|
||||
touching every PC on the floor and losing it again at the next reimage.
|
||||
Both the USB and printed-parts kiosks now admit a caller on either ground -
|
||||
the per-device token, or a source address inside an allowlist - the same
|
||||
shape the GE-Enforce client endpoints already use. `shopdb.api` gained
|
||||
`ip_in_cidrs` and `request_ip_allowlisted` (contract 0.35.0) so the two
|
||||
plugins share one gate rather than two copies of a CIDR parser; they fail
|
||||
closed on an empty setting, an unparseable caller address and a malformed
|
||||
entry, and they read `remote_addr`, so the IIS rewrite rule is what makes
|
||||
them meaningful. The allowlists are separate per kiosk
|
||||
(`usb_kiosk_allowed_cidrs`, `printedparts_kiosk_allowed_cidrs`) for the same
|
||||
reason the tokens are, and both SHIP EMPTY: nothing opens by itself, and a
|
||||
site that enters no ranges keeps exactly today's token-only behaviour. Both
|
||||
settings pages get a picker listing the network plugin's subnets with their
|
||||
VLANs, storing resolved CIDR strings rather than subnet ids so the setting
|
||||
still works at a site that does not run that plugin. Stated plainly in the
|
||||
admin UI: anyone on an allowed network can check a drive in or out against a
|
||||
badge number they guess, and badges are attribution, not authorization.
|
||||
|
||||
- **A kiosk says when it is not set up, and an administrator can copy the setup
|
||||
link.** The settings API masks every key whose name contains `token`, so the
|
||||
real kiosk token is on screen for exactly one moment - after Generate, before
|
||||
Save - and a screen provisioned with the literal mask answers 404, which is
|
||||
also how the route answers a caller it has never heard of. Each kiosk gained
|
||||
a permission-gated provisioning-link route that returns the real setup URL to
|
||||
somebody who already holds `settings.edit`, and both settings pages a "Copy
|
||||
setup link" button that falls back to showing the link when the clipboard is
|
||||
refused. Revealing a token writes an audit row. The USB kiosk now asks the
|
||||
server on load whether this screen may be used and says plainly when it may
|
||||
not, instead of rendering a keypad that only fails once somebody has typed
|
||||
their SSO.
|
||||
|
||||
- **The USB audit says whether a returned drive came back wiped and scanned.**
|
||||
Removable-media custody is CMMC evidence, and custody without the state of
|
||||
the media on return is half the record. Both stores already held the values -
|
||||
`sanitized` and `scanned_viruses` in the external `checkinoutlog`,
|
||||
`waswiped` and `scannedviruses` in the self-hosted `usbcheckouts` - and
|
||||
neither mode selected them. Both now report them in the JSON, the CSV and the
|
||||
column list. Only a return carries them, so a check-out row shows blanks
|
||||
rather than asserting something about an event that had not happened yet, and
|
||||
unknown stays blank rather than rendering as "No": both stores keep these
|
||||
nullable deliberately, and "nobody wrote it down" is a different claim from
|
||||
"it came back unwiped".
|
||||
|
||||
### Changed
|
||||
|
||||
- **Endpoint agent 1.20.1 and tray 1.7.1. Ship them together.** Every HTTP
|
||||
client in the agent, script and package downloads included, is now built by
|
||||
one `AgentHttpFactory`, so there is a single place the pins and the
|
||||
credential are applied rather than one per caller. `EnrollmentService`
|
||||
enrolls once using whatever shared key the PC already has, stores the
|
||||
credential DPAPI-protected in a SYSTEM-only directory and swaps to it without
|
||||
a restart; an old server with no enroll route simply leaves the agent on the
|
||||
shared key. A named pipe lets the interactive user ask the agent to send
|
||||
status, send a report, list alerts and acknowledge one, so `--once` and
|
||||
`--report` run as a user become pipe clients, and `appsettings.json` is
|
||||
restricted to SYSTEM and Administrators once the pipe is up. The tray holds
|
||||
no key and reads no agent configuration at all - everything it does goes
|
||||
through that pipe, an acknowledgement is remembered only once the agent
|
||||
confirms it, and when the agent is not running the menu says so instead of
|
||||
failing quietly.
|
||||
|
||||
The version numbers moved from 1.20.0 and 1.7.0 with no behaviour change of
|
||||
their own: those binaries were built and shipped to the display scope before
|
||||
two fixes landed, so a rebuild carried different bytes under the same version
|
||||
number. Detection is by hash and copes with that, but nobody reading a
|
||||
version number can tell two 1.20.0.0 files apart, and the runbook asks for one
|
||||
build across the share, the agent store and the display profile.
|
||||
|
||||
- **A PC that has enrolled is refused the shared fleet key.** Once a PC holds
|
||||
its own credential, presenting the fleet key is either a misconfiguration or
|
||||
somebody else using a leaked secret, and either way the right answer is not
|
||||
to accept it: that call is refused with `SHARED_KEY_REFUSED`. This only ever
|
||||
applies to a PC that has already enrolled. `plugin.endpoints.sharedkeyallowed`
|
||||
still DEFAULTS TO ON, so every un-migrated bay goes on reporting exactly as it
|
||||
does today; a site turns it off deliberately once its fleet has moved, and the
|
||||
page asks for confirmation and says how many PCs are not enrolled yet.
|
||||
`plugin.endpoints.autoenroll` also defaults to on, so a clean hostname and
|
||||
serial match is issued a credential without an administrator waiting on it.
|
||||
|
||||
- **The shared-key page now reads as the migration state it is.** Settings >
|
||||
Endpoints presents the key as what PCs use UNTIL they enroll, with enrollment
|
||||
progress, a link to Enrollment, and rotate and revoke confirmations that say
|
||||
how many PCs would lose their only credential. The settings cards describe
|
||||
the server certificate as what PCs actually receive, on IIS or Docker.
|
||||
`AGENT-CONTRACT.md` documents per-PC credentials, and the rollout runbook
|
||||
covers the phases, the gates, rollback, the pin and wave entries, and every
|
||||
place the shared key lives and how to clear it at cutover.
|
||||
|
||||
### Removed
|
||||
|
||||
- **Client certificate authentication, the 2026-09-14 attempt.** `client_cert.py`,
|
||||
certificate renewal, expiry monitoring, the unauthenticated
|
||||
`/test/client-cert` route, the nginx client-certificate forwarding and the
|
||||
agent certificate script are all gone. The header carrying the client
|
||||
certificate could be forged on IIS, the private key was readable by users,
|
||||
and the shared key remained a fallback underneath it all, so it bought
|
||||
nothing the per-PC credential above does not buy properly. Migration 0023
|
||||
drops the certificate columns from `assets` where a test database has them;
|
||||
0021 and 0022 keep their ids but no longer touch the core table.
|
||||
|
||||
### Security
|
||||
|
||||
- **The tray no longer holds a secret.** It ran as the interactive user and
|
||||
carried enough configuration to talk to the server on its own. Everything it
|
||||
needs now comes over the agent's pipe, which means a standard user on a bay
|
||||
can read nothing out of it.
|
||||
|
||||
- **`collect_log` refuses to collect the things that hold credentials.** Setup
|
||||
answer files, `appsettings.json`, agent directories and key files are refused
|
||||
at enqueue rather than shipped to the server inside a log bundle. The setup
|
||||
token is no longer printed to the log either, and two VM notes that carried a
|
||||
live collector key and advised plain http were deleted rather than corrected.
|
||||
|
||||
## [0.12.0] - 2026-08-20
|
||||
|
||||
Printers become a property of the bay rather than of whoever last walked up to a
|
||||
|
||||
Reference in New Issue
Block a user