Monitoring Hardened Domain Controllers Without Admin Rights: The Five Permission Layers Nobody Documents
The Mental Model: Five Doors in a Corridor
A remote CIM query from your monitoring host to a DC passes through five successive access checks. Each one produces its own flavor of "access denied", and - this is the trap - the error message rarely tells you which door refused you.
The doors, in order:
- Network logon right - may the account authenticate to this machine over the network at all?
- WinRM root SDDL - may it talk to the WinRM service?
- WMI namespace DACL - may it use the
root\cimv2namespace remotely? - Service Control Manager SDDL - may it enumerate Windows services?
- Per-service security descriptors - may it read this particular service?
Adding the account to Administrators opens all five at once, which is exactly why it "fixes" everything and teaches you nothing.
Layer 1 - Access This Computer from the Network
Before WinRM even looks at your request, Windows checks SeNetworkLogonRight. By default it includes Authenticated Users; hardening baselines restrict it on DCs to a short list (Administrators, ENTERPRISE DOMAIN CONTROLLERS...). Your service account gets turned away at the front door, before any of the layers you actually configured.
The telltale sign lives in the DC security log - reproduce the failure, then look for event 4625 and read its Status field:
Get-WinEvent -FilterHashtable @{ LogName = 'Security'; Id = 4625; StartTime = (Get-Date).AddMinutes(-10) } | Where-Object { $_.Message -match 'svc-monitoring' } | Select-Object -First 2 -ExpandProperty Message
0xC000015B means "the user has not been granted the requested logon type" - this layer, confirmed.
The fix belongs in the hardening GPO, not on the machines. Add your monitoring group to Computer Configuration โ Policies โ Windows Settings โ Security Settings โ Local Policies โ User Rights Assignment โ Access this computer from the network in the GPO applied to the Domain Controllers OU. A local change would be silently reverted at the next policy refresh - a pattern you will meet again in this article.
While you are there, check that the account does not match anything in Deny access to this computer from the network: a Deny wins over everything.
Layer 2 - The WinRM RootSDDL
WinRM gates all remote management through a root security descriptor. On modern Windows Server the default contains (A;;GA;;;RM) - an allow entry for BUILTIN\Remote Management Users, so putting your group in that Builtin group is enough. (Handy fact: DCs have no local groups; their "local" groups are the domain Builtin groups, so one membership covers every DC.)
Hardened or upgraded-in-place systems, however, sometimes carry a stripped RootSDDL. Check yours:
Invoke-Command -ComputerName $dcs -ScriptBlock { (Get-Item WSMan:\localhost\Service\RootSDDL).Value }
If you see something like O:NSG:BAD:P(A;;GA;;;BA)(A;;GR;;;IU)S:... - Administrators and Interactive only, no RM - that is your blocker.
Insert an ACE for your group's SID at the end of the DACL, just before the S: (SACL) marker:
$sid = (Get-ADGroup 'GRP-AD-Monitoring').SID.Value
Invoke-Command -ComputerName $dcs -ScriptBlock {
param ($sid)
$current = (Get-Item WSMan:\localhost\Service\RootSDDL).Value
if ($current -match [regex]::Escape(";;;$sid)")) { return 'Already present' }
Set-Content "C:\Windows\Temp\RootSDDL_backup_$(Get-Date -Format yyyyMMdd_HHmmss).txt" -Value $current
$ace = "(A;;GA;;;$sid)"
$idx = $current.IndexOf('S:')
$new = if ($idx -ge 0) { $current.Insert($idx, $ace) } else { $current + $ace }
Set-Item WSMan:\localhost\Service\RootSDDL -Value $new -Force
'Updated'
} -ArgumentList $sid
(Why String.Insert instead of a regex replace: it behaves identically whether or not a SACL exists, and cannot silently do nothing. SDDL strings punish creativity.)
Same GPO caveat as layer 1: if a baseline manages this value, codify the change in the baseline or watch it evaporate every 90-120 minutes.
Layer 3 - The WMI Namespace DACL
The layer everyone knows about, with two classic mistakes. Remote CIM access to root\cimv2 requires two rights on the namespace: Enable Account (0x1) and Remote Enable (0x20). The wmimgmt.msc UI makes it very easy to tick only Remote Enable - which yields the signature symptom "WinRM accepts the connection, then WMI returns access denied".
The second mistake is setting the rights on root instead of root\cimv2 with "this namespace only" scope, which does not propagate.
Do not trust the UI; read what the DC actually enforces:
Invoke-Command -ComputerName $dcs -ScriptBlock {
(Invoke-WmiMethod -Namespace 'root/cimv2' -Path '__SystemSecurity=@' -Name GetSecurityDescriptor).Descriptor.DACL |
ForEach-Object {
[PSCustomObject]@{
Trustee = '{0}\{1}' -f $_.Trustee.Domain, $_.Trustee.Name
EnableAccount = [bool]($_.AccessMask -band 0x1)
RemoteEnable = [bool]($_.AccessMask -band 0x20)
}
}
}
Two implementation notes that cost me hours. First, invoke GetSecurityDescriptor / SetSecurityDescriptor through Invoke-WmiMethod on the __SystemSecurity=@ singleton path - calling the method directly on the object returned by Get-WmiObject fails on some systems with "does not contain a method named GetSecurityDescriptor". Second, an ACE with mask 0x21, AceType 0, no inheritance is all you need; the account stays strictly read-only.
An idempotent grant script (with -WhatIf and rollback) ships in the ADEHM repo under Tools/Grant-ADEHMWmiPermission.ps1.
Layer 4 - The Service Control Manager SDDL
Here is where hardened systems diverge sharply from lab machines. Your OS query now works (Win32_OperatingSystem returns data), but SELECT * FROM Win32_Service fails with access denied. The WMI service provider impersonates the caller and asks the Service Control Manager to enumerate services - and the SCM's own security descriptor, on hardened (and even many default) systems, does not grant enumeration to remote non-admin callers.
Check and fix, granting the same read set Windows gives interactive users (CCLCRPRC: connect, enumerate, query status, read control - no start, stop or modify):
$sid = (Get-ADGroup 'GRP-AD-Monitoring').SID.Value
Invoke-Command -ComputerName $dcs -ScriptBlock {
param ($sid)
$current = ((& sc.exe sdshow scmanager) | Where-Object { $_ -match '\S' }) -join ''
if ($current -match [regex]::Escape(";;;$sid)")) { return 'Already present' }
Set-Content "C:\Windows\Temp\scmanager_sddl_backup_$(Get-Date -Format yyyyMMdd_HHmmss).txt" -Value $current
$ace = "(A;;CCLCRPRC;;;$sid)"
$idx = $current.IndexOf('S:')
$new = if ($idx -ge 0) { $current.Insert($idx, $ace) } else { $current + $ace }
"$(& sc.exe sdset scmanager $new)"
} -ArgumentList $sid
Layer 5 - Per-Service Security Descriptors
The subtlest one, because it fails silently. With the SCM open, enumeration succeeds - but each service then filters the caller through its own security descriptor. A service whose SD does not grant read to your account is not reported as denied; it is simply absent from the results. Your query for NTDS returns nothing, your monitoring tool concludes the service does not exist, and no error is raised anywhere.
I only caught this because my diagnostic script printed OK (NTDS = ) - an empty state - on two DCs while a third returned Running. Same domain, same baseline on paper; per-service SDs had drifted.
The fix mirrors layer 4, per service, with the interactive-users read set (CCLCSWLOCRRC):
& sc.exe sdshow NTDS # inspect
# insert (A;;CCLCSWLOCRRC;;;<SID>) before S: and apply with:
sc.exe sdset NTDS <new-sddl>
For a whole list of monitored services across all DCs, idempotent and with backups, see Tools/Grant-ADEHMServiceReadPermission.ps1 in the repo.
Three Operational Rules That Will Save You a Day Each
Restart WinRM after any ACL change. WinRM keeps per-user host processes (WsmProvHost.exe) alive between sessions. A stale host process serves your requests with pre-change access state - meaning a correct fix keeps failing for hours, until the process idles out. I watched identical client PIDs in the WMI log across a 40-minute debugging window and chased ghosts the whole time. Restart-Service WinRM on the DCs after ACL work; the impact is a brief drop of active remote-management sessions, nothing more.
root\interop denials are a red herring. Once you enable the WMI-Activity log analysis (below), you will see 0x80041003 denials for connect to namespace: root\interop on every CIM session - including fully working ones. The WinRM WMI plugin probes that namespace and shrugs off the refusal. I initially granted rights on it; field testing proved it unnecessary. Least privilege says: do not grant what is not needed. Ignore these entries.
Test with explicit credentials, never with runas. Hardening baselines deny service accounts interactive logon - runas fails with error 1385, and that is correct behavior, not a bug to fix. Test the way your tool will actually run: New-CimSession -Credential, and Test-WSMan -Credential -Authentication Default (without -Authentication Default, Test-WSMan sends an unauthenticated probe that succeeds for any account and proves nothing). For scheduled tasks, the batch logon type applies: grant Log on as a batch job on the execution host only - never on the DCs.
The Diagnostic That Ends the Guessing
The single most valuable tool in this whole journey is the DC-side WMI-Activity operational log, because it names the refusal instead of letting you infer it:
Get-WinEvent -LogName 'Microsoft-Windows-WMI-Activity/Operational' -MaxEvents 50 |
Where-Object { $_.Message -match '0x80041003' } |
Select-Object TimeCreated, Message
Each denial entry carries User, Namespace and Operation - the exact identity refused, on the exact object, doing the exact thing. One look at Operation = connect to namespace: root\cimv2 versus Start IWbemServices::ExecQuery ... Win32_Service tells you whether you are fighting layer 3 or layer 4.
Combine it with the layered functional test (WinRM auth โ CIM session โ OS read โ service read) and the 4625 status codes from layer 1, and no failure mode in this stack can hide from you.
The full one-shot diagnostic script - functional chain, namespace DACLs, WMI denials and SCM SDDL captured at the same instant - is in the repo as Tools/Test-ADEHMPermission.ps1.
Closing Thought
None of these five layers is exotic. Each is a legitimate, documented-somewhere Windows security boundary doing its job. What is missing from the ecosystem is the map - the knowledge that they stack, the order they trip in, and the misleading symptoms each produces. That gap is why so many monitoring service accounts end up in Domain Admins.
The complete tooling (grant scripts, rollback, one-shot diagnostic) lives in the ADEHM repository, MIT licensed: https://github.com/oussangelo/ADEHM - where the tool itself gives you agentless, read-only health monitoring of your DCs as a daily HTML report. If your environment trips a sixth door I have not met, open an issue; the map only gets better with more territory.
Comments
No comments yet. Start the discussion.