Skip already-hardened config backups in the startup ACL sweep (#2093) - #2094
Conversation
The #1816 sweep re-attempted its ACL rewrite on every start for every darling.json.bak-*, and the rewrite needs OWNERSHIP -- which the error message's own icacls remediation does not transfer. An operator who followed the instructions closed the exposure and still got the ERROR line every start about a file that was already secure (the independent READABLE-by-Users CRITICAL check stayed silent, correctly). The sweep now gates on IsReadableByOrdinaryUsers: exposure closed means nothing to do and nothing to report; the CRITICAL check remains the witness for the still-exposed case. Canary-ACE test pins that an already-hardened backup is skipped untouched, not re-hardened. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| if (!DarlingFileSecurity.IsReadableByOrdinaryUsers(backup)) | ||
| { | ||
| continue; | ||
| } |
There was a problem hiding this comment.
IsReadableByOrdinaryUsers returns false in two different situations, and this gate treats them identically:
- The exposure is genuinely closed (the intended case for this fix).
- The DACL itself couldn't be read —
GetAccessControl()threwIOException/UnauthorizedAccessException/InvalidOperationException/PlatformNotSupportedExceptionand was swallowed (DarlingFileSecurity.cs~line 213).
Case 2 is exactly the scenario the method's own doc comment was written for — but only for the existing call site, after a HardenFile attempt: "Returns false when the DACL itself cannot be read — the harden attempt the caller just made already logs loudly on failure, and a Critical raised on an unreadable DACL would be noise, not signal."
That assumption no longer holds here, because this new gate runs before HardenFile is ever attempted. Previously, a backup whose ACL couldn't even be inspected would fall through to HardenFile, which would very likely fail for the same reason and land in the catch below, logging the actionable icacls remediation. Now it hits continue here instead, and since the CRITICAL check further down (line 683) uses the same IsReadableByOrdinaryUsers(backup) call and also gets false, nothing is logged at all.
Net effect: a backup carrying the same DPAPI-protected credentials as the live config, whose permissions are broken enough that the ACL can't even be read, now fails completely silently instead of producing the loud, actionable error this file was designed to always emit. Worth distinguishing "confirmed already-secure" from "couldn't determine" before skipping — e.g. have IsReadableByOrdinaryUsers surface the read failure (or add a small wrapper) so this gate can fall through to the existing attempt/log path instead of swallowing it.
|
Reviewed the diff (CHANGELOG.md, DarlingWorker.cs, DarlingFileSecurityTests.cs). Scope check: this is Darling-only. Lite hardens its config directory's ACL once, differently ( Fix logic: gating Test: No SQL changed in this PR, so no T-SQL style-guide or missing-index concerns apply. No injection/secrets/network issues spotted — this is local ACL manipulation on paths derived from the resolved config path, not external input. |
Field shape (ghauan, #2093): the #1816 backup-hardening sweep re-attempts its ACL rewrite unconditionally on every start, and the rewrite needs ownership — which the error message's own
icaclsremediation doesn't transfer. So following the instructions closed the exposure but the ERROR kept firing every start about an already-secure file (the independent READABLE-by-Users CRITICAL check stayed silent — the honest witness).Fix: gate the sweep on
IsReadableByOrdinaryUsers— exposure already closed means skip entirely (nothing to do, nothing to report); the CRITICAL check remains the witness for still-exposed backups. Test pins the skip with a canary ACE that aHardenFilerewrite would strip: its survival proves the descriptor was never touched. Existing sweep tests (exposed backups get hardened, live file untouched, decoys ignored) unchanged and still passing semantics.🤖 Generated with Claude Code