Appearance
Windows agent - security model
What the agent protects, with what, and where the protection stops.
1. Identity
| Property | How |
|---|---|
| Key type | ECDSA P-256 in TPM 2.0 where the Microsoft Platform Crypto Provider is available; otherwise Ed25519. Generated on the endpoint; the private key is never transmitted |
| Generation | At enrolment, from the OS CSPRNG |
| Storage | Preferred: non-exportable machine key in the Microsoft Platform Crypto Provider. Fallback: DPAPI machine scope (CryptProtectData with CRYPTPROTECT_LOCAL_MACHINE) under the state-directory ACL |
| Proof before commit | A TPM key's provider hardware flag, export policy, public point and a self-test signature are checked. DPAPI fallback encrypts, decrypts and compares on that machine before plaintext is discarded |
| Reported | Enrollment persists and exposes tpm_backed, dpapi_machine, restricted_file or keystore_unavailable; the signing algorithm is pinned beside it. A software fallback never claims TPM protection |
| Rotation | Server-issued credential with an expiry; the agent renews before it |
| Revocation | Server-side; a revoked device is refused at check-in |
An identity created before TPM support remains on its existing Ed25519/DPAPI key. Moving it to TPM requires explicit force re-enrollment, so an upgrade never silently abandons the device identity. A restored image that contains the TPM reference but not the TPM object fails closed instead of inventing a software identity. The residual local-administrator and hardware-evidence limits are in T-W03.
2. Enrolment
- An administrator generates a one-time token in the console.
- The installer or
dlp-agent.exe enroll --tokenuses it exactly once. - The agent generates its key pair and sends the public key with the machine identifiers.
- The server consumes the token, assigns tenant, endpoint and agent ids, and issues a scoped credential.
- The token is not persisted - not in the config, not in the state directory, not in the log.
Replay is refused server-side: the token is consumed, and enrolment is rate-limited and audited. Request freshness is carried by a request id and a timestamp on every signed request (packages/crypto/src/request_auth.rs), which also bounds clock drift.
3. Policy bundles
Verified before a bundle is allowed to influence anything:
signature · signing key · tenant id · endpoint or group scope · schema version · policy version · issued time · expiry · minimum agent version · anti-rollback counter · integrity hash.
Activation is atomic. A bundle that fails any check is rejected as a reported validation error and the last known good bundle stays in force. There is no path that partially activates a bundle.
4. Local state
%PROGRAMDATA%\MyDLP\ holds the wrapped key, the verified bundle, the event queue and the local history. Between them they describe what was found on this machine and would let anything that can read them impersonate the device.
- ACL set at creation: the service account and administrators, nothing else. Since
DLP-1671the call also makes Administrators the owner, because an owner can always rewrite an ACL whatever it says, and adds an OWNER RIGHTS entry with no rights. This is cross-compiled, not yet observed on a host. - ACL audited at every start, and the owner with it: an owner other than SYSTEM or Administrators is reported as unsafe. An ACL set once and then changed - by a misapplied group policy, by a restore from backup - would otherwise be a protection the agent still claims to have. A weakened ACL is impaired health.
- The decision is this grant set acceptable is a pure function over a list of grants, so it is unit-tested on any platform; only reading the grants is Windows-specific.
5. Inter-process communication
The service and the session process talk over a named pipe.
| Control | Detail |
|---|---|
| Creation | FILE_FLAG_FIRST_PIPE_INSTANCE, explicit security descriptor |
| Direction of trust | The session process asks; the service decides. The service performs no action on the session process's say-so that it would not perform on its own |
| Caller | The connecting SID is read and checked |
| Message | Schema-validated, size-capped |
| Failure | A refused connection is logged with the SID and the reason |
6. Event integrity in transit
Every batch is signed with the device key. The server verifies the signature, the tenant binding, the sequence number and the batch id. Duplicate delivery is idempotent; a gap in sequence numbers is itself reported (tests/sequence_gaps.sh).
7. Local queue
agent/secure-buffer is a SQLite queue with a bounded size, an age cap, lease semantics for in-flight batches, integrity checks and crash recovery. It never holds a file body or a raw match - only the redacted event.
Two things it is not, stated because the crate's name invites both assumptions:
- Not encrypted. Its confidentiality is the state directory's ACL (§4), audited at every start, and the fact that it holds redacted events rather than content. An attacker who can read the directory learns which files matched what, on which paths, under which user name - which is why §4 treats read access to that directory as a compromise.
- Not covered by the server's retention rules. The retention job governs what the server keeps; an event that never arrived is governed by nothing, and a subject-access request cannot reach a queue on a laptop in a drawer. That is what the age cap is for, and it is the reason the cap ignores severity - see below.
The age cap is six months (DLP-272). This section claimed an age cap before one existed, which DLP-134 found; the claim is now true. Two mechanisms, for two different problems:
- Overflow eviction drops the oldest, lowest-severity events when the row cap is reached. Severity outranks age here, because under sustained disconnection an operator would rather keep one critical credential leak than a thousand informational file-write notices.
- The age cap drops anything still undelivered after 180 days, whatever its severity. A cap that spared the severe events would leave exactly the records with the most detail in them sitting outside every retention rule the product has - which is the case the cap exists for, not an exception to it.
The number is generous on purpose. An endpoint is offline exactly when exfiltration is most likely, and an event dropped for being old is evidence destroyed by the tool that collected it, so the cap is set where "this machine has not been switched on for half a year" is a truer description than "this is an investigation in progress".
Both drops are counted, separately, and both reach the health report: queue_dropped_total says this endpoint produces events faster than it can deliver them, queue_expired_total says it has not been able to deliver them for half a year. Neither is silent - the module's rule is that silently discarding evidence is worse than admitting to it.
8. What the agent does not do
- No TLS interception. Not in the endpoint, not optionally, not behind a flag. Terminating the user's TLS on the endpoint is a larger risk than the one it would address - a machine-in-the-middle on every banking session on that laptop, installed by the security team. SPIKE-012 prices what the rule costs rather than restating it: the endpoint cannot see what is inside an HTTPS upload from the network layer, which is why the browser extension exists and why anything that is not a managed browser is visible as a connection and never as content.
- No credential collection of any kind.
- No process injection, no API hooking, no driver, no boot-start component.
- No hiding. The service is visible in the service list, the process in Task Manager, the files on disk, and the uninstall path is documented.
9. Supply chain
| Control | State |
|---|---|
| Dependency advisories | cargo deny and cargo audit, in CI |
| Licence policy | cargo deny plus tools/licence_policy.py, in CI |
| SBOM | CycloneDX and SPDX generated in CI (sbom/) |
| Secret scanning | gitleaks, in CI |
| Fuzzing | Weekly campaign over the extraction and bundle paths |
| Reproducibility | Not established |
| Authenticode signing | Implemented as a protected release stage, not yet in force. No production certificate or successful signing run exists. , DLP-1175 |
| Signed update manifests | Implemented (DLP-920): the server composes the manifest and signs it with the tenant's policy-bundle key, and the endpoint verifies a release against the keys it was enrolled with before staging it (DLP-369). Designed in SPIKE-009. Applying it on a real Windows host is not proved (DLP-447) |
The last three rows are the honest state of the Windows supply chain. An MSI that is not signed is an MSI that SmartScreen will warn about and that an administrator has no way to verify - which is why DLP-150 treats signing as part of the installer's definition of done rather than as a later nicety.
10. Recovery and removal
An authorised administrator always has a way out, and it is documented rather than discovered:
sc.exe stop MyDLPAgent
dlp-agent.exe uninstall --confirm --authorised-by "CHG-1234"--authorised-by is required. Without it the server records nothing, the endpoint simply goes quiet, and the console reports an unexplained silence - which is correct, because an uninstall nobody authorised is exactly what tampering looks like from the outside.
There is no anti-uninstall mechanism, no re-installing watchdog and no tamper-protection driver. An administrator who wants the agent gone can have it gone, and the record of that is the control.