Skip to content

fix(core): enforce owner-only Windows ACLs on sensitive files and dirs - #3495

Open
pkhodade-NV wants to merge 4 commits into
windowsfrom
fix/windows-acl-owner-only-permissions
Open

pkhodade-NV wants to merge 4 commits into
windowsfrom
fix/windows-acl-owner-only-permissions

Conversation

@pkhodade-NV

Copy link
Copy Markdown
Collaborator

Summary

  • set_dir_owner_only/set_file_owner_only in openshell-core/src/paths.rs were unconditional no-ops on Windows (documented as such), so locally-stored CLI credential material (mTLS client private key, OIDC/edge tokens, cached SSH keys) and the gateway's key-encryption key relied entirely on inherited NTFS ACLs, with no OpenShell-applied restriction.
  • is_file_permissions_too_open (the one available detection mechanism) was also Unix-only, so there was no way to audit for this after the fact either.

Related Issue

No linked issue — this is a security-sensitive, localized fix to platform-specific permission-hardening helpers already documented as required (the doc comments on these functions state the intent plainly; only the Windows implementation was missing).

Changes

  • paths.rs: set_dir_owner_only/set_file_owner_only now apply a real owner-only Windows ACL via SetEntriesInAclW/SetNamedSecurityInfoW with PROTECTED_DACL_SECURITY_INFORMATION, which strips inherited ACEs from the parent directory — the actual source of the unintended access. Directories additionally propagate the ACE to children (SUB_CONTAINERS_AND_OBJECTS_INHERIT).
  • is_file_permissions_too_open is no longer Unix-only; on Windows it inspects the DACL via GetNamedSecurityInfoW and flags any ACE granted to a trustee other than the current user.
  • Added the windows crate (already a workspace dependency, used by the MXC driver's ETW consumer) as a Windows-only dependency of openshell-core, with the additional Win32_Security*/Win32_Storage_FileSystem/Win32_System_Memory/Win32_System_SystemServices/Win32_System_Threading features needed for the ACL APIs. The unsafe FFI surface is confined to a private windows_acl submodule, matching the existing precedent in openshell-driver-mxc.
  • Added Windows-side test coverage for openshell-driver-db-credstore's key-encryption-key generation (previously #[cfg(unix)]-only, so the gateway's master key-encryption key had no Windows test coverage at all).
  • Fixed a stale "no-op on non-Unix platforms" doc comment in openshell-server/src/persistence/sqlite.rs.
  • Regenerated the two example crates' separate Cargo.lock files (governance-interceptor, supervisor-middleware-content-guard) since both path-depend on openshell-core; diffs are purely additive (new windows-* crate entries only, no other package bumped).

No caller changes were needed in openshell-bootstrap/mtls.rs, edge_token.rs, oidc_token.rs, or openshell-cli/ssh.rs — they already route through the fixed paths.rs functions.

Testing

  • Added Windows-parity unit tests in paths.rs mirroring the four existing Unix tests.
  • Independently verified the resulting ACLs with icacls: a restricted file gets a single owner-only ACE (user:(F)), a restricted directory gets user:(OI)(CI)(F) — no leftover SYSTEM/Administrators/Users entries from inheritance.
  • cargo test -p openshell-driver-db-credstore --target x86_64-pc-windows-msvc generated_key_encryption_key_file_is_owner_only passes on Windows.

Checklist

  • Tests added/updated for the new behavior
  • Docs/comments updated where stale
  • No unrelated changes bundled in

Originally opened as GitLab MR !113 against our internal mirror; re-opened here against windows for upstream review.

set_dir_owner_only/set_file_owner_only were unconditional no-ops on
Windows, so the CLI's mTLS client private key, OIDC/edge tokens, cached
SSH keys, and the gateway's key-encryption key relied entirely on
inherited NTFS ACLs with no OpenShell-applied restriction. Apply an
owner-only DACL via SetEntriesInAclW/SetNamedSecurityInfoW with
PROTECTED_DACL_SECURITY_INFORMATION to strip inherited ACEs, matching
the 0700/0600 guarantee already provided on Unix. is_file_permissions_too_open
now also works on Windows instead of being Unix-only, closing the
detection gap alongside the prevention gap.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
(cherry picked from commit 71560e947f85819efbcddf70ddda94befab62b0b)
has_foreign_trustee conflated a NULL DACL with an unreadable/invalid
ACL and returned Some(false) (not too open) for both. Per the Win32
contract, a NULL DACL means the object grants full access to everyone
-- the most permissive state possible -- so it must be flagged as too
open. Split the null and invalid-ACL branches: null now returns
Some(true), invalid ACL keeps the existing unreadable-ACL fallback
(None, which the caller maps to false via unwrap_or). Adds a
regression test that constructs a real NULL DACL via a
SetNamedSecurityInfoW helper confined to the windows_acl module,
consistent with the existing unsafe-FFI confinement in that module.

Found by CodeRabbit review on MR !113.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
(cherry picked from commit 46e635a4ef1d6937cdb088f46aa85baee3d6ad28)
@copy-pr-bot

copy-pr-bot Bot commented Sep 20, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@shailendra-nv shailendra-nv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes because the new Windows ACL audit can report a path as owner-only when it is not. The inline comments cover three false-negative cases: a foreign object owner is neither changed nor audited, Win32 inspection errors are mapped to safe, and non-basic access-allow ACE types are skipped.

Please also update architecture/gateway.md:449, which still says SQLite files are tightened to Unix mode 0o600. The canonical architecture documentation should distinguish Unix modes from the new protected-DACL behavior on Windows.

Validation on this head was otherwise clean: the native Windows ARM64 workspace check passed; the Windows pre-commit suite passed 4,949 tests with 26 skipped; and cargo fmt --all --check plus git diff --check passed. The requested changes concern the security contract rather than build correctness.

Comment thread crates/openshell-core/src/paths.rs Outdated
PWSTR::from_raw(path_hstring.as_ptr().cast_mut()),
SE_FILE_OBJECT,
DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION,
None,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This updates only the DACL and leaves the object owner unchanged. For a pre-existing or migrated sensitive path owned by another SID, the call can succeed when the current process has WRITE_DAC, but the foreign owner still has implicit WRITE_DAC and can later replace this DACL. The audit below also retrieves only the DACL, so it will report that path as secure. Please request/check OWNER_SECURITY_INFORMATION and either set the owner to the current user or reject a foreign-owned object, with a regression test. Microsoft documents the implicit DACL-modification right of the owner here: https://learn.microsoft.com/en-us/windows/win32/secauthz/owner-of-a-new-object

Comment thread crates/openshell-core/src/paths.rs Outdated
/// See the Unix doc comment above for the cross-platform contract.
#[cfg(windows)]
pub fn is_file_permissions_too_open(path: &Path) -> bool {
windows_acl::has_foreign_trustee(path).unwrap_or(false)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unwrap_or(false) converts every Win32 inspection failure into permissions are not too open. Missing READ_CONTROL, unsupported filesystem behavior, token-query failures, or an invalid ACL can therefore produce a security false negative. Please expose a Result<bool> to callers, or conservatively treat an inspection failure as too open, and add an error-path regression test.

let mut ace_ptr: *mut core::ffi::c_void = core::ptr::null_mut();
// SAFETY: `dacl` is valid and `index` is within `AceCount`.
if unsafe { GetAce(dacl, index, &raw mut ace_ptr) }.is_err() {
continue;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment is not true for all other ACE types: Windows also defines access-allowed object, callback, and callback-object ACEs, each of which may grant rights to a foreign trustee. Skipping them lets this audit return false for a non-owner-only DACL. Please parse all access-allow layouts or conservatively flag unsupported allow ACEs, with coverage for these variants. See the Microsoft ACE type table: https://learn.microsoft.com/en-us/windows/win32/secauthz/ace-strings

restrict_to_current_user() updated only the DACL, leaving a foreign
owner's implicit WRITE_DAC right intact -- they could later replace
the DACL we just set. Query OWNER_SECURITY_INFORMATION and take
ownership in the same SetNamedSecurityInfoW call; if the caller can't
(a genuinely foreign-owned object), the call now fails instead of
silently leaving the object insecure.

is_file_permissions_too_open() mapped every Win32 inspection failure
(missing READ_CONTROL, an invalid ACL, a token-query failure) to
"not too open" via unwrap_or(false). Fail closed instead: an
inspection failure is a security false-negative risk, not a green
light.

has_foreign_trustee()'s ACE loop only recognized plain
ACCESS_ALLOWED_ACE_TYPE and treated every other type as non-granting.
Windows also defines access-allowed object, callback, and
callback-object ACE variants that can grant rights to a foreign
trustee; this audit doesn't parse their wider layouts, so their mere
presence is now conservatively flagged as too open instead of
silently skipped.

Also updates architecture/gateway.md, which still described the
SQLite file-tightening behavior only in terms of Unix mode 0o600, to
distinguish it from the owner-only DACL behavior on Windows.

Addresses review comments on PR #3495.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
…ner-only-permissions

# Conflicts:
#	Cargo.toml
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.

2 participants