Appearance
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
| Claim | Proved by |
|---|---|
| A run that was killed is reported on the next start | continuity::tests::a_run_that_was_killed_is_reported_on_the_next_start |
| A clean shutdown leaves the next run nothing to report | continuity::tests::a_clean_shutdown_leaves_the_next_run_nothing_to_report |
| The gap is measured from the last heartbeat | continuity::tests::the_gap_is_measured_from_the_last_heartbeat |
| A clock that went backwards does not produce a nonsense gap | continuity::tests::a_clock_that_went_backwards_does_not_produce_a_nonsense_gap |
| A first run finds nothing and reports nothing | continuity::tests::a_first_run_finds_nothing_and_reports_nothing |
| A continuity report never carries content | continuity::tests::a_report_never_carries_content |
| The gap is described in units a person reads | continuity::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
| Claim | Proved by |
|---|---|
| The state directory's ACL is audited at every start | state_acl - 14 tests, and SECURITY_MODEL.md §4 |
| A missing ACL is the worst case, not the best | state_acl::tests::a_missing_access_control_list_is_the_worst_case_not_the_best |
| An unreadable permission set is not assumed safe | state_acl::tests::an_unreadable_permission_set_is_not_assumed_safe |
| Every untrusted reader is named | state_acl::tests::several_offenders_are_all_named |
| The finding reports what was there before the repair | state_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 verifyis 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
| Capability | State |
|---|---|
| Detecting a changed agent binary | Implemented. The installed and running hashes are reported separately from an invalid Authenticode signature (DLP-199). |
| A service DACL an administrator cannot casually weaken | Absent. DLP-153 |
| An unauthorised uninstall being refused rather than merely recorded | Absent. DLP-154; today the forgery direction is closed - an unauthenticated caller cannot claim a clean uninstall |
| Broader tamper resistance | DLP-33 |
6. What is not covered
- The agent's configuration file is not audited.
state_aclaudits the state directory at every start; nothing checks the permissions ofagent.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, becauseProtectSystem=strictmakes 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-latestCI job written to exercise it has not yet passed (Windows CI status), and a machine that lost power mid-scan isDLP-3.