Skip to content

Self-protection, claim by claim ​

WIN-DLP-EPIC-022 asks for legitimate tamper resistance - protect, detect and report, without behaving like malware - because an agent that hides itself will be treated as malware by whatever else is installed, and deserves to be.

The design answer this project reached is the right one and is worth stating before any of the tests: it does not try to be hard to stop. It tries to make stopping it impossible to do quietly.


1. The decision the epic rests on ​

From deploy/mydlp-agent.service, and it is the clearest statement of the principle anywhere in the repository:

The watchdog half of tamper resistance is systemd's, not the agent's. That is a deliberate choice: a process that restarted itself from somewhere unexpected, hid from the process list, or resisted being killed would be indistinguishable from malware - and would be treated as malware by whatever endpoint protection is already on the machine.

And SECURITY_MODEL.md §8 lists the same thing as prohibitions: no process injection, no API hooking, no driver, no boot-start component, no hiding - the service visible in the service list, the process in Task Manager, the files on disk, and the uninstall path documented.

So the triad is:

  • Protect - an ACL on the state directory, and an operating system supervisor that restarts the agent.
  • Detect - a run marker that survives a kill, an ACL audited at every start, and the installed-binary hash plus (on Windows) Authenticode verdict checked at process start (DLP-199).
  • Report - silence alerting, a continuity report, and an audit entry for an authorised removal.

2. Detect ​

ClaimProved by
A run that was killed is reported on the next startcontinuity::tests::a_run_that_was_killed_is_reported_on_the_next_start
A clean shutdown leaves the next run nothing to reportcontinuity::tests::a_clean_shutdown_leaves_the_next_run_nothing_to_report
The gap is measured from the last heartbeatcontinuity::tests::the_gap_is_measured_from_the_last_heartbeat
A clock that went backwards does not produce a nonsense gapcontinuity::tests::a_clock_that_went_backwards_does_not_produce_a_nonsense_gap
A first run finds nothing and reports nothingcontinuity::tests::a_first_run_finds_nothing_and_reports_nothing
A continuity report never carries contentcontinuity::tests::a_report_never_carries_content
The gap is described in units a person readscontinuity::tests::a_gap_is_described_in_units_a_person_reads

The first two together are the whole mechanism: systemctl stop sends SIGTERM, which the agent handles and shuts down cleanly, so ordinary operations produce no alert. SIGKILL cannot be caught, and that is exactly the case this makes visible.

3. Protect ​

ClaimProved by
The state directory's ACL is audited at every startstate_acl - 14 tests, and SECURITY_MODEL.md §4
A missing ACL is the worst case, not the beststate_acl::tests::a_missing_access_control_list_is_the_worst_case_not_the_best
An unreadable permission set is not assumed safestate_acl::tests::an_unreadable_permission_set_is_not_assumed_safe
Every untrusted reader is namedstate_acl::tests::several_offenders_are_all_named
The finding reports what was there before the repairstate_acl::tests::the_finding_reports_what_was_there_before_the_repair

The systemd unit does the rest, and does it well: ProtectSystem=strict with a single ReadWritePaths, MemoryDenyWriteExecute, NoNewPrivileges, a system-call filter, and a capability set reduced to CAP_DAC_READ_SEARCH - "an agent that could craft packets would be a better attack tool than a DLP control".

It also runs as root and says so plainly rather than hiding it: "the agent reads files across the machine to scan them… anything narrower cannot read another user's documents, which is the entire job."

The safeguard that existed only in a comment ​

That unit warns about a specific mistake:

These two belong in [Unit], not [Service]. systemd only warns about them in the wrong section and then ignores them, so getting this wrong leaves the default in place and looks exactly like it worked - systemd-analyze verify is what catches it.

Nothing ran systemd-analyze verify. Not CI, not a test, not the deploy script. The consequence of the mistake is not cosmetic: the default restart limit is five starts in ten seconds and then systemd gives up, so a crash loop would end with monitoring switched off permanently - which is the outcome that file exists to prevent.

tests/systemd_units.sh runs it now, on every unit, in the lint job.

With one subtlety that is the reason it is a script and not a one-line step: systemd-analyze verify exits 0 on that very mistake. It prints

Unknown key name 'StartLimitIntervalSec' in section 'Service', ignoring.

and returns success. A gate that trusted the exit code would have passed a unit whose restart limits had silently reverted. The script treats any output as a failure, and then makes the mistake on purpose in a temporary copy to prove the check can fail - because a gate nobody has seen fail is a gate nobody knows works.

4. Report ​

tests/tamper_visibility.sh asserts the distinctions that matter, and they are distinctions between kinds of silence:

  • an agent removed with an authorisation is explained, and audited;
  • an agent that just stops is not explained, and is listed by name;
  • an agent reporting that its previous run was killed is recorded;
  • a laptop that missed one check-in is not an alert;
  • an unauthenticated caller cannot mark a device as cleanly uninstalled.

The fourth is as important as the others. A tamper alert that fires when somebody closes a laptop lid is one an operator learns to dismiss, and then the real one arrives.


5. What is absent ​

CapabilityState
Detecting a changed agent binaryImplemented. The installed and running hashes are reported separately from an invalid Authenticode signature (DLP-199).
A service DACL an administrator cannot casually weakenAbsent. DLP-153
An unauthorised uninstall being refused rather than merely recordedAbsent. DLP-154; today the forgery direction is closed - an unauthenticated caller cannot claim a clean uninstall
Broader tamper resistanceDLP-33

6. What is not covered ​

  • The agent's configuration file is not audited. state_acl audits the state directory at every start; nothing checks the permissions of agent.toml. On Windows that file lives inside the protected directory and is covered by the same ACL. On Linux it is /etc/mydlp/agent.toml, outside it, unaudited - though the agent cannot write it either, because ProtectSystem=strict makes the filesystem read-only except /var/lib/mydlp, and the one genuinely dangerous setting is already refused: transport::tests::insecure_tls_is_refused_outside_debug_builds. Recorded rather than ticketed: the exposure is a mis-packaged installation, which is a packaging concern rather than an agent one.
  • Nothing has been killed on Windows. The continuity marker is tested on Linux; the windows-latest CI job written to exercise it has not yet passed (Windows CI status), and a machine that lost power mid-scan is DLP-3.

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