icacls /inheritance:r before the grant leaves a file with no ACEs at all, so an interrupted hardening script locks every consumer out of a file that still says you own it
env paths · case
Symptom
Section titled “Symptom”A hardening routine that locks down a secrets file half-runs — a timeout, a transient failure, a killed CI job — and afterwards nothing can read it:
Access is denied.You own it. dir shows it. You cannot read it and cannot delete it, and neither
can the service that needs it. Re-running the hardening script does not help,
because it starts by stripping inheritance again on a file that already has no
ACEs. Recovery exists, but it is a DIFFERENT command than the one that broke it,
and nothing in the failure tells you that.
The dangerous order, interrupted after the first step:
C:\> echo secret > secret.txtC:\> icacls secret.txt /inheritance:rprocessed file: secret.txt
C:\> type secret.txtAccess is denied.
C:\> del secret.txtAccess is denied.The file now has an owner and an EMPTY DACL, which is not the same as no DACL: an empty DACL grants nothing to anyone, while a null DACL grants everything to everyone. Every consumer is locked out, including the service the hardening was for.
Recovery is possible because ownership carries WRITE_DAC — but only through a
different invocation:
C:\> icacls secret.txt /grant *S-1-5-32-544:(F)C:\> icacls secret.txt /resetThat is the part that makes this expensive in practice: the automation cannot
self-heal by retrying, and a human has to know that /grant or /reset is the
way back in.
POSIX chmod 000 is a fair comparison for the data access and not for the
recovery — there the owner restores the mode with the same tool they broke it
with.
/inheritance:r removes inherited ACEs IMMEDIATELY, and it does not care that
the explicit ACEs meant to replace them do not exist yet. Between that call and
the grant that follows, the file’s DACL is empty — and an empty DACL is not
“default permissions”, it is “deny everyone”.
POSIX intuition breaks in two places here. First, ownership does not imply read
access on Windows the way it effectively does under a POSIX mode. Second, there is
no single atomic operation that says “these are the permissions now”; icacls is
a sequence of mutations, and every gap between them is a state a crash can leave
you in.
So the ordering is not a style preference. Restrict-then-grant has a window where failure is unrecoverable by the same tool; grant-then-restrict does not.
Workaround
Section titled “Workaround”Grant first, restrict second, and treat the sequence as one that can be interrupted at any point:
icacls "%TARGET%" /grant *%SID%:(F) rem 1. keep a way back inicacls "%TARGET%" /inheritance:r rem 2. now safe to stripicacls "%TARGET%" /remove:g *S-1-5-32-545 rem 3. drop the restName principals by SID rather than by USERDOMAIN\USERNAME — that is
env-domain-principal, and it matters here because a grant against the wrong
name is a grant that did not happen.
For a directory, the grant needs the inheritance flags — (OI)(CI)(F) — or
children created later inherit nothing.
Design the routine so that failure at any step leaves the target USABLE rather than merely leaving it unhardened. Unhardened is a security finding you can fix on the next run; locked-out is a support ticket.
Verify at the end rather than trusting exit codes: icacls reports per-file
success lines, and a partially applied ACL can still exit zero on the step that
did run.
env-domain-principal covers WHO to name in the grant — the token SID rather
than USERDOMAIN\USERNAME. This case is about WHEN: the identity can be perfectly
correct and the order still locks every consumer out.