Verify the data directory the way the service will, before starting it - #187
Merged
Conversation
A 1.2.3 install failed at the first service start with "JWT signing-key file security validation failed: parent directory 'C:\ProgramData\NodePilot' grants mutation rights to an untrusted principal", and rolled back. The check is not new — it has existed since 1.0.0 — and the installer already applied the right ACL. What it never did was verify the result. The trusted set the service evaluates against includes the identity it is RUNNING as, so an ACE for a service account is only harmless while the service runs as that account. Install once as A and again as B and A's leftover ACE is a stranger with write access next to the signing key. Nothing between the ACL call and Start-Service noticed, so the API discovered it — after the binaries had been replaced, which puts the failure in the rollback path the catch block already documents as able to fail on its own. The installer now asks the same question itself. Test-ServiceDirectoryAclTrust mirrors BuildTrustedSids (as SIDs, not names — a localised Windows calls these groups something else), and Assert-ServiceDirectoryAclUsable verifies, repairs once through the same Set-DirectoryAclForService, verifies again, and otherwise gives up naming the principal and the icacls command that removes it. It runs before the artifact is extracted, so giving up costs nothing. The security check itself is unchanged. Widening the trusted set to bless any configured service account would defeat an audit finding whose whole point is a foreign account with write access beside the JWT key; producing a clean directory achieves the same outcome without giving that up. RestrictedFileWriter now names the offending principal instead of reporting an anonymous "untrusted principal" — account name and SID where it resolves, the bare SID where the account is gone, which is the common case here and exactly what icacls needs.
Sev7eNup
added a commit
that referenced
this pull request
Aug 11, 2026
Carries the installer fix (#187): the data directory is verified with the service's own rule after the ACL is applied, repaired once if a leftover ACE from an earlier service identity is in the way, and only then does the service start — instead of the API discovering it at first start, after the binaries have been replaced. Bumped through `npm version --no-git-tag-version` per package rather than a text replace over the tree, so only the top-level version and its lockfile root entry move.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
A 1.2.3 install failed at the first service start and rolled back:
The check is not new —
git log -Sputs it in the 1.0.0 commit — andgit diff v1.2.2..v1.2.3touches nothing outside the LLM stack, docs and version numbers. It was never a version problem.The installer already applied the correct ACL. What it never did was verify the result. The trusted set the service evaluates against (
BuildTrustedSids) includes the identity it is running as, so an ACE for a service account is only harmless while the service runs as that account. Install once as A and again as B, and A's leftover ACE is a stranger with write access next to the signing key. Nothing betweenSet-DirectoryAclForServiceandStart-Servicenoticed, so the API discovered it — after the binaries had been replaced, which lands the failure in the rollback path that the catch block already documents as able to fail on its own. That exact scenario was reproduced in the lab on 2026-08-05 and written down; the guard that would have caught it was never added.What
Test-ServiceDirectoryAclTrustanswers the same question the service will, the same way: trusted owner, no foreign Allow-ACE carrying mutation rights. The trusted set is kept as SIDs, not names — a localised Windows calls these groups something else, and a domain account resolves differently — and a contract test pins that it matches the API's set, because an installer that blesses a directory the service then rejects is the precise failure this exists for.Assert-ServiceDirectoryAclUsableverifies → repairs once through the sameSet-DirectoryAclForService(which drops inheritance, wipes explicit ACEs and forces the owner back to Administrators) → verifies again → otherwise gives up naming the principal and theicaclscommand that removes it. It runs before the artifact is extracted, so giving up costs nothing.RestrictedFileWriternames the offender. "grants mutation rights to an untrusted principal" is true and unusable. It is now…to DOMAIN\account (S-1-5-21-…), or the bare SID with "account no longer exists" — the common case here, since a decommissioned service account keeps its ACE, and the SID is whaticaclstakes.Deliberately not done
Invoke-NodePilotPreflightis the natural home for surfacing this earlier in the wizard, but it would pull in the fixed-sizeCheckIdsarray,CheckCountand their contract tests. The installer-side verification already guarantees the install succeeds; the wizard row is a separate, larger change.Verification
Set-DirectoryAclForServiceit is secure; an injectedBUILTIN\Users:(M)ACE is detected and named (VORDEFINIERT\Benutzer (S-1-5-32-545)on this German host — which is exactly why the trusted set is SID-based);Assert-ServiceDirectoryAclUsablerepairs it and the re-check passes.deploy/Test-DeploymentTemplates.ps1green, with four new contracts: the verification exists and follows the ACL apply, it runs before extraction, the repair is re-verified and gives up loudly, and the trusted SIDs match the API's.Jwt:RotateInsecureKeyFile=true, which the error message suggests, does not help: it replaces the key file, and the directory is what was rejected.