Files
pkhodade-NV ab64e84bfc fix(core): enforce owner-only Windows ACLs on sensitive files and dirs (#3495)
* fix(core): enforce owner-only Windows ACLs on sensitive files and dirs

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)

* fix(core): treat a NULL DACL as too open in is_file_permissions_too_open

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)

* fix(core): close three false-negative gaps in the Windows ACL audit

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>

* fix(core): conditional owner claim and audit owner in Windows ACL helpers

restrict_to_current_user: query the current owner before calling
SetNamedSecurityInfoW. Include OWNER_SECURITY_INFORMATION only when the
path has a foreign owner -- requesting it unconditionally fails with
ACCESS_DENIED (0x80070005) on standard credentials even when the current
user is already the owner, because WRITE_OWNER is not implied by object
ownership. A foreign-owned path still triggers an ownership claim and
fails hard if the claim is denied, preserving the security contract.

has_foreign_trustee: request OWNER_SECURITY_INFORMATION alongside
DACL_SECURITY_INFORMATION and reject paths with a foreign owner
immediately, before inspecting the DACL. A foreign owner has implicit
WRITE_DAC rights and can replace any DACL we set, so a clean DACL is not
sufficient evidence of safety on a foreign-owned object.

architecture/gateway.md: clarify that the Windows path-hardening behavior
sets mode 0o600 on Unix and applies a protected owner-only DACL on
Windows, with conditional ownership claim and fail-hard semantics for
foreign-owned objects.

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>

---------

Signed-off-by: Prashant Khodade <pkhodade@nvidia.com>
2026-09-23 10:13:32 -07:00

67 lines
2.2 KiB
TOML

# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
[package]
name = "openshell-core"
description = "Shared library for OpenShell - proto definitions, config, and errors"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
[dependencies]
async-trait = "0.1"
openshell-extension-core = { path = "../openshell-extension-core" }
glob = { workspace = true }
prost = { workspace = true }
prost-types = { workspace = true }
tonic = { workspace = true, features = ["channel", "tls-aws-lc"] }
tonic-prost = { workspace = true }
tonic-types = { workspace = true }
tokio = { workspace = true }
tokio-stream = { workspace = true }
thiserror = { workspace = true }
miette = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
sha2 = { workspace = true }
tracing = { workspace = true }
url = { workspace = true }
ipnet = "2"
rustls = { workspace = true }
rustls-pemfile = { workspace = true }
base64 = { workspace = true }
chrono = { version = "0.4", default-features = false, features = ["clock", "std"], optional = true }
reqwest = { workspace = true, features = ["blocking", "rustls-tls-native-roots-no-provider"], optional = true }
tar = { version = "0.4", optional = true }
tempfile = { version = "3", optional = true }
[target.'cfg(unix)'.dependencies]
nix = { workspace = true }
rustix = { workspace = true }
[target.'cfg(windows)'.dependencies]
windows = { workspace = true }
[features]
default = ["telemetry"]
## Compile in anonymous telemetry emission support. On by default; disable with
## `--no-default-features` (plus any other features you need) for a build that
## contains no telemetry endpoint, no HTTP client, and no emission code at all.
driver-extraction = ["dep:tar", "dep:tempfile"]
telemetry = ["dep:reqwest", "dep:chrono", "reqwest?/blocking"]
## OAuth2 token request helpers used by supervisor and gateway.
oauth = ["dep:reqwest"]
[build-dependencies]
tonic-prost-build = { workspace = true }
protoc-bin-vendored = { workspace = true }
[dev-dependencies]
tempfile = "3"
rcgen = { workspace = true }
[lints]
workspace = true