diff --git a/Cargo.lock b/Cargo.lock
index 463a32b57..910a25f57 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -942,7 +942,7 @@ dependencies = [
[[package]]
name = "dsc-lib-registry"
-version = "1.2.0"
+version = "1.3.0"
dependencies = [
"cc",
"crossterm",
@@ -955,6 +955,7 @@ dependencies = [
"tracing",
"tracing-subscriber",
"utfx",
+ "windows 0.62.2",
]
[[package]]
@@ -1005,7 +1006,7 @@ dependencies = [
[[package]]
name = "dsc-resource-registry"
-version = "1.2.0"
+version = "1.3.0"
dependencies = [
"clap",
"crossterm",
diff --git a/Cargo.toml b/Cargo.toml
index 5504aa256..067a4e3bf 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -290,7 +290,7 @@ urlencoding = { version = "2.1" }
which = { version = "8.0.6" }
# dsc-lib
ipnetwork = { version = "0.21" }
-# WindowsUpdate, windows_service, dsc-lib (ACL checks)
+# WindowsUpdate, windows_service, dsc-lib (ACL checks), dsc-lib-registry
windows = { version = "0.62", features = [
"Win32_Foundation",
"Win32_NetworkManagement_WindowsFirewall",
@@ -299,6 +299,7 @@ windows = { version = "0.62", features = [
"Win32_System_Com",
"Win32_System_Memory",
"Win32_System_Ole",
+ "Win32_System_Registry",
"Win32_System_Services",
"Win32_System_Variant",
"Win32_System_UpdateAgent",
diff --git a/docs/reference/resources/Microsoft/Windows/Registry/index.md b/docs/reference/resources/Microsoft/Windows/Registry/index.md
index c59e6c087..5343ccd81 100644
--- a/docs/reference/resources/Microsoft/Windows/Registry/index.md
+++ b/docs/reference/resources/Microsoft/Windows/Registry/index.md
@@ -1,6 +1,6 @@
---
description: Microsoft.Windows/Registry resource reference documentation
-ms.date: 03/25/2025
+ms.date: 10/13/2026
ms.topic: reference
title: Microsoft.Windows/Registry
---
@@ -35,6 +35,7 @@ resources:
keyPath: string
# Instance properties
_exist:
+ acl:
valueData:
valueName:
```
@@ -46,6 +47,7 @@ values. The resource can:
- Add and remove registry keys.
- Add, update, and remove registry values.
+- Retrieve and configure registry key access control lists (ACLs).
> [!NOTE]
> This resource is installed with DSC itself on Windows systems.
@@ -106,6 +108,7 @@ The following list describes the properties for the resource.
They define the desired state for an instance of the resource.
- [_exist](#_exist) - Defines whether the registry key or value should exist.
+ - [acl](#acl) - Defines the registry key ACL in Security Descriptor Definition Language (SDDL).
- [valueData](#valuedata) - The data for a registry value.
- [valueName](#valuename) - The name of the registry value.
@@ -162,6 +165,28 @@ value for `_exist` is `true`, the resource adds or creates the instance if it do
the value for `_exist` is `false`, the resource removes or deletes the instance if it does exist.
The default value for this property when not specified for an instance is `true`.
+### acl
+
+Expand for acl property metadata
+
+```yaml
+Type : string
+IsRequired : false
+IsKey : false
+IsReadOnly : false
+IsWriteOnly : false
+```
+
+
+
+Defines the registry key ACL in Security Descriptor Definition Language (SDDL). For **Get**
+operations, include the owner (`O:`), group (`G:`), or discretionary access control list (`D:`)
+section in the input to retrieve those sections. For **Set** operations, the resource applies only
+the sections specified in the SDDL string. System access control lists (`S:`) aren't supported.
+
+This property applies to the key identified by `keyPath`, including when the instance also specifies
+a registry value. ACL operations aren't supported when using an offline registry hive.
+
### valueData
Expand for valueData property metadata
@@ -378,6 +403,9 @@ non validating keywords are omitted.
}
}
},
+ "acl": {
+ "type": "string"
+ },
"keyPath": {
"type": "string",
"pattern": "^(HKCR|HKEY_CLASSES_ROOT|HKCU|HKEY_CURRENT_USER|HKLM|HKEY_LOCAL_MACHINE|HKU|HKEY_USERS|HKCC|HKEY_CURRENT_CONFIG)\\\\"
diff --git a/lib/dsc-lib-registry/Cargo.toml b/lib/dsc-lib-registry/Cargo.toml
index 0f9981d12..2d21387d7 100644
--- a/lib/dsc-lib-registry/Cargo.toml
+++ b/lib/dsc-lib-registry/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "dsc-lib-registry"
-version = "1.2.0"
+version = "1.3.0"
edition = "2024"
[package.metadata.i18n]
@@ -22,6 +22,7 @@ thiserror = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
utfx = { workspace = true }
+windows = { workspace = true }
[build-dependencies]
cc = { workspace = true }
diff --git a/lib/dsc-lib-registry/locales/en-us.toml b/lib/dsc-lib-registry/locales/en-us.toml
index afffeb9af..55baa305f 100644
--- a/lib/dsc-lib-registry/locales/en-us.toml
+++ b/lib/dsc-lib-registry/locales/en-us.toml
@@ -4,19 +4,25 @@ _version = 1
# No localizable strings
[error]
+acl = "Registry ACL"
+invalidAcl = "The SDDL must define an owner, group, or DACL"
invalidHive = "Invalid hive"
json = "JSON"
offlineRegistry = "Offline registry"
+offlineAcl = "ACL operations are not supported for offline registries"
offlineValueEnumeration = "Value enumeration is not supported for offline registries"
registry = "Registry"
registryKey = "Registry key"
registryKeyNotFound = "Registry key not found"
registryValue = "Registry value"
+saclUnsupported = "System ACLs (SACLs) are not supported"
utf16Conversion = "UTF-16 conversion failed due to interior NULL values for"
unsupportedValueDataType = "Unsupported registry value data type"
+win32 = "%{operation} failed with error code %{code}"
[registry_helper]
whatIfCreateKey = "Key '%{subkey}' not found, would create it"
+whatIfSetAcl = "Would set ACL on key '%{key_path}'"
whatIfDeleteValue = "Would delete value '%{value_name}'"
whatIfDeleteSubkey = "Would delete subkey '%{subkey_name}'"
whatIfDeleteNonexistingKey = "Key '%{subkey}' not found, would do nothing"
diff --git a/lib/dsc-lib-registry/src/acl.rs b/lib/dsc-lib-registry/src/acl.rs
new file mode 100644
index 000000000..197d9e49f
--- /dev/null
+++ b/lib/dsc-lib-registry/src/acl.rs
@@ -0,0 +1,332 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use core::ffi::c_void;
+
+use windows::Win32::Foundation::{
+ ERROR_FILE_NOT_FOUND, ERROR_INSUFFICIENT_BUFFER, ERROR_PATH_NOT_FOUND, ERROR_SUCCESS, HLOCAL,
+ LocalFree,
+};
+use windows::Win32::Security::Authorization::{
+ ConvertSecurityDescriptorToStringSecurityDescriptorW,
+ ConvertStringSecurityDescriptorToSecurityDescriptorW,
+};
+use windows::Win32::Security::{
+ ACL, DACL_SECURITY_INFORMATION, GROUP_SECURITY_INFORMATION, GetSecurityDescriptorControl,
+ GetSecurityDescriptorDacl, GetSecurityDescriptorGroup, GetSecurityDescriptorOwner,
+ GetSecurityDescriptorSacl, OBJECT_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
+ PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR, PSID, SE_DACL_PROTECTED,
+ UNPROTECTED_DACL_SECURITY_INFORMATION,
+};
+use windows::Win32::System::Registry::{
+ HKEY, HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE,
+ HKEY_USERS, REG_SAM_FLAGS, RegCloseKey, RegGetKeySecurity, RegOpenKeyExW,
+ RegSetKeySecurity,
+};
+use windows::core::{BOOL, HSTRING, PCWSTR, PWSTR};
+
+use crate::error::RegistryError;
+use rust_i18n::t;
+
+const SDDL_REVISION_1: u32 = 1;
+const READ_CONTROL_ACCESS: REG_SAM_FLAGS = REG_SAM_FLAGS(0x0002_0000);
+
+struct RegistryKey(HKEY);
+
+impl Drop for RegistryKey {
+ fn drop(&mut self) {
+ // SAFETY: This handle was returned by RegOpenKeyExW and is owned by this wrapper.
+ unsafe {
+ let _ = RegCloseKey(self.0);
+ }
+ }
+}
+
+struct LocalAllocation(HLOCAL);
+
+impl Drop for LocalAllocation {
+ fn drop(&mut self) {
+ // SAFETY: SDDL conversion APIs allocate this pointer with LocalAlloc.
+ unsafe {
+ let _ = LocalFree(Some(self.0));
+ }
+ }
+}
+
+pub fn get(path: &str, requested_sddl: &str) -> Result {
+ let descriptor = SecurityDescriptor::from_sddl(requested_sddl)?;
+ let (security_information, _) = descriptor.permissions(false)?;
+ let key = open(path, READ_CONTROL_ACCESS)?;
+ let mut size = 0;
+
+ // SAFETY: The first call intentionally supplies no buffer to obtain the required size.
+ let status = unsafe { RegGetKeySecurity(key.0, security_information, None, &mut size) };
+ if status != ERROR_INSUFFICIENT_BUFFER {
+ return Err(win32_error("RegGetKeySecurity", status.0));
+ }
+
+ let mut descriptor = vec![0_u8; size as usize];
+ let security_descriptor = PSECURITY_DESCRIPTOR(descriptor.as_mut_ptr().cast::());
+ // SAFETY: The descriptor buffer is writable and has the size requested by RegGetKeySecurity.
+ let status = unsafe {
+ RegGetKeySecurity(
+ key.0,
+ security_information,
+ Some(security_descriptor),
+ &mut size,
+ )
+ };
+ if status != ERROR_SUCCESS {
+ return Err(win32_error("RegGetKeySecurity", status.0));
+ }
+
+ let mut sddl = PWSTR::null();
+ // SAFETY: The security descriptor was populated by RegGetKeySecurity. The returned string is
+ // released by LocalAllocation.
+ unsafe {
+ ConvertSecurityDescriptorToStringSecurityDescriptorW(
+ security_descriptor,
+ SDDL_REVISION_1,
+ security_information,
+ &mut sddl,
+ None,
+ )
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ }
+ let allocation = LocalAllocation(HLOCAL(sddl.0.cast::()));
+ // SAFETY: The conversion API returned a null-terminated UTF-16 string.
+ let result = unsafe { sddl.to_string() }.map_err(|error| RegistryError::Acl(error.to_string()));
+ drop(allocation);
+ result
+}
+
+pub fn validate(sddl: &str) -> Result<(), RegistryError> {
+ SecurityDescriptor::from_sddl(sddl)?.permissions(false)?;
+ Ok(())
+}
+
+pub fn set(path: &str, sddl: &str) -> Result<(), RegistryError> {
+ let descriptor = SecurityDescriptor::from_sddl(sddl)?;
+ let (security_information, access) = descriptor.permissions(true)?;
+ let key = open(path, access)?;
+
+ // SAFETY: The descriptor was returned by the SDDL conversion API and remains allocated for the
+ // duration of this call.
+ let status =
+ unsafe { RegSetKeySecurity(key.0, security_information, descriptor.descriptor) };
+ if status != ERROR_SUCCESS {
+ return Err(win32_error("RegSetKeySecurity", status.0));
+ }
+
+ Ok(())
+}
+
+pub fn can_set(path: &str, sddl: &str) -> Result {
+ let descriptor = SecurityDescriptor::from_sddl(sddl)?;
+ let (_, access) = descriptor.permissions(true)?;
+ match open(path, access) {
+ Ok(_) => Ok(true),
+ Err(RegistryError::RegistryKeyNotFound(_)) => Ok(false),
+ Err(error) => Err(error),
+ }
+}
+
+struct SecurityDescriptor {
+ descriptor: PSECURITY_DESCRIPTOR,
+ _allocation: LocalAllocation,
+}
+
+impl SecurityDescriptor {
+ fn from_sddl(sddl: &str) -> Result {
+ let sddl = HSTRING::from(sddl);
+ let mut descriptor = PSECURITY_DESCRIPTOR::default();
+
+ // SAFETY: The input is a valid null-terminated HSTRING. The returned descriptor is owned by
+ // the LocalAllocation field.
+ unsafe {
+ ConvertStringSecurityDescriptorToSecurityDescriptorW(
+ PCWSTR(sddl.as_ptr()),
+ SDDL_REVISION_1,
+ &mut descriptor,
+ None,
+ )
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ }
+ Ok(Self {
+ descriptor,
+ _allocation: LocalAllocation(HLOCAL(descriptor.0)),
+ })
+ }
+
+ fn permissions(
+ &self,
+ include_dacl_protection: bool,
+ ) -> Result<(OBJECT_SECURITY_INFORMATION, REG_SAM_FLAGS), RegistryError> {
+ let mut information = OBJECT_SECURITY_INFORMATION(0);
+ let mut access = 0;
+ let mut defaulted = BOOL::default();
+ let mut owner = PSID::default();
+ let mut group = PSID::default();
+ let mut dacl = std::ptr::null_mut::();
+ let mut sacl = std::ptr::null_mut::();
+ let mut dacl_present = BOOL::default();
+ let mut sacl_present = BOOL::default();
+
+ // SAFETY: The descriptor is valid for the lifetime of self, and all output pointers are
+ // writable local variables.
+ unsafe {
+ GetSecurityDescriptorOwner(self.descriptor, &mut owner, &mut defaulted)
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ GetSecurityDescriptorGroup(self.descriptor, &mut group, &mut defaulted)
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ GetSecurityDescriptorDacl(
+ self.descriptor,
+ &mut dacl_present,
+ &mut dacl,
+ &mut defaulted,
+ )
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ GetSecurityDescriptorSacl(
+ self.descriptor,
+ &mut sacl_present,
+ &mut sacl,
+ &mut defaulted,
+ )
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ }
+
+ if sacl_present.as_bool() {
+ return Err(RegistryError::Acl(t!("error.saclUnsupported").to_string()));
+ }
+ if !owner.0.is_null() {
+ information |= OWNER_SECURITY_INFORMATION;
+ access |= 0x0008_0000; // WRITE_OWNER
+ }
+ if !group.0.is_null() {
+ information |= GROUP_SECURITY_INFORMATION;
+ access |= 0x0008_0000; // WRITE_OWNER
+ }
+ if dacl_present.as_bool() {
+ information |= DACL_SECURITY_INFORMATION;
+ access |= 0x0004_0000; // WRITE_DAC
+ if include_dacl_protection {
+ let mut control = 0_u16;
+ let mut revision = 0_u32;
+ // SAFETY: The descriptor is valid and both output pointers are writable.
+ unsafe {
+ GetSecurityDescriptorControl(
+ self.descriptor,
+ &mut control,
+ &mut revision,
+ )
+ .map_err(|error| RegistryError::Acl(error.to_string()))?;
+ }
+ if control & SE_DACL_PROTECTED.0 != 0 {
+ information |= PROTECTED_DACL_SECURITY_INFORMATION;
+ } else {
+ information |= UNPROTECTED_DACL_SECURITY_INFORMATION;
+ }
+ }
+ }
+ if information == OBJECT_SECURITY_INFORMATION(0) {
+ return Err(RegistryError::Acl(t!("error.invalidAcl").to_string()));
+ }
+ Ok((information, REG_SAM_FLAGS(access)))
+ }
+}
+
+fn open(
+ path: &str,
+ access: windows::Win32::System::Registry::REG_SAM_FLAGS,
+) -> Result {
+ let (root, subkey) = split_path(path)?;
+ let subkey = HSTRING::from(subkey);
+ let mut key = HKEY::default();
+
+ // SAFETY: root is a predefined HKEY, subkey is a null-terminated HSTRING, and key is writable.
+ let status = unsafe { RegOpenKeyExW(root, PCWSTR(subkey.as_ptr()), Some(0), access, &mut key) };
+ if status == ERROR_FILE_NOT_FOUND || status == ERROR_PATH_NOT_FOUND {
+ return Err(RegistryError::RegistryKeyNotFound(path.to_string()));
+ }
+ if status != ERROR_SUCCESS {
+ return Err(win32_error("RegOpenKeyExW", status.0));
+ }
+
+ Ok(RegistryKey(key))
+}
+
+fn split_path(path: &str) -> Result<(HKEY, &str), RegistryError> {
+ let (hive, subkey) = path
+ .split_once('\\')
+ .map_or((path, ""), |(hive, subkey)| (hive, subkey));
+ let root = match hive {
+ "HKCC" | "HKEY_CURRENT_CONFIG" => HKEY_CURRENT_CONFIG,
+ "HKCR" | "HKEY_CLASSES_ROOT" => HKEY_CLASSES_ROOT,
+ "HKCU" | "HKEY_CURRENT_USER" => HKEY_CURRENT_USER,
+ "HKLM" | "HKEY_LOCAL_MACHINE" => HKEY_LOCAL_MACHINE,
+ "HKU" | "HKEY_USERS" => HKEY_USERS,
+ _ => return Err(RegistryError::InvalidHive(hive.to_string())),
+ };
+ Ok((root, subkey))
+}
+
+fn win32_error(operation: &str, code: u32) -> RegistryError {
+ RegistryError::Acl(t!("error.win32", operation = operation, code = code).to_string())
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn permissions_detects_requested_sections() {
+ let descriptor = SecurityDescriptor::from_sddl("O:SYG:BAD:(A;;KA;;;SY)").unwrap();
+ let (information, access) = descriptor.permissions(false).unwrap();
+
+ assert_eq!(
+ information.0,
+ OWNER_SECURITY_INFORMATION.0
+ | GROUP_SECURITY_INFORMATION.0
+ | DACL_SECURITY_INFORMATION.0
+ );
+ assert_eq!(access.0, 0x000c_0000);
+ }
+
+ #[test]
+ fn permissions_preserves_dacl_protection() {
+ let descriptor = SecurityDescriptor::from_sddl("D:P(A;;KA;;;SY)").unwrap();
+ let (information, access) = descriptor.permissions(true).unwrap();
+
+ assert_eq!(
+ information,
+ DACL_SECURITY_INFORMATION | PROTECTED_DACL_SECURITY_INFORMATION
+ );
+ assert_eq!(access.0, 0x0004_0000);
+ }
+
+ #[test]
+ fn permissions_ignores_component_markers_inside_conditional_aces() {
+ let descriptor =
+ SecurityDescriptor::from_sddl(r#"D:(XA;;KR;;;WD;(@User.Title == "O:operator"))"#)
+ .unwrap();
+ let (information, access) = descriptor.permissions(false).unwrap();
+
+ assert_eq!(information, DACL_SECURITY_INFORMATION);
+ assert_eq!(access.0, 0x0004_0000);
+ }
+
+ #[test]
+ fn permissions_rejects_sacl() {
+ let descriptor = SecurityDescriptor::from_sddl("S:(AU;SA;KA;;;WD)").unwrap();
+ let error = descriptor.permissions(false).unwrap_err();
+
+ assert!(matches!(error, RegistryError::Acl(_)));
+ }
+
+ #[test]
+ fn validate_rejects_invalid_sddl() {
+ let error = validate("D:not-valid").unwrap_err();
+
+ assert!(matches!(error, RegistryError::Acl(_)));
+ }
+}
diff --git a/lib/dsc-lib-registry/src/config.rs b/lib/dsc-lib-registry/src/config.rs
index e988c3fe2..9ec3f5683 100644
--- a/lib/dsc-lib-registry/src/config.rs
+++ b/lib/dsc-lib-registry/src/config.rs
@@ -30,6 +30,9 @@ pub struct Registry {
/// The data of the registry value.
#[serde(rename = "valueData", skip_serializing_if = "Option::is_none")]
pub value_data: Option,
+ /// The registry key access control list in Security Descriptor Definition Language (SDDL).
+ #[serde(rename = "acl", skip_serializing_if = "Option::is_none")]
+ pub acl: Option,
#[serde(rename = "_exist", skip_serializing_if = "Option::is_none")]
pub exist: Option,
/// Optional path to an offline registry hive file. When specified, operations
diff --git a/lib/dsc-lib-registry/src/error.rs b/lib/dsc-lib-registry/src/error.rs
index b33884ed8..dfdcf974e 100644
--- a/lib/dsc-lib-registry/src/error.rs
+++ b/lib/dsc-lib-registry/src/error.rs
@@ -7,6 +7,9 @@ use thiserror::Error;
#[derive(Error, Debug)]
#[allow(clippy::module_name_repetitions)]
pub enum RegistryError {
+ #[error("{t}: {0}", t = t!("error.acl"))]
+ Acl(String),
+
#[error("{t}: {0}", t = t!("error.invalidHive"))]
InvalidHive(String),
diff --git a/lib/dsc-lib-registry/src/lib.rs b/lib/dsc-lib-registry/src/lib.rs
index f62bc830e..7ac41ac7d 100644
--- a/lib/dsc-lib-registry/src/lib.rs
+++ b/lib/dsc-lib-registry/src/lib.rs
@@ -12,6 +12,7 @@ rust_i18n::i18n!("locales", fallback = "en-us");
pub mod error;
pub mod config;
pub mod offreg;
+mod acl;
use std::ffi::OsStr;
use std::os::windows::ffi::OsStrExt;
@@ -105,6 +106,7 @@ impl RegistryHelper {
key_path: key_path.to_string(),
value_name,
value_data,
+ acl: None,
metadata: None,
exist: None,
registry_file_path: None,
@@ -138,6 +140,28 @@ impl RegistryHelper {
return self.get_offline();
}
+ if self.config.value_name.is_none()
+ && let Some(requested_acl) = &self.config.acl
+ {
+ return match acl::get(&self.config.key_path, requested_acl) {
+ Ok(current_acl) => Ok(Registry {
+ key_path: self.config.key_path.clone(),
+ acl: Some(current_acl),
+ ..Default::default()
+ }),
+ Err(RegistryError::RegistryKeyNotFound(_)) => Ok(Registry {
+ key_path: self.config.key_path.clone(),
+ exist: Some(false),
+ ..Default::default()
+ }),
+ Err(error) => Err(error),
+ };
+ }
+
+ if let Some(requested_acl) = &self.config.acl {
+ acl::validate(requested_acl)?;
+ }
+
let exist: bool;
let (reg_key, _subkey) = match self.open(Security::Read) {
Ok((reg_key, subkey)) => {
@@ -153,6 +177,10 @@ impl RegistryHelper {
},
Err(e) => return Err(e),
};
+ let acl = match &self.config.acl {
+ Some(requested_acl) => Some(acl::get(&self.config.key_path, requested_acl)?),
+ None => None,
+ };
if let Some(value_name) = &self.config.value_name {
let value = match reg_key.value(value_name) {
@@ -162,6 +190,7 @@ impl RegistryHelper {
return Ok(Registry {
key_path: self.config.key_path.clone(),
value_name: Some(value_name.clone()),
+ acl,
exist: Some(exist),
..Default::default()
});
@@ -173,11 +202,13 @@ impl RegistryHelper {
key_path: self.config.key_path.clone(),
value_name: Some(value_name.clone()),
value_data: convert_reg_value(&value)?,
+ acl,
..Default::default()
})
} else {
Ok(Registry {
key_path: self.config.key_path.clone(),
+ acl,
..Default::default()
})
}
@@ -225,10 +256,37 @@ impl RegistryHelper {
/// * `RegistryError` - The error that occurred.
pub fn set(&self) -> Result