Skip to content

Threat model ​

Scope: the MyDLP agent, management API, dashboard and the channels between them. Out of scope: the customer's operating systems, network and identity provider, except where we depend on them.

Method: STRIDE per trust boundary, with each mitigation named as implemented, partial or not implemented. A mitigation marked "implemented" has a test.


1. Assets ​

AssetWhy it matters
Matched sensitive valuesThe data the product exists to protect. Never leaves the endpoint in an event, a log or a preview. A sealed copy of the whole file leaves only where an organisation switched full-file evidence on and a policy asks for it (ADR-0038, PRIVACY_MODEL §39)
Policy bundlesDetermine what is detected and what happens. Forgery or rollback disables protection
Device private keysImpersonating a device lets an attacker inject false events or suppress real ones
Session tokens and API tokensConsole access
The audit logThe record of who did what. Its value is entirely in being unalterable
Incident evidenceRedacted, but still describes where sensitive data is
EDM fingerprintsKeyed hashes of an authorised dataset

2. Actors ​

ActorCapability assumed
Endpoint userLocal user, sometimes local administrator. Can read and write files, inspect processes, edit files the agent can write
Malicious insiderA legitimate console user acting beyond their remit
Network attackerCan observe and modify traffic between agent and server; cannot break TLS
Compromised endpointFull local control of one machine
Compromised serverFull control of the management API and database

3. Endpoint user → agent ​

3.1 Tampering with the cached policy bundle ​

Threat. A local administrator edits the cached bundle to remove policies. Mitigation - implemented. The bundle is re-verified on every load, not trusted because we wrote it. Signature, tenant, device scope, group scope, expiry and a monotonic version floor are all checked. A tampered cache is rejected and the agent runs with no policy rather than an attacker's policy. Tests: policy_runtime::tests::a_tampered_cache_file_is_not_loaded, and the end-to-end script asserts it against a real edited file. An emptied, truncated or unparseable cache is treated the same way since DLP-1614: the agent runs with no policy and fetches the current bundle, rather than exiting, which it did on YUSIF for a 0-byte file. The service no longer stops over a cache. The cache is written whole, synced and renamed, so a crash leaves the old bundle or the new one (policy_runtime::tests::an_empty_truncated_or_garbage_cache_starts_with_no_policy_and_never_stops_the_agent).

3.2 Downgrading to an older bundle ​

Threat. Replaying a genuinely signed older bundle to reinstate a permissive policy. The signature is valid, so signature checking alone does not help. Mitigation - implemented. highest_bundle_version is persisted in the device identity and raised on every acceptance. A bundle below it is refused. Test: a_downgraded_bundle_is_refused_and_the_current_one_survives.

3.3 Stealing the device private key ​

Threat. A local administrator reads identity.json and impersonates the device. Mitigation - platform-specific. On Windows, a TPM 2.0 device key is a non-exportable P-256 object in the Microsoft Platform Crypto Provider. Where that is unavailable, the Ed25519 fallback is sealed with machine-scope DPAPI. Linux and macOS retain the 0600 service-account file. A local administrator can still invoke signing on the original Windows machine or read the Unix file; server-side revocation remains the recovery control.

3.4 Stopping the agent ​

Threat. Killing the service to avoid detection. Mitigation - partial. Restart=always; absence shows as a stale endpoint. An operator must actually look. Tamper protection is DLP-SEC-014.

3.4a Reinstalling to undo a revocation ​

Threat. An endpoint is revoked - lost, stolen, sold, or belonging to somebody who has left. Whoever still holds it reinstalls the agent to get it trusted again.

Mitigation. Enrolment refuses a device whose row is revoked, and the refusal is audited (agent.enrolment_refused_revoked), so an administrator learns that somebody tried. Coming back is a deliberate administrative act - POST /api/v1/agents/{id}/readmit - which is audited with who allowed it, and which keeps revoked_at rather than pretending the revocation never happened.

This was not true until DLP-486. The enrolment upsert set status = 'active', revoked_at = NULL on every conflict, so revocation held exactly until somebody ran the installer again, and the row afterwards carried no evidence it had ever been revoked. The same line silently re-activated a deliberately deactivated endpoint. Recorded here rather than quietly fixed, because anybody who reasoned about revocation before that commit was reasoning about a control that did not hold.

What it still does not cover. Revocation is keyed on the hardware id. A machine that is rebuilt from bare metal, or whose identifiers are deliberately changed, presents a different id and enrols as a new device. That is the boundary of what any agent-side identity can do, and the answer to a genuinely hostile holder is the enrolment token's own lifetime and scope, not the device record.

3.5 Learning what is detected ​

Threat. Reading the bundle to discover detection rules. Accepted. Detection rules are not secrets - most are published formats. Secrecy would be a thin defence against a determined local attacker. What matters is that rules cannot be changed, which §3.1 and §3.2 cover.

3.6 The session channel: a trust boundary inside one machine ​

The agent runs as a service with no session; the session agent runs as the person, in theirs. Between them is a local channel - a unix socket under the user's runtime directory, a named pipe on Windows - and it carries two things worth attacking: whatever was last copied to the clipboard, and an answer the server will record as consent.

The process on the far side is not trusted. It runs as an ordinary user who can attach a debugger to it, so everything arriving is treated as a claim.

ThreatMitigation
Another local user reads somebody's clipboard, or answers a prompt meant for themThe socket is 0600 and the pipe carries a DACL, so the operating system refuses the connection. Enforced by the transport, not by a token in a message - a token is something to steal, a mode is not
Fabricating consent: a local process records a justification nobody gaveAn answer is only routed to a question the agent actually asked, by id, and an id nobody is waiting for is dropped. A compromised session agent can answer what it was asked; it cannot conjure a question. The reason text is then re-checked against the same rules at the agent's end, because the far end's validation is a suggestion
Suppressing a prompt: kill the session agent and require_justification quietly becomes auditSilence is never consent and never an answer. Nobody attached is CannotAsk; attached and then killed with a question open is also CannotAsk, because the pending answer is failed rather than left to time out. Only a live peer that stays silent to the deadline is NoAnswer, and the two are recorded as different strings so an analyst can tell them apart
Exhausting the agent's memory from an unprivileged processEvery message is bounded before it is parsed, at 1 MiB; an over-long one ends the connection rather than being resynchronised
Two session agents, or a browser host pretending to be oneOne session agent at a time; a second is refused. A peer that said Hello and then sends a browser message has its connection dropped rather than being served both ways
Any program in the session speaks as the session agent (Windows, DLP-1886)The pipe's DACL admits every interactive user, so the service also asks Windows for the image of the process on the far end and refuses one that is not the installed dlp-agent.exe, or the copy an in-place update moved aside beside it, before a welcome. Each refusal is counted as not_installed_agent and shown on the endpoint's page. This is a check of a path in a directory only administrators can write, not of a signature: a program that injects into the real session agent still speaks as it, which is the debugger case below
A person's identity taken from what they claimThe identity on an event is the kernel's answer for the peer, never the claimed_user in the message. Unix peer-credential lookup failure and a non-empty claim that contradicts it both refuse the connection before a welcome; an unresolvable uid remains a uid:<n> identity

What this does not cover. A user with administrative rights on their own machine can stop the agent (§3.4) or run a debugger against it, and no local IPC design changes that. The boundary is between ordinary local users, and between a session agent and the service - not between a machine's administrator and the software on it.

3.7 A fake prompt, drawn by something already in the session ​

Everything in §3.6 protects the channel. None of it protects the person looking at the screen, who never sees the channel. An attacker already running code in somebody's session can draw a pixel-perfect copy of the justification dialog, and Windows offers no mechanism by which an application can prove to a user that a window is really its own. The Secure Desktop is the one thing that would work and it is reserved for the system.

This is an accepted risk, not a solved one.DECISIONS/0006 is the reasoning: the product ships no trust indicator, because an indicator an attacker can reproduce teaches people to trust the thing that can be forged, and one nobody checks is the same with extra steps.

ThreatMitigation
A fake prompt harvests whatever is typed into itBounded rather than prevented. The real dialog asks for a business justification and never a credential, so the most a convincing fake can collect is a sentence about somebody's work. Enforced by agent/core/tests/the_prompt_never_asks_for_a_secret.rs, which fails on a password field and on any prompt string in any shipped locale that asks for one
A fake prompt convinces somebody an operation was blocked when it was not, or the reverseNot prevented at the moment of the prompt. What the agent actually did is in dlp-agent history and on the dlp-agent ui page, both read from the agent's own protected state - a surface the attacker does not control. Somebody suspicious of a prompt can check what really happened afterwards
A future feature treats "the user answered" as proof of identity or intentRuled out by DECISIONS/0006 rather than by code. An answer is evidence that an answer was given on this endpoint, and nothing more

Authenticode (DLP-1175) is worth doing for SmartScreen, tamper evidence and the supply chain, and it does not solve this: a signature is a property of a process, and a person cannot inspect the process behind a window they are looking at.

3.7a Changing what the real dialog says ​

Threat. Somebody who can write a file the session UI reads swaps the logo or the words, so the agent's own dialog, tray card or toast says something it should not under the real cyberkap mark.

Mitigation - implemented (DLP-1887). The UI reads no such file. The mark is resource 1 of the executable, compiled from assets/cyberkap.ico by build.rs. The wordmark is baked into the code with include_bytes!. Every string is compiled in (localisation.rs, card.rs). The toast carries no image. The tray card's facts come from the summary the service writes into a directory only it can write (about.rs, state_acl::secure_readable). Asserted by agent/core/tests/ui_resources_are_compiled_in.rs. It reads every module that draws and fails on any file load, LR_LOADFROMFILE, image path or user-writable location, beside a positive check that the built-in loaders are there.

What it leaves. The binary itself is the trust anchor. It lives in Program Files, which only an administrator can write. Authenticode (DLP-1175, not yet bought) is what will let anybody verify it. An administrator can already replace the binary, which is §3.6's boundary, not a new one.


4. Agent ↔ server ​

4.1 Spoofing a device ​

Mitigation - implemented. Every request carries a signature over method, path, SHA-256 of the body, device id, timestamp and nonce. The server verifies with the public key and algorithm pinned at enrollment (Ed25519, or ECDSA P-256 for a TPM-backed Windows key); the request cannot select its verifier. Changing any component invalidates the signature, and malformed key/signature shapes or unknown algorithms are refused.

4.2 Replay ​

Mitigation - implemented. Two checks, both required. The timestamp must fall within a 300-second window, which bounds how long a replay cache must live. The nonce is inserted into request_nonces with a primary key on (device_id, nonce), so a second use conflicts and is rejected. Nonces are garbage-collected past the window.

4.3 Reusing an enrollment token ​

Mitigation - implemented. The token is consumed by an atomic UPDATE … WHERE uses_remaining > 0 … RETURNING, so two agents racing on one token cannot both enrol. Expired, revoked, exhausted and unknown tokens are indistinguishable in the response. Asserted in the end-to-end script.

4.4 Injecting false events ​

Threat. A compromised endpoint floods the server with fabricated incidents. Mitigation - partial. Events are bound to a device identity and rejected if tenant_id or device_id disagree with the authenticated device. Per-device rate limiting is not implemented (DLP-SEC-016); nginx applies a coarse per-IP limit today.

4.5 Suppressing events ​

Threat. A compromised endpoint simply sends nothing. Mitigation - implemented, with a limit worth naming. Events carry a strictly increasing per-device sequence and the server tracks the high-water mark, so a hole in the numbering is detected on the batch that skips it, recorded with the range that is missing, audited as agent.sequence_gap and surfaced on Administration → System health (#351). A gap that later fills closes itself, so what remains open has stayed missing.

Sending nothing at all is caught by the other half: an endpoint that stops checking in is listed after fifteen minutes and alerts after thirty by default, each a tenant setting (DLP-2177), and that is distinguished from an authorised uninstall or a revocation, which are recorded and never alert (#347).

The limit. An attacker who silences the agent and lets it keep checking in cannot be caught this way: the sequence simply stops advancing, and a device that generates no events looks identical to one whose events are being discarded. Detecting that needs a heartbeat the agent signs over its own event count, which is not built. The controls above raise the cost - the attacker has to keep a process alive and talking rather than kill it - without closing the gap.

Neither alert is delivered anywhere; webhook and SIEM integration is delivered to a webhook or a syslog collector (DLP-43, DLP-44); a tenant that has configured neither is one where somebody has to open the console.

4.6 Man-in-the-middle ​

Mitigation - implemented. TLS with rustls. Plain HTTP and insecure_skip_tls_verify are refused at runtime in release builds - verified in production during bring-up, where the release agent rejected an http:// URL. An internal CA can be pinned. A CA bundle containing no certificate is rejected rather than silently trusted.


5. Console user → server ​

5.1 Cross-tenant access ​

Mitigation - implemented, two layers. Every query filters on tenant_id as part of the lookup, never as a check afterwards, so another tenant's id is simply not found - a 404 identical to a genuinely missing row, which stops the API being an enumeration oracle. Underneath, composite foreign keys (id, tenant_id) make a cross-tenant reference impossible to store. Asserted in the end-to-end script.

5.2 Privilege escalation ​

Mitigation - implemented. Five roles against twenty-one permissions, checked server-side on every route. The dashboard's hiding of controls is a convenience, never a control. An API token is further limited to its explicit scopes, so a token minted by an owner is not an owner-equivalent key. Ten tests cover the matrix.

5.3 Session theft ​

Mitigation - implemented. Session tokens are 256-bit random values; only a SHA-256 hash is stored, so a database leak yields no usable credential. The cookie is HttpOnly, Secure, SameSite=Strict. CORS is restricted to configured origins; a wildcard is impossible with credentialed requests.

What a stolen session can still do, and what it cannot (DLP-613). A session taken from an unlocked machine or a copied cookie reads what its person could read, for as long as it lives. It cannot release a policy, give anybody a role, redirect or remove incident delivery, change single sign-on, or issue, rotate or revoke an API token, unless the person signed in within the last 15 minutes. Past that, each of those actions needs the password again, or a fresh sign-in at the identity provider whose auth_time the server checks. The confirmation is one use of one named action on that session, for five minutes. It is kept server-side and dies with the session. Wrong passwords count toward the same lockout as signing in, so a stolen session is not a way to guess the password unthrottled. An API token can never confirm. The routes and the actions deliberately left out are listed in docs/API.md, Confirming it is you.

What this does not cover: the first 15 minutes after a sign-in, which is the window a session stolen straight after sign-in has; a thief who also has the password; and an identity provider that ignores prompt=login while claiming a fresh auth_time. The server refuses a provider that answers from an older sign-in (not_recent), but it cannot detect one that lies about the time.

5.4 Brute force ​

Mitigation - implemented. Argon2id, account lockout after a configurable number of failures, uniform responses regardless of failure cause, and a timing equaliser so the unknown-account path costs the same as a wrong password. nginx limits /auth/login to 5 requests per minute per address.

5.5 Insider reading evidence ​

Mitigation - implemented. A separate evidence:read permission, a mandatory justification of at least five characters, and an append-only audit entry for every access. The incident detail carries no preview for anybody, only a count and a reason; previews come only through the justified evidence path. Until DLP-651 the detail handed previews to every evidence:read holder on each open, with no justification and no entry, so this section described a control that the most-used response bypassed. Asserted by tests/evidence_access.sh.


5.6 A leaked SCIM token (DLP-1187) ​

A SCIM token lets whoever holds it create, change and close accounts, and put them in groups. With a group named after a mapped one, that can make an existing person an Admin, which lasts until that person's next sign-in recomputes the role from the provider's own token.

Mitigation - implemented.

  • Roles come only from the mappings single sign-on already uses, and provisioning never grants or changes Owner, so the tenant cannot be taken with it.
  • A new account has no password, so it signs in through the provider, which must authenticate that address, unless an administrator issues it a password.
  • The token opens nothing but /api/v1/scim/v2.
  • It is created only by a person holding sso:write after a step-up, lives at most a year, shows when it was last used, and is revoked in one step.
  • Every change it makes is audited as scim:<token>.

Honest limit. Between a leak and its revocation, a holder can disable every non-Owner account, and raise a colluding person to any non-Owner mapped role until they next sign in. The audit log shows each step, and nothing rate-limits it.

5.7 A forged change notification (DLP-1196) ​

/api/v1/m365/notifications is public, because Graph posts to it from the internet with no credential of ours. Anybody who knows the server's address can post there.

Mitigation - implemented.

  • A notification is believed only when it names a known subscription and carries that subscription's clientState: 32 random bytes that were sent to Graph once, and are kept here only as a hash.
  • Anything else is answered 202, exactly as a genuine one is, and ignored. So the endpoint says nothing about which subscriptions exist.
  • A believed notification only queues a scan that would run on schedule anyway, coalesced so a burst is one scan. Nothing in the payload is used.

Honest limit. Whoever holds a subscription's clientState can make the server scan that site as often as a scan takes. That value lives at Microsoft, and it is replaced when the site is subscribed again.

6. Server-side ​

6.1 Rewriting the audit log ​

Mitigation - implemented. UPDATE and DELETE on audit_log are refused by database trigger - asserted directly in the end-to-end script - and each entry hashes its predecessor, so removing or editing one breaks the chain at that point and every point after it. Mitigation - implemented (DLP-SEC-018). Signed checkpoints. A checkpoint is a statement about the chain's head at a moment, signed with a key whose only purpose is that - "at entry 5 the head was H" - and it travels with the public key that checks it, so it survives a rotation.

tests/audit_checkpoints.sh performs the attack described below, with the append-only trigger switched off the way its owner would switch it off, and shows the two answers side by side: /audit/verify reports the chain intact, and the checkpoint reports that the head no longer matches.

Mitigation - implemented where configured (DLP-1185). An RFC 3161 timestamp authority stamps each checkpoint automatically, on a schedule. The authority signs "this digest existed at this time" with a key this server does not hold, and nobody can obtain a token dated earlier than the moment they asked. So a rewrite cannot carry the old stamps. The old stamps verify and disagree with the rewritten chain, and any new stamp is dated after the rewrite. The server verifies every token before keeping it, and lists every token for independent checking with openssl ts -verify. tests/audit_anchoring.sh performs the same rewrite as the checkpoint suite. It shows the stamps no longer holding, a re-stamp dated after the rewrite, and a backdated column that does not move the reported time.

Honest limit. Neither control prevents a rewrite. A signed checkpoint stored only in this database is one the same attacker rewrites beside the chain. With no timestamp authority configured, the control is still the customer having taken a copy out, and nothing forces that. With one configured, an attacker cannot forge or backdate a stamp. They can delete stamps, and a deleted stamp is a gap, not a contradiction. Copies of the tokens kept outside the database, or the authority's own records where it keeps them, turn that gap back into evidence. Anchoring is off unless an operator configures an authority, and the choice of authority, and trust in it, is the operator's.

6.2 Forging a policy bundle ​

Mitigation - implemented. Bundles are Ed25519-signed; agents accept only key ids they were given at enrollment. The envelope's key id must match the id inside the signed payload, so an attacker cannot point the envelope at a different key. Honest limit. A new server's signing key is sealed in the database (inline:), and a server compromise yields it. An operator can move it to a secret manager (env:), or into a PKCS#11 token (DLP-239), where a compromise yields only the ability to sign while the attacker is on the server (ADR-0039 §A17). This paragraph used to call HSM support future work after it had shipped.

6.3 SQL injection ​

Mitigation - implemented. All queries are parameterised through sqlx. The two dynamically assembled statements (incident list and export) compose only fixed fragments; every value is bound.

6.4 CSV formula injection ​

Mitigation - implemented. Exported values beginning =, +, -, @ or a control character are prefixed with an apostrophe, so a hostile file name cannot execute in a spreadsheet. Tested.

6.5 Resource exhaustion by hostile documents ​

Mitigation - implemented for the endpoint path. Byte, match, location and time budgets on every scan; input-size, output-size and container limits on extraction; regex compile-size caps with linear-time matching. Malformed PDFs, truncated archives and multi-byte boundary cases are fuzz-adjacent tested. Not implemented. The Tika and Presidio sandboxes, because those services are contract-only.


Removing audit entries (DLP-55) ​

The append-only trigger refuses every UPDATE and DELETE on audit_log, and still does. One function is the exception, and it is deliberately narrower than the trigger is broad: prune_archived_audit() removes only the rows a segment covers, only when that segment has been written, read back off the storage, chain-verified and signed, and only when the table still holds exactly the number of rows the segment claims for that range.

What this changes in the model. Before, an attacker with the application's database role could not remove audit entries at all - and neither could the product, which is why audit retention was unimplemented and retention_days.audit did nothing. That role can now remove entries that already exist in a signed archive. The gain is therefore bounded by the archive: to make an entry disappear an attacker must also reach the archive storage and the signing key, and removing a segment from storage is detectable, because the database keeps that segment's range, hashes and signature.

What it does not change. An entry that has never been archived cannot be removed by anything. The chain still verifies end to end, continuing from the last archived entry_hash rather than from genesis.

The honest caveat. Where the application role owns audit_log - the default here, because migrations run as that role - the trigger was already a control against accident and injection rather than against a fully compromised role, since an owner may disable a trigger. Running migrations as a separate role is the deployment change that would make the original claim true, and it is not made here.

7. Residual risks accepted for this release ​

RiskWhy acceptedTicket
Enforcement on one channel, one platformLinux file_open can be denied where a path asks for it (DLP-62); everything else reports and does not prevent. Stated in the product, per endpoint, not hiddenADR-0010; Accepted: ADR-0037
Device key on disk on Linux and macOSWindows seals it with DPAPI at machine scope; elsewhere it is a 0600 file, compensated by revocationAccepted: ADR-0039 §A2
Audit anchoring is off until an operator names a timestamp authority, and a deleted stamp is a gapWith DLP_AUDIT_TSA_URL set, every checkpoint is stamped by an RFC 3161 authority, and a rewrite cannot carry the old stamps (DLP-1185). Without it, a full-tail rewrite is detectable only against a checkpoint somebody kept. Either way, somebody with the database can delete stamps: hiding one is possible, forging one is notAccepted: ADR-0039 §A16
Rate limiting follows the proven identityPer-device buckets apply after signature verification, so a claimed identity cannot drain another device's allowance. Enrolment has no device yet and is limited by token use counts and per address. This row said until DLP-1174 that there was no per-device limitAccepted: ADR-0039 §A8
Windows flow not run on Windowswindows-e2e is written to run enrolment, the private-key and state-directory ACLs, ReadDirectoryChangesW monitoring, offline buffering and revocation on windows-latest (DLP-3). It has not yet passed: no GitHub-hosted job ran from 2026-09-15, before it existed, to 2026-10-01 (DLP-1215), and since then it stops before enrolment (DLP-1492, Windows CI status). The self-hosted runner mydlp-host (DLP-1225) is Linux. Until it runs, the flow is asserted only by Linux tests and a Windows cross-compile. Even then it will not cover a fleet - no domain, no Group Policy, no endpoint protection alongside the agent, and no reboot, because a hosted runner cannot restart and return to the same jobDLP-449
An alert is raised but not deliveredSequence gaps and prolonged silence are detected, audited and surfaced, and incidents are delivered to a webhook (DLP-43) or a syslog collector (DLP-44) with health reporting and six attempts over about fifteen minutes. A delivery that fails its last attempt is set aside, not retried again, and stays undelivered until somebody replays it: Incident delivery lists each one with the receiver's last answer and sends them again on request, behind a fresh sign-in (DLP-932). So a receiver down for longer than a quarter of an hour loses those incidents from its feed until an administrator acts. A tenant with no destination configured delivers nothing, which the console says out loudAccepted: ADR-0039 §A13
An agent can still be stoppedDeliberate. Stopping it cannot be quiet: the OS supervisor restarts it, a kill is reported at the next start, and unexplained silence is alerted by hostname. Making it un-stoppable would make it indistinguishable from malware and unremovable by an entitled administratorAccepted: ADR-0039 §A12
Windows installer evidence does not exist yetA CI job is written to run MSI failure rollback, install, running-service upgrade, downgrade refusal and uninstall on windows-latest, and has not yet passed: no GitHub-hosted job ran from 2026-09-15 to 2026-10-01 (DLP-1215), and none of the Windows jobs has passed since (Windows CI status). What runs is the static half, 26 assertions against Package.wxs under pwsh on Linux. Even once the job runs, it covers no domain, endpoint-protection coexistence, disk-full volume or rebootDLP-449

7.1 Stopping the agent ​

The threat is an administrator, or someone who has become one, stopping the agent to move data without it being recorded. The control is not prevention.

An agent that resisted being killed would be indistinguishable from malware - it would be flagged by whatever endpoint protection is already on the machine, and it would be unremovable by an administrator entitled to remove it. So the agent does not resist. It makes stopping visible, within one check-in interval:

  1. The supervisor restarts it. systemd Restart=always, or the Windows Service Control Manager's recovery actions. Both are in the place an administrator already looks, and both can be turned off by one who is entitled to - which is the point.
  2. A kill is reported. The agent holds a marker file while running and removes it on a clean shutdown. Finding one at start-up means the previous run did not reach its shutdown path, which is reported once at the next check-in and recorded as agent.interrupted. A clean shutdown leaves its time, reported at the next check-in as agent.stopped with its length (DLP-1613): a Stop-Service by a local administrator is no longer invisible. Since DLP-1770 it is told apart from an update's and a shutdown's by what the next start sees (another version, or a boot after the stop), and counted apart as unexplained. That is inferred: a person who stops the service and then restarts the machine is read as a shutdown.
  3. Silence is classified. The server knows which endpoints stopped for a recorded reason. Everything else is unexplained and is named on Administration → System health.
  4. An authorised removal says so first. dlp-agent uninstall --confirm --authorization &lt;id> reports the removal before wiping the identity - afterwards the agent cannot prove who it is - and the server records agent.uninstalled with the id.

SIGTERM is handled so systemctl stop is a clean exit. An alert that fired on every ordinary restart would be switched off within a week, and then the real one would not be seen either.

What an attacker with administrator rights can still do: uninstall the agent quoting an authorisation id they invented. The console will show a clean removal. What they cannot do is leave no trace - the removal is in the append-only audit log with the id they chose and the device it came from.

What the agent deliberately does not do: hidden persistence, process or service hiding, blocking Task Manager or systemctl, a second watchdog process restarting the first, Protected Process Light, or any form of security-tool evasion. Each of those would make the product a better attack tool than a control, and the ticket that asked for tamper resistance said so explicitly.


8. What this model does not cover ​

  • The customer's identity provider. Sign-in against one is implemented (DLP-28) and what this product does with an ID token is in scope - the signature is checked against the provider's published keys, symmetric algorithms and none are refused by name, iss, aud, azp, exp and the nonce are all mandatory, and a person with no mapped group gets no session. What is outside this model is the provider itself: if the directory is compromised, or somebody is left in a privileged group after they should not be, MyDLP will faithfully sign them in as whatever that group maps to. The compensating controls are the tenant's own - group hygiene, conditional access, and the local administrator accounts that keep working when the provider does not.
  • The customer's network. We assume TLS is not broken.
  • Physical access to endpoints. Disk encryption is the control, not DLP.
  • Supply-chain compromise of our own dependencies. Partially addressed by SBOM generation and cargo-audit, both of which are CI work in progress.

Statements here describe what is built and tested; planned work is marked as planned.