Split from #4043. Part 1 of that issue, the ACL check before the lock, ships in #4050. This is the part 2 residual (Low, defence in depth).
The gap
#4038's lock grants the Darling service account Modify on the whole install tree. Only pg-runtime\ and pg-runtime-prev\ need writes from the service. So a compromised service or postgres process can rewrite the service exe or a DLL and persist across restarts.
Fix
Grant the service account Read & Execute on the install root, and Modify only on pg-runtime\ and pg-runtime-prev\, created ahead of time. Do this in both install-darling.ps1 and the upgrade script, and pin the resulting ACEs in a test.
Write-site audit (static, done in #4050, not yet closed)
| Path (under the install root) |
Service writes? |
pg-runtime\, pg-runtime-prev\, and the pg-runtime.sha256 / .stamp / .blocked stamp files |
Yes. Confirmed, and scoped to those two folders (DarlingManagedPostgres, DarlingStoreUpgrade). |
darling.json |
No. Only the elevated installer (step 4b) and the --configure-network verb write it. |
DarlingInstallLocation.Report |
No. It reads only. |
wwwroot (the web content root) |
Believed read-only. Not verified. |
The BringYourOwnDirectoryName folder beside darling.json (DarlingLogHashKeyFile.DirectoryFor) |
Open. It is used only when postgres.managed = false and the service is not in a container (bring-your-own Postgres). The managed default resolves under %ProgramData%, outside the tree. |
Resolve the open row before narrowing anything. Pick one:
- move that folder under
%ProgramData%, and migrate an existing key;
- fold it into the Modify grant;
- show that nobody runs that combination on Windows.
Risk and verification
If a write site is missed, the service breaks at start, which is tier 1. So, before shipping:
- verify that
wwwroot is read-only;
- run a real service install and upgrade on a disposable box, never on a monitored or dev-in-use host. Walk it through install, start, one collection cycle, restart, an upgrade that rescue-copies into
pg-runtime-prev, and bring-your-own-Postgres mode if that mode keeps a write site in the tree.
Also exercise #4050 on the same box
#4050's check couldn't be tested where it was built. The box run should also cover:
- a real locked install re-run;
- a gMSA service account;
- a domain admin whose local-admin membership is nested;
- an install root on a second volume.
Confirm each passes or refuses as #4050's PR body says it should.
Done when
- The narrowed grant is in both scripts.
- A test pins the ACEs.
- The box run is recorded in the PR body.
Split from #4043. Part 1 of that issue, the ACL check before the lock, ships in #4050. This is the part 2 residual (Low, defence in depth).
The gap
#4038's lock grants the Darling service account Modify on the whole install tree. Only
pg-runtime\andpg-runtime-prev\need writes from the service. So a compromised service or postgres process can rewrite the service exe or a DLL and persist across restarts.Fix
Grant the service account Read & Execute on the install root, and Modify only on
pg-runtime\andpg-runtime-prev\, created ahead of time. Do this in bothinstall-darling.ps1and the upgrade script, and pin the resulting ACEs in a test.Write-site audit (static, done in #4050, not yet closed)
pg-runtime\,pg-runtime-prev\, and thepg-runtime.sha256/.stamp/.blockedstamp filesDarlingManagedPostgres,DarlingStoreUpgrade).darling.json--configure-networkverb write it.DarlingInstallLocation.Reportwwwroot(the web content root)BringYourOwnDirectoryNamefolder besidedarling.json(DarlingLogHashKeyFile.DirectoryFor)postgres.managed = falseand the service is not in a container (bring-your-own Postgres). The managed default resolves under%ProgramData%, outside the tree.Resolve the open row before narrowing anything. Pick one:
%ProgramData%, and migrate an existing key;Risk and verification
If a write site is missed, the service breaks at start, which is tier 1. So, before shipping:
wwwrootis read-only;pg-runtime-prev, and bring-your-own-Postgres mode if that mode keeps a write site in the tree.Also exercise #4050 on the same box
#4050's check couldn't be tested where it was built. The box run should also cover:
Confirm each passes or refuses as #4050's PR body says it should.
Done when