Pin the approved content digest on an endpoint command
Delivery descriptors for run_script and update_app are resolved when the agent claims a command, not when an operator queues it, and the command row recorded no digest: the enqueue path resolved the descriptor purely to validate it and threw the result away. So what an operator approved and what an endpoint runs were two separate lookups against mutable content, minutes or hours apart. Anyone able to replace a script body or an installer package could do so in the gap. The claim then re-resolves, the descriptor faithfully describes the new bytes, the agent verifies correctly, and it runs as LocalSystem. The audit trail names the operator who queued the original. The gate on the verb and the gate on the payload were also different grants: pushing needs endpoints.deploy, but replacing what gets pushed needs only applications.edit. pinnedsha256 records the digest as it stood at enqueue, and the claim refuses a descriptor that has moved, failing the push with an audit row instead of shipping it. Null for actions that deliver no content, and for rows queued before this migration, which claim as before. A descriptor that vanished entirely is not treated as tampering: that path already sends the command through undescribed so the agent fails closed. Separately, a result may now only finalize a command that is actually running. That needs no agent change and closes two things: reporting on a command that was never claimed, and overwriting a recorded failure with a success. The agent now sends its hostname on the result callback so the server can check it owns the command it is finalizing. The shared collector key does not say who is calling, so without it any agent can post a result for any command id. The server treats the parameter as optional for agents that predate this and enforces it when present, so there is no flag day. This is a floor, not a fix: only a per-endpoint credential makes the caller's identity provable, and that is tracked separately. The .NET side is unbuilt here and needs a build plus a win11 VM run. Scope: product
This commit is contained in:
15
CHANGELOG.md
15
CHANGELOG.md
@@ -167,6 +167,21 @@ ADR-007 and ADR-002.
|
||||
set `TRUSTED_PROXY_COUNT`** or its whole floor shares one rate-limit bucket;
|
||||
INSTALL-WINDOWS-IIS.md now says so.
|
||||
|
||||
- **What an operator approved and what an endpoint ran were two different
|
||||
lookups.** Delivery descriptors for `run_script` and `update_app` are resolved
|
||||
when the agent claims a command, not when the command is queued, and the
|
||||
command row recorded no digest - the enqueue path resolved the descriptor
|
||||
purely to validate it and threw the result away. Anyone able to replace a
|
||||
script body or an installer package could do so in the gap, and the
|
||||
re-resolved descriptor would then faithfully describe the new bytes: the agent
|
||||
verifies correctly and runs something nobody approved, with the audit trail
|
||||
naming the operator who queued the original. The approved digest is now pinned
|
||||
on the command at enqueue and the claim refuses a descriptor that no longer
|
||||
matches, failing the push instead of shipping it. Separately, an agent may
|
||||
only finalize a command that is actually running, so a result cannot be posted
|
||||
for a command that was never claimed nor overwrite a recorded failure with a
|
||||
success.
|
||||
|
||||
- **The published site's API documentation page had no spec to show.**
|
||||
`shopdb/core/api/docs.py` reads `docs/openapi.json` at runtime to serve
|
||||
`/api/docs`, and publication excluded the whole of `docs/` because the prose
|
||||
|
||||
Reference in New Issue
Block a user