Skip to content

Stop the config backups granting INTERACTIVE read, and pin the exact identity set (#1769) - #1786

Merged
erikdarlingdata merged 5 commits into
devfrom
feature/1769-config-backup-acl
Jul 28, 2026
Merged

erikdarlingdata merged 5 commits into
devfrom
feature/1769-config-backup-acl

Conversation

@erikdarlingdata

@erikdarlingdata erikdarlingdata commented Jul 28, 2026 •

Copy link
Copy Markdown
Owner

Closes #1769, which has been retitled to what actually ships: "Config BACKUPS were interactive-readable; darling.json's INTERACTIVE grant is load-bearing by design (Viewer)".

The original ask — drop the INTERACTIVE ACE from darling.json itself — is not being made, because it would break the shipped Viewer. That design question, with all three options recorded verbatim, now lives in #1792 and does not close with this PR.

The issue's fix shape does not survive contact, and this is why

#1769 asks for the INTERACTIVE ACE to be dropped from darling.json's hardening so the emitted DACL is exactly SYSTEM + Administrators + the service identity. That would break the shipped Viewer, and the chain is not subtle:

  1. The Viewer reads the config file directly — ViewerSettings.TryLoad does File.ReadAllText(path) at Darling/PerformanceMonitor.Darling.Viewer/ViewerSettings.cs:122.
  2. It runs non-elevated — <requestedExecutionLevel level="asInvoker" /> at Darling/PerformanceMonitor.Darling.Viewer/app.manifest:20, wired via <ApplicationManifest> in the Viewer csproj.
  3. Under UAC, an interactive administrator's default token carries BUILTIN\Administrators as SE_GROUP_USE_FOR_DENY_ONLY, so an Allow ACE for Administrators grants that token nothing. A three-identity DACL therefore denies the Viewer.
  4. The failure is total, not degraded: MainWindow.OnLoaded catches and shows "darling.json could not be read: …" (MainWindow.xaml.cs:174-178). No connection, no data.
  5. It hits the default shipped layout — the release zip puts the Viewer in viewer\ under the service root, and it probes the parent for the service's darling.json (ViewerSettings.cs:92-104).

The existing code says as much at DarlingWorker.cs:540-542, and install-darling.ps1 mirrors the grant deliberately. So the current behavior is intentional; the issue and the design disagree, and the issue did not account for the Viewer.

The security rationale is still sound, to be clear. I checked the obvious rebuttal: the Viewer already gets INTERACTIVE read on pg-admin-credential.dpapi by design, but the admin role's SELECT carves out the secret columns, so darling.json really is the only interactive-readable path to the monitored servers' SQL passwords and the MCP/web tokens. The tightening is worth doing — it just cannot be done by deleting the ACE alone, and choosing how to give the Viewer a non-secret path to what it needs (it only wants the postgres section) is a design decision, not a small fix.

What this PR does instead

The config BACKUPS have no such constraint. A darling.json.bak-* is a byte-for-byte copy of the same secrets, and nothing reads one: the only references to the .bak- name in non-test code are the two lines that CONSTRUCT it (DarlingCliCommands.cs:1333,1336), and restoring one is a hand operation that already requires elevation, because writing darling.json does — INTERACTIVE only ever had Read here, never Write.

So the grant bought nothing and left a second readable copy of every encrypted password and access token, exposed to any interactively-logged-on user. HardenConfigBackup now passes allowInteractiveRead: false, and install-darling.ps1 gets the same split so the installer and the service cannot disagree — its summary line now states which file gets which posture rather than claiming one rule for both.

This is correct under all three ways the headline question could be decided, which is why it is worth landing on its own.

Pins

Two, because the two failure modes are different.

  • HardenFile_NonInteractive_EmitsExactlyTheThreeIntendedIdentities — the exactly-three-identities assertion the issue asked for, valid on the non-interactive posture (superuser credential, transient init pwfile, and now backups). Presence/absence assertions pass happily while a fourth principal rides along, which is exactly the reported defect's shape, so this one names the intruder.
  • The end-to-end assertion added to WriteWithBackup_HardensTheBackup_… runs through the real WriteWithBackupAsync path, because the thing being fixed is a call site passing the wrong argument — a test of the ACL shape alone cannot see that.

Mutation table

# Mutation Result
M5 Make HardenFile add INTERACTIVE unconditionally RED (2): the new pin, reporting granted principals beyond SYSTEM, Administrators and the service identity: NT AUTHORITY\INTERACTIVE, plus the existing superuser test
M6 Revert HardenConfigBackup to allowInteractiveRead: true RED (1): WriteWithBackup_HardensTheBackup_BecauseFileCopyDoesNotCarryTheDacl

M6 is the one that matters for this change: it proves the call site is pinned, not merely the shape.

Verification

  • Darling.Tests full suite: 3485 passed, 0 failed, 171 skipped (the live-Postgres tests skip without DARLING_TEST_PG; unrelated to this change).
  • DarlingFileSecurityTests: 12/12.
  • dotnet build … Darling.Tests -c Debug -t:Rebuild and … Darling.Service -c Debug -t:Rebuild — both 0 Warning(s), 0 Error(s).
  • install-darling.ps1 parses clean (Parser::ParseFile, 0 errors).

Issue hygiene done alongside this

#1769 carries a comment with the full six-link chain at file:line (verified at 50012266) and an explicit decision block; it is retitled to the shipping scope; and #1792 was opened to hold Erik's three-way decision on darling.json so it survives this issue closing.

Note for the prompt maintainer

The client checklist's step-14 note currently says INTERACTIVE:(R) "will be removed in a future build". That is now false and wants rewording to by-design-pending-#1792 — this PR does not change darling.json's own DACL, so the observation stays expected on a field box. It also gains a new falsifiable check: post-this-build, any darling.json.bak-* shows three identities only.

🤖 Generated with Claude Code

erikdarlingdata and others added 3 commits July 27, 2026 23:57
…identity set (#1769)

This is the half of #1769 that is safe regardless of how the headline question
is decided. It does NOT close the issue.

VERIFIED, AND IT CONTRADICTS THE ISSUE'S FIX SHAPE. Removing INTERACTIVE:(R)
from darling.json ITSELF would break the shipped Viewer. ViewerSettings.TryLoad
reads the file directly (ViewerSettings.cs:122), the Viewer manifest is
asInvoker (app.manifest:20), and under UAC an interactive admin's default token
carries BUILTIN\Administrators as SE_GROUP_USE_FOR_DENY_ONLY - so a DACL of
exactly SYSTEM + Administrators + service identity denies it, and MainWindow
shows "darling.json could not be read". That hits the default shipped layout,
where the Viewer sits in viewer\ under the service root and probes the parent
for the service's config. So darling.json keeps its INTERACTIVE grant here,
pending a decision on how to give the Viewer a non-secret path to what it needs.

The BACKUPS have no such constraint. A .bak-* is a byte-for-byte copy of the
same secrets, and nothing reads one: the only references to the name in
non-test code are the two lines that construct it, and restoring one is a hand
operation that already requires elevation, because writing darling.json does -
INTERACTIVE only ever had Read, never Write. The grant bought nothing and left
a second readable copy of every encrypted password and access token.

install-darling.ps1 gets the same split so the installer and the service cannot
disagree, and its summary line now says which file gets which posture instead of
claiming one rule for both.

Two pins, because the two failure modes are different. The exactly-three-
identities assertion the issue asked for catches an unintended principal in the
emitted DACL and names it (presence/absence assertions pass happily while a
fourth rides along, which is the shape of the reported defect). The end-to-end
backup assertion runs through the real WriteWithBackupAsync path, because the
thing being fixed is a CALL SITE passing the wrong argument, which a test of
the shape alone cannot see.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
erikdarlingdata and others added 2 commits July 28, 2026 00:23
…fork (#1786)

#1769 is retitled to what actually ships (backups), so this closes it. The
darling.json question - the three options - moves to #1792 rather than closing
with it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@erikdarlingdata erikdarlingdata changed the title Stop the config backups granting INTERACTIVE read, and pin the exact identity set (partial #1769) Stop the config backups granting INTERACTIVE read, and pin the exact identity set (#1769) Jul 28, 2026
@erikdarlingdata
erikdarlingdata merged commit 5b128d8 into dev Jul 28, 2026
5 checks passed
@erikdarlingdata
erikdarlingdata deleted the feature/1769-config-backup-acl branch July 28, 2026 04:48
erikdarlingdata added a commit that referenced this pull request Jul 28, 2026
Resolved to get CI to run at all, not as an arm-time step: a CONFLICTING PR
has no merge ref, and pull_request workflows build the merge ref -- so Build
and Claude Auto Review could not start on this PR while the conflict stood,
while other branches' runs kept firing normally. That is what made this look
like an Actions outage for ~25 minutes.

CHANGELOG.md was the only conflict, and only in the link-ref block. Resolved
KEEP-BOTH with no re-sorting: this branch's #1665/#1788 refs and dev's
#1781/#1783/#1786/#1792 refs all retained, in the order they appeared. No
entry text on either side was touched.

Everything else auto-merged. Verified rather than assumed, because the
dangerous case here is textual cleanliness hiding a semantic break: full
solution rebuild 0 warnings / 0 errors, Darling suite 3549 passed / 0 failed.
The one real collision this class of merge produced -- #1776's new live-store
hygiene guard versus this branch's new own-store live test -- was found the
same way and fixed in 59f877a before this merge.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
pull Bot pushed a commit to ehtick/PerformanceMonitor that referenced this pull request Jul 29, 2026
…gdata#1816)

Field-verified exposure: backups created before erikdarlingdata#1786 fixed the
creation path kept inherited BUILTIN\Users read - full DPAPI
recoverability for every stored credential and token by any local
account. The installer's check flags them but only prints the fix.

The config-hardening sweep now also hardens every darling.json.bak-*
sibling (allowInteractiveRead: false, matching the creation path), with
the live file's best-effort + runnable-remediation contract and the
still-readable CRITICAL backstop. One service start closes the exposure
fleet-wide with no human in the loop.

ACL tests against real files pin: both backups stripped, live file left
to its own hardening, lookalike backups untouched, no-backup no-op;
watched red by disabling the hardening call. Full fast suite 3572
green; zero warnings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant