Appearance
Updates, claim by claim
WIN-DLP-EPIC-024 asks for signed, staged, resumable, reversible updates, because an update channel is a remote code execution channel with a friendly name.
The updater exists, and its Windows half is unproven. The agent receives an update as a queued command (DLP-369), downloads it signed and bounded to a .partial name, verifies size, digest and signature against the keys it was enrolled with, and only then renames it and runs msiexec. Everything up to msiexec is proved with a real agent (tests/agent_update.sh, DLP-1108). The first joined run found that the command carried no release (DLP-1109) and that the artefact was a console-only route (DLP-1110), so until 2026-09-27 no update could have reached an endpoint.
msiexecon a real Windows host is not proved (DLP-447,DLP-448).- No MSI is published yet (see §3).
- The watchdog's rollback has never run on Windows. The MSI now admits it and still refuses any other downgrade, but nothing has built that MSI and rolled an endpoint back with it (
DLP-1121,DLP-1122).
This paragraph used to say there was no updater. It was written before DLP-369 and stayed true in no sense after it.
1. What the server does
GET /api/v1/agent-packages lists each platform's package with:
| Field | What it is for |
|---|---|
sha256 | Detecting a corrupted transfer |
signature | Detecting a substituted one - a detached Ed25519 signature over the digest, made with the tenant's active signing key |
version | Read from a sidecar written when the artefact was published, not the running server's version |
version_matches_server | Whether the artefact is the server's version; routinely false since DLP-632, when a release that changes no agent code stopped rebuilding the agents |
is_current_release | Whether this is the newest published agent. False is the state that needs explaining: a publish left this package behind its siblings |
authenticode_signed | Always false, said out loud rather than omitted |
Two of those are the product being careful in ways worth copying.
The version is the artefact's, not the server's. It used to be state.version(), which meant a package published two releases ago reported today's version - a number that looked authoritative and was wrong. Now an unknown artefact reports no version, because "I do not know what this is" is a true statement and a useful one.
The signature is over the digest, not the file: cheap to produce, and the verifier checks the same digest it already downloaded.
2. The finding
The console's own words, beside the download:
Verifying the signature - not just the hash - is what makes the artefact trustworthy independently of this server: a compromised server can report a hash matching whatever it served, but cannot forge a signature without the key.
That is correct and it is the right thing to tell somebody. Nothing in the product lets them do it. There is no openssl recipe, no documented procedure in , and no dlp-agent verify-package. The signature appears as a badge with the key id in a tooltip.
And the sharper half: verifying independently of the server requires the public key from somewhere that is not the server. The listing returns public_key_b64 in the signature block - from the same server the advice is about. Somebody who could serve a forged package could serve the matching key beside it.
That does not make today's state useless. The key is the tenant's own bundle-signing key, so an administrator who already holds it out of band can verify properly, by hand, if they know how. It does mean the page promises more than it delivers.
Filed as DLP-273. The useful shape is a verify command in the agent, defaulting to the key the endpoint already trusts - because verify this against the key I already trust is a much stronger statement than verify this against the key printed next to it.
3. Signed, staged, resumable, reversible
| Property | State |
|---|---|
| Signed | Yes. The endpoint verifies every update against the keys it was enrolled with before anything runs, and the server signs with that key (DLP-1104). Proved with a real agent, including a refused altered file (DLP-1108) |
| Staged | Yes. Downloaded to a .partial name and renamed only after verification (SPIKE-009, DLP-369) |
| Resumable | No. A failed transfer is discarded and fetched whole at a later check-in; nothing resumes a partial download |
| Reversible | Built, not proved. The watchdog decides when to roll back (DLP-555), passes MYDLP_ROLLBACK=1 to the older MSI, and reports what happened (DLP-1167). The MSI admits that downgrade and refuses every other one (DLP-1121, below). No rollback has run on Windows: DLP-1122 is the run that would prove it, and tests/windows_msi_installer.ps1 carries its steps |
The design is decided (SPIKE-009). In one sentence: the update manifest is a policy bundle in everything but name - same Ed25519 envelope, same trusted keys from enrolment, same anti-rollback floor - the artefact is staged to quarantine and renamed into a runnable path only after its signature verifies, rings are agent groups rather than percentages so an offline endpoint still knows what it may run, and the service wrapper rolls back when a new version cannot complete a signed check-in within ten minutes. msiexec on the MSI does the install and the upgrade; the updater is not a second installer. Two things this sentence used to claim are not true yet:
- No MSI is published by the server's deploy. The Windows CI job is written to build it and has not yet passed on
windows-latest(Windows CI status); an operator placesdlp-agent-windows-x86_64.msiwith its.versionsidecar in the package directory. Until then, a Windows update is refused with that reason (DLP-1166). - The rollback has not run. The MSI's rows that admit it are written and checked as far as a Linux host can check them (
DLP-1121); nobody has rolled an endpoint back with them (DLP-1122).
Whatever a rollback does, the server now hears about it (DLP-1167). The watchdog writes updates/rollback-report.json, and the next version to complete a check-in reports it once, as failed for the version rolled back from. The sentence says whether the endpoint is back on the previous version or still on the failed one. Before this, the outcome went to the local log only, and the endpoint was silent either way.
Seven implementation tickets are listed in §10 of that document.
The service does not wait for msiexec (DLP-1287)
The package's ServiceControl row stops the agent's service and waits for it to stop. The service used to start msiexec and wait for it to exit, from inside the loop a stop has to end, so each waited for the other until Windows Installer gave up on the service. In quiet mode that is a failed, rolled-back update. That was read from the code; it has not been seen on a host.
So an MSI update and the watchdog's MSI rollback are handed off, as an in-place update already was (DLP-1253):
- The pending record is written first (
updater::pending_before_msi). msiexecis started outside the service's job, in its own process group, and not waited on. Its verbose log goes beside the package.- The new version, once started, is armed and watched as before, and reports the update when it settles.
- If the old version is still running at its next tick, it reads the installer's exit code from that log (
updater::msi_log_ending) and reports it once (updater::msi_install). A 3010 means installed, waiting for the restart, as before. A failure keeps the package for the retry. Ten minutes with no ending is a failure.
Proved: these decisions by unit test, and, under Wine, that the start returns before a child that takes three seconds (runner::detached). Making it wait fails that test. Not proved: an MSI update or rollback on a Windows host, which is the rest of DLP-1287's acceptance and needs DLP-1122's real runner.
DLP-1175 is Authenticode signing, and is a different trust chain for a different audience: SmartScreen and enterprise application control, rather than an administrator at a terminal. Its protected HSM release stage is implemented in , but it is not in force until the code-signing certificate that ADR-0010 decided to buy produces a verified release.
Rolling back past the MSI's downgrade rule (DLP-1121)
The MSI declares MajorUpgrade with a DowngradeErrorMessage, so msiexec /i on an older MSI fails, and tests/windows_msi_installer.ps1 asserts exactly that. The watchdog's rollback is such an install.
Decided: the older MSI removes the newer product as an upgrade does, and only when the install says it is the watchdog's rollback:
- The agent passes
MYDLP_ROLLBACK=1on the rollback and on nothing else (updater::rollback_msiexec_arguments, unit-tested). This part is built. - The MSI replaces
MajorUpgradewith explicitUpgraderows:- one detecting older versions, as today;
- one detecting newer versions into
WIX_DOWNGRADE_DETECTED, which is not detect-only, soRemoveExistingProductsremoves the newer product during the rollback.
- A launch condition refuses
WIX_DOWNGRADE_DETECTED AND NOT MYDLP_ROLLBACK="1", with today's downgrade message, so a downgrade somebody types is still refused. - The newer product is removed with
UPGRADINGPRODUCTCODEset, so itsRemoveAgentStatedoes not run and the endpoint keeps its identity, as in an upgrade. BootstrapAgentand the fresh-install launch condition treat a downgrade like an upgrade: no enrolment token is needed.
Decided against:
- Uninstall, then install the previous MSI. A plain uninstall runs
RemoveAgentStateand wipes the endpoint's identity, so a rollback would un-enrol the machine it is trying to save. It would also leave a window with no agent at all. AllowDowngrades="yes". It would let anybody take an endpoint back a version silently. The refusal is a control, and the rollback is the one exception the product means.
Built, and not run on Windows. Package.wxs writes out the rows MajorUpgrade generated, with the two changes above. They were checked in three ways:
- WiX 4.0.6 compiles and links the package (
tools/wix_link_check.py,DLP-1214). - The linked intermediate was compared row by row with what
MajorUpgradeproduced. The older-version row is unchanged. The newer-version row differs only in no longer being detect-only.RemoveExistingProductsis still scheduled afterInstallInitialize. tests/windows_msi_installer.ps1pins each decision statically, and a perturbation of each fails it.
Its integration half now does what DLP-1122 asks:
- an ordinary downgrade is refused;
- the watchdog's
MYDLP_ROLLBACK=1downgrade succeeds, keeping the identity; - an upgrade afterwards succeeds.
Nobody has read that run. The binder needs Windows' msi.dll, and the Windows job's result cannot be read here (DLP-448).
An install the setup program made updates itself (DLP-1253)
No endpoint had ever updated. The server publishes no MSI, and endpoints installed by MyDLP-Agent-Setup.exe are not MSI-managed anyway: an MSI over one collides with the service dlp-agent install created. The first physical host (DLP-1176) was one of those, and the owner was asked to download the agent again for every fix.
So a release has a kind, msi or executable, and an endpoint takes only its own:
- How the endpoint knows. The setup program and
dlp-agent installwriteHKLM\…\Uninstall\MyDLPAgent; an MSI install is listed under its product code instead. The key present meansexecutable, absent meansmsi, and a registry that cannot be read advertises nothing, which the server reads asmsi, what every agent applied before. - What it tells the server.
capabilities.update_kinds, so anexecutablerelease is queued only to an endpoint that lists it. An older agent lists nothing, so it is never handed an executable to feed to Windows Installer. - How it applies one. It downloads and verifies the executable exactly as an MSI: bounded, then digest, then signature against the enrolled keys. It keeps the binary running now in staging as the rollback target and writes the pending record. Then it starts
dlp-agent-<v>.exe install --server <url> --progressdetached, with no window, output toupdates/install-<v>.log. That command already upgrades in place: it stops the service, replaces the binary (renaming a locked one), keeps the configuration and identity, and starts the service again. It is never given--forceor a token. - How the outcome is learned. It cannot be waited on, because it stops the service that started it.
- The new version arms the watchdog when it starts. After two check-ins it reports
applied. - The old version still running means the swap did not happen. It reports
failedwith the install's own last line, or after the watchdog's ten minutes with no line, and clears the record so the next release can come.
- The new version arms the watchdog when it starts. After two check-ins it reports
- A new version that will not start. The install stops the service that started it, so if the service will not come back nothing else is running to notice. The service therefore passes the retained binary as
--restore-on-failure. When registering or starting the service fails after the swap, the install retries the start twice, then puts the previous binary back and starts it. Its last line says which happened, and the restored version reports it. A review found this gap before any endpoint ran the path. - Rollback. Silence from the new version runs the retained executable's
installthe same way. The rollback report claims success only if the version checking in is the one rolled back to.
Not replaced by this path: dlp-session.exe, the sign-in launcher. It is a fixed shim that starts dlp-agent.exe session, and the install re-registers the one already in place. A launcher change needs a reinstall.
Not yet proved: any of it on a Windows host. The first run is DESKTOP-MNVLKOM going from the version the owner installs to the next release unattended.
4. Why the channel was built last
This section was written while there was no updater. It is kept because the order it defends is the one the project followed.
The epic's own sentence is the argument: an update channel is a remote code execution channel with a friendly name. An agent that can replace its own binary on instruction from a server is, by construction, an agent that executes whatever that server sends. Building that before the signing story is settled - before there is a certificate, a key custody decision, and a verification path an administrator can follow - would be building the channel before the lock.
The order this project has taken is: publish packages, sign the digest, decide key custody (DLP-64, cloud HSM), then build the consumer. That is the right order, and this document exists partly so nobody reverses it to save a sprint.
5. What is not covered
- No test of an update applied by the agent on Windows.
DLP-1122,DLP-448. The chain up tomsiexecis tested on Linux (DLP-1108), and the MSI upgrade path is tested by the installer suite, but nothing has had an agent install its own update on a Windows host. - The rollback cannot install yet. The watchdog triggers and reports (
DLP-555,DLP-1167), and the MSI refuses the downgrade untilDLP-1121's installer half lands. authenticode_signedremains honestly false until the protected release actually signs the package. A Windows administrator today sees a SmartScreen warning, and an enterprise policy requiring signed executables refuses it outright. The existence of the signing workflow does not change package metadata retroactively.