Found in an end-to-end test on a Windows 11 VM against a self-signed server.
- Rollback: a 1.20 agent re-enrolls within a second of an un-enroll, so an
older agent pushed afterwards arrived on an enrolled PC and was refused.
Revoke now takes {"blockreenroll": true, "reason": ...}; a blocked PC is
never issued a credential and keeps working on the shared key until
POST .../assets/<assetid>/allow-reenroll. The Enrollment page offers the
block when un-enrolling and shows blocked PCs with an allow button.
- First upgrade of old agents: agents before 1.20 check the server
certificate when downloading the upgrade, so a store wave failed on PCs that
do not trust a self-signed server while the Migration tab said ready. A
failed download now marks the PC blocked with "server certificate not
trusted by this PC". New Install-ShopDBServerCertificate.ps1 (GE-Enforce,
SYSTEM) adds a non-CA server certificate to the trusted roots after checking
its thumbprint; the server certificate page offers a .cer download and the
SHA-1 thumbprint.
- The generated installer writes ServerPins (with an optional next pin for a
planned rotation), can leave the shared key out so the PC waits for
approval, never touches an existing credential, and skips the trusted-root
import for a CA certificate.
- The agent re-reads server pins about every 60 seconds and recycles pooled
connections within 2 minutes, so a pin change needs no restart.
- An alert can only be acknowledged by a PC it targets.
- A wave no longer counts a PC as failed after a later wave upgraded it.
- The scripts report a pin mismatch only when the pin really is missing, and
otherwise log the real error.
- Runbook: trust step before the first wave, rollback order, pin re-read,
verifying the pin against the live URL; agent contract updated.
318 lines
15 KiB
Markdown
318 lines
15 KiB
Markdown
# Per-PC credentials rollout
|
|
|
|
Runbook for moving a site's endpoint fleet from the shared collector key to
|
|
per-PC credentials. The wire contract is `AGENT-CONTRACT.md` section 9.
|
|
|
|
Placeholders: `<server>` is the ShopDB base URL, `<share>` the GE-Enforce share
|
|
root, `<pin>` a server key pin, `<pilotbay>` / `<pilothost>` the pilot PC.
|
|
|
|
## What changes
|
|
|
|
- Every endpoint gets its own secret (`sdbe_...`), stored in
|
|
`%ProgramData%\ShopDB\EndpointAgent\credential\credential.bin`, DPAPI
|
|
LocalMachine, readable by SYSTEM and Administrators only.
|
|
- The server knows which asset is calling from the secret. A PC can no longer
|
|
report as another PC.
|
|
- The tray holds no secret. It talks to the SYSTEM agent over a local pipe.
|
|
- The agent, the upgrade script and the SYSTEM reporters
|
|
(`Report-AssetToShopDB.ps1`, `Report-PrintersToShopDB.ps1`) pin the server's
|
|
public key when `ServerPins` is set.
|
|
- The shared key keeps working for PCs that have NOT enrolled, until an admin
|
|
turns it off. An enrolled PC is always refused the shared key
|
|
(`401 SHARED_KEY_REFUSED`).
|
|
|
|
## Before you start
|
|
|
|
1. Server deployed with the endpoints migration `endpoints0023credentials`
|
|
applied (`flask plugin upgrade-all`).
|
|
2. Settings -> Endpoints -> Server Certificate shows a **Public key pin (SPKI
|
|
SHA-256)**. Cross-check it from a PC:
|
|
`Get-ShopDBServerPin.ps1 -Url https://<server>`. The two must match.
|
|
3. Agent 1.20 (and tray 1.7) uploaded as the current release under Settings ->
|
|
Endpoints -> Upload New/Legacy Agents. Keep the previous release staged as a
|
|
legacy build: it is the rollback.
|
|
4. Settings -> Endpoints -> Enrollment -> Credentials: **Shared key allowed**
|
|
on, **Auto-enroll** on (both are the defaults).
|
|
5. The GE-Enforce manifest scope that will carry the entries below is at
|
|
`Version` 2.2 or later, and the fleet's GE-Enforce client library
|
|
understands `TargetHostnames` / `TargetMachineNumbers`. An older manifest or
|
|
library ignores those lists and applies the entry to EVERY PC in scope. The
|
|
same happens with an empty list: never publish a targeted entry with a blank
|
|
target.
|
|
|
|
## Phase 1: pins on the PCs
|
|
|
|
Pins must be on a PC BEFORE agent 1.20 arrives there. A 1.20 agent with no pin
|
|
falls back to plain Windows validation, which a self-signed or internal-CA
|
|
server may fail; a pin set after the fact is not a rollout order you want to
|
|
debug per bay.
|
|
|
|
Stage `plugins/endpoints/client/Set-ShopDBServerPins.ps1` on the share and add
|
|
a PS1 entry. Pilot first:
|
|
|
|
```json
|
|
{
|
|
"_comment": "Server key pin for per-PC credentials. Pilot only.",
|
|
"Name": "ShopDB server pins",
|
|
"Type": "PS1",
|
|
"Script": "scripts/Set-ShopDBServerPins.ps1",
|
|
"Args": "-Pins <pin>",
|
|
"DetectionMethod": "Always",
|
|
"TargetMachineNumbers": ["<pilotbay>"]
|
|
}
|
|
```
|
|
|
|
Then widen by hostname list (or machine number list) in batches:
|
|
|
|
```json
|
|
{
|
|
"Name": "ShopDB server pins",
|
|
"Type": "PS1",
|
|
"Script": "scripts/Set-ShopDBServerPins.ps1",
|
|
"Args": "-Pins <pin>",
|
|
"DetectionMethod": "Always",
|
|
"TargetHostnames": ["<host1>", "<host2>", "<host3>"]
|
|
}
|
|
```
|
|
|
|
The script compares before it writes, so `Always` costs nothing per cycle.
|
|
A running agent picks up a changed `ServerPins` within about 60 seconds; no
|
|
service restart is needed.
|
|
|
|
**Gate:** on the pilot, `HKLM\SOFTWARE\GE\ShopDB` and
|
|
`HKLM\SOFTWARE\WOW6432Node\GE\ShopDB` both hold `ServerPins` (REG_MULTI_SZ)
|
|
equal to the pin on the settings page, and the next `report-asset-*.log` under
|
|
`C:\Logs\Shopfloor` says `server pin check on (1 pin(s))` followed by a
|
|
RESPONSE line.
|
|
|
|
**Rotating the server key later:** any server-side change that gives the
|
|
certificate a new key (a regenerated or re-issued certificate) changes the pin.
|
|
Add the NEXT pin everywhere first: put the old AND the new pin in `-Pins`
|
|
(`"<oldpin>,<newpin>"`), wait until every PC has both, only then switch the
|
|
certificate on the server, then drop the old pin. Switching the server first
|
|
cuts off every pinned PC, including its ability to receive the fixed pin list
|
|
through anything that talks to ShopDB. A renewal that keeps the same key keeps
|
|
the same pin. PCs trusted through the Root store (below) also need the new
|
|
certificate imported before the switch.
|
|
|
|
### Trust the server certificate on PCs with agents older than 1.20
|
|
|
|
Required before ANY store wave. Agents older than 1.20 know nothing about
|
|
`ServerPins`: when they download the upgrade package they check the normal
|
|
Windows certificate chain. With a self-signed server certificate that check
|
|
fails and the upgrade never lands.
|
|
|
|
- PCs installed by the server's generated installer (Build site agent) already
|
|
trust the certificate.
|
|
- PCs installed from the share do NOT, and need this entry.
|
|
|
|
Get the certificate and its thumbprint from the server:
|
|
|
|
- Settings -> Endpoints -> Server Certificate -> download, or
|
|
`GET /api/endpoints/settings/server-cert/download` (a `.cer`).
|
|
- The thumbprint is `sha1thumbprint` on `GET /api/endpoints/settings/server-cert`.
|
|
|
|
Stage the `.cer` and `plugins/endpoints/client/Install-ShopDBServerCertificate.ps1`
|
|
on the share and add a PS1 entry with the SAME targeting as the pin entry:
|
|
|
|
```json
|
|
{
|
|
"_comment": "Trust the ShopDB server certificate for pre-1.20 agent upgrades.",
|
|
"Name": "ShopDB server certificate",
|
|
"Type": "PS1",
|
|
"Script": "scripts/Install-ShopDBServerCertificate.ps1",
|
|
"Args": "-CertPath <share>\\scripts\\shopdb-server.cer -Thumbprint <sha1thumbprint>",
|
|
"DetectionMethod": "Always",
|
|
"TargetMachineNumbers": ["<pilotbay>"]
|
|
}
|
|
```
|
|
|
|
The script refuses a file whose thumbprint differs, refuses a CA certificate
|
|
(one in Root would trust everything it signs), imports into
|
|
`LocalMachine\Root` only when the certificate is not already there, and removes
|
|
nothing. A non-CA leaf in Root trusts only this one server.
|
|
|
|
If a wave reaches a PC that does not trust the certificate, the download fails
|
|
and the Migration tab marks the PC **Blocked** with `server certificate not
|
|
trusted by this PC`. Fix trust with the entry above, then queue the PC again.
|
|
|
|
## Phase 2: re-stamp the share's agent entries
|
|
|
|
PCs that got the agent from the share (collections manifest entries with a
|
|
`Hash` detection on `shopdbagent.exe`) are held to that hash by GE-Enforce. If
|
|
a store upgrade wave puts 1.20 on such a PC while the entry still carries the
|
|
1.19 hash, GE-Enforce reinstalls 1.19 on its next cycle and the PC flips back.
|
|
|
|
Before any wave reaches a share-installed PC, either:
|
|
|
|
- re-stamp those entries to the 1.20 payload and its `DetectionValue` SHA-256
|
|
(and keep the `ProductCode`/version guards the entries already have), or
|
|
- remove the entries for the PCs the wave will cover.
|
|
|
|
Agent entries stay targeted. Illustrative shape for the pilot (keep your
|
|
existing entry's type, installer and guards; what matters is the hash and a
|
|
non-empty target):
|
|
|
|
```json
|
|
{
|
|
"Name": "ShopDB endpoint agent 1.20",
|
|
"Type": "PS1",
|
|
"Script": "apps/shopdbagent/1.20.0/install-shopdbagent.ps1",
|
|
"DetectionMethod": "Hash",
|
|
"DetectionPath": "C:\\Program Files\\ShopDB\\EndpointAgent\\shopdbagent.exe",
|
|
"DetectionValue": "<sha256 of the 1.20 shopdbagent.exe>",
|
|
"TargetHostnames": ["<pilothost>"]
|
|
}
|
|
```
|
|
|
|
**Gate:** the manifest preview for the pilot shows the 1.20 hash, and no entry
|
|
for the same file with a blank target list exists in any scope that reaches it.
|
|
|
|
## Phase 3: pilot one PC
|
|
|
|
1. Enrollment -> Migration: find `<pilothost>`. It should be **Ready**. If it is
|
|
**Blocked**, the Blockers column lists every reason; fix those first.
|
|
2. Tick it, **Upgrade selected**.
|
|
3. Watch the row: **Upgrading**, then **Enrolled** within a few minutes of the
|
|
new agent starting. The agent enrolls on its own with the shared key it
|
|
already holds.
|
|
|
|
**Gate before any wave:**
|
|
|
|
- Credentials tab shows the PC enrolled, `enrolledvia` = `auto`, and a
|
|
`lastusedat` that keeps moving.
|
|
- The PC's Fleet row keeps reporting; commands (Collect now) still work.
|
|
- The tray shows alerts and Send Status works (it now goes through the pipe).
|
|
- `report-asset-*.log` and `report-printers-*.log` show `RESPONSE (credential)`.
|
|
- Nothing reinstalled the old agent after two GE-Enforce cycles.
|
|
|
|
## Phase 4: waves
|
|
|
|
Migration tab -> **Upgrade next 25**. The confirm dialog states how many PCs get
|
|
agent 1.20. The server re-checks readiness per PC and packages the agent once
|
|
per wave; PCs that stopped being ready are returned as skipped.
|
|
|
|
Between waves, in the Waves list:
|
|
|
|
- wait until the wave shows **Done** (every PC enrolled or failed),
|
|
- investigate every **failed** PC before the next wave,
|
|
- check nothing moved from Enrolled back to Upgrading or Ready (a share entry
|
|
reinstalling the old agent, see phase 2).
|
|
|
|
Repeat until **Ready** is 0.
|
|
|
|
## Phase 5: stragglers and new PCs
|
|
|
|
- **Blocked:** each blocker names its fix. `offline`: bring the PC back or wait.
|
|
`no serial on record` / `duplicate hostname`: fix the computer record.
|
|
`commands locked`: release the endpoint lock or policy hold. `no agent package
|
|
in store`: upload 1.20. `agent too old for remote upgrade`: move it via the
|
|
share (targeted agent entry as in phase 2). `server certificate not trusted
|
|
by this PC`: add the PC to the certificate entry (phase 1), wait a cycle,
|
|
queue it again. `reenroll blocked`: the PC was rolled back on purpose; clear
|
|
it with allow-reenroll (see Rollback) only when moving it forward again.
|
|
- **Not reporting:** the PC has no working agent. It can only move via the
|
|
share channel: add its hostname to a targeted 1.20 agent entry. It shows as
|
|
Pending or Enrolled once the agent runs.
|
|
- **Pending:** the agent asked but auto-enroll could not decide (serial
|
|
mismatch, duplicate or unknown hostname, already enrolled, or auto-enroll
|
|
off). Credentials tab -> Requests -> **Approve**; the picker defaults to the
|
|
matched computer, search to choose another. The PC collects its credential on
|
|
its next retry (within 15 minutes). **Reject** anything you do not recognise.
|
|
- **Newly imaged PCs:** imaging installs 1.20 with the shared key, and the agent
|
|
auto-enrolls on first start. After the shared key is turned off (phase 6) a
|
|
new PC cannot auto-enroll: its request lands in Pending and an admin approves
|
|
it in the UI. That is the steady-state path for new PCs.
|
|
|
|
## Phase 6: cutover
|
|
|
|
**Gate:** Credentials tab **Not enrolled** is 0, or every remaining PC is one
|
|
you have decided may stop reporting. Remember that turning the shared key off
|
|
also refuses it on `/api/collector/computers` and `/api/collector/printers`, so
|
|
a PC with reporter scripts but no enrolled agent goes silent.
|
|
|
|
1. Turn **Shared key allowed** off. The confirm shows how many PCs are not
|
|
enrolled; type `OFF`.
|
|
2. Watch Fleet for an hour. A PC that went quiet is either unenrolled (turn the
|
|
shared key back on, fix it, try again) or failed pinning.
|
|
3. Remove the shared key from every place it lives (below).
|
|
4. Rotate the old key so any copy you missed is dead: Settings -> Endpoints ->
|
|
**Regenerate key** (and rotate the collector token the reporters used, if it
|
|
was a separate token). With the shared key off nothing on the floor needs
|
|
the new key: approved enroll requests are collected without it. Store it
|
|
the way the key page describes and keep it out of every file below. A
|
|
fleet rollback after this point (see Rollback) needs the new key pushed to
|
|
the PCs again.
|
|
|
|
### Every place the shared key lives
|
|
|
|
| Where | Put there by | How to clear at cutover |
|
|
|---|---|---|
|
|
| GE-Enforce manifest entry `Args` (`-ApiKey ...`) | whoever authored the entry | Remove the argument from the entry and publish. Search every scope for the key prefix. |
|
|
| `HKLM\SOFTWARE\GE\ShopDB` `CollectorKey`, both registry views | agent `install.ps1`; `Set-ShopdbCollectorKey.ps1` from `<share>\common\configs\shopdb-collector.txt` | First retire the `Set-ShopdbCollectorKey.ps1` entry (or it rewrites the value every cycle), then a targeted PS1 entry that deletes `CollectorKey` in both views. |
|
|
| `<share>\common\configs\shopdb-collector.txt` | share staging | Delete the file after the entry above is retired. |
|
|
| `appsettings.json` `apikey` in the agent install folder | agent package / hand edits | Targeted PS1 entry that sets `apikey` to an empty string. The agent never needs it once enrolled. |
|
|
| Generated `install.ps1` default `-CollectorKey` (Build site agent downloads) | Settings -> Endpoints -> Build site agent | Delete every kept copy (share, USB, work folders). Rebuild after rotation only if you still need the package. |
|
|
| Display kiosk `shopdb-key.txt` | display imaging | Confirm what the kiosk uses it for before clearing; a kiosk has no endpoint agent, so anything it reports with the shared key stops at cutover. Delete the file once that is settled. |
|
|
|
|
The agent never deletes the shared key itself: other SYSTEM scripts may still
|
|
read it until this table is done.
|
|
|
|
## Rollback
|
|
|
|
The order matters. A 1.20 agent that is plainly un-enrolled enrolls again
|
|
within about a second, before any downgrade can land, and the old agent it is
|
|
then replaced with only knows the shared key, which the server refuses for an
|
|
enrolled PC. Block re-enrollment FIRST.
|
|
|
|
**One PC:**
|
|
|
|
1. Make sure **Shared key allowed** is on.
|
|
2. Credentials tab -> **Un-enroll** the PC with **Keep this PC from enrolling
|
|
again** ticked. API equivalent:
|
|
`POST /api/endpoints/enrollment/assets/<assetid>/revoke`
|
|
`{"blockreenroll": true, "reason": "rollback"}`.
|
|
Its credential stops working immediately, the server admits the shared key
|
|
for it again, and its enroll attempts stay pending with `reenroll blocked`.
|
|
3. Make the previous agent current in the store (Upload New/Legacy Agents) and
|
|
queue the upgrade for that PC, or re-stamp its share entry to the old hash.
|
|
4. Verify: the Fleet row reports the old agent version, and the reporter logs
|
|
show `RESPONSE (shared key)`.
|
|
5. Remove the PC from the pin entry's targets only if pinning itself is the
|
|
problem (`Set-ShopDBServerPins.ps1 -Clear` removes the value).
|
|
6. Later, when moving the PC forward again, lift the block:
|
|
`POST /api/endpoints/enrollment/assets/<assetid>/allow-reenroll` (or the
|
|
same action in the UI). The PC enrolls on its next attempt once it runs
|
|
1.20. Until then the Migration tab shows it blocked with `reenroll blocked`.
|
|
|
|
**Whole fleet:** turn **Shared key allowed** back on first. Every PC that still
|
|
holds the shared key reports again immediately. Then revoke with the block and
|
|
downgrade PCs as above, in waves.
|
|
|
|
## Verified on Windows
|
|
|
|
A full pass on a Windows test PC against a test server proved:
|
|
|
|
- the agent enrolls on its own;
|
|
- the shared key is refused for the enrolled PC (`SHARED_KEY_REFUSED`);
|
|
- a request naming another asset is refused (`ASSET_MISMATCH`);
|
|
- server pins are enforced;
|
|
- the tray pipe works for a standard (non-admin) user;
|
|
- alert acknowledge works;
|
|
- both SYSTEM reporters send with the per-PC credential;
|
|
- revoke is followed by re-enrollment;
|
|
- rollback to the previous agent works;
|
|
- with the shared key switched off, enrolled PCs keep working.
|
|
|
|
## Quick reference
|
|
|
|
- Pin of the live server: Server Certificate settings page, or
|
|
`Get-ShopDBServerPin.ps1 -Url https://<server>`.
|
|
- Pin of a certificate file: `Get-ShopDBServerPin.ps1 -Path <file>`.
|
|
- Agent log of enrollment: `%ProgramData%\ShopDB\EndpointAgent\logs`.
|
|
- Reporter logs: `C:\Logs\Shopfloor\report-asset-*.log`,
|
|
`report-printers-*.log`. `SHARED_KEY_REFUSED` there means the PC is enrolled
|
|
or the shared key is off and the reporter could not read the credential;
|
|
`CREDENTIAL_INVALID` means the PC was un-enrolled and the agent is
|
|
re-enrolling.
|