Over the years, I’ve written a couple of different blog posts and tools designed to help identify AD objects with broken inheritance–primarily because they intersect with mailbox migrations or AAD Connect–err, Entra Connect Sync–problems,
Today, we’ll dig into some automation I put together to help identify why Entra Connect is failing to write-back to on-premises AD objects.
First things first
You should probably go read some of the longer articles I’ve written on this, such as:
But, for anyone who wants the TL;DR version:
- Active Directory has a concept of protected groups, which are essentially privileged groups whose members are excluded from various permissions inheritance. This prevents, say, a user object that is a member of a privileged group such as Dns Admins from being modified by an account that is less-privileged.
- This list of protected groups is maintained by the directory. Objects that are members of protected groups get a special property (
adminCount) set to1on their object. - After an object is no longer a member of a privileged group, the adminCount property is not cleared or reset, nor is permissions inheritance re-enabled.
This series of events can prohibit Entra Connect object writeback from being able to update on-premises objects.
Second things second, I suppose
Organizations typically go through a cycle of maturity, starting off very immature and improving their policies, procedures, and security over time. One of the most common improvements is implementing dedicated administrative or privileged accounts for administrative and support users. Frequently, this means that daily-driver user accounts are removed from privileged groups in favor of dedicatred privileged accounts.
Knowing what you now know about the object inheritance for protected accounts, you’ll connect the dots that these former administrative accounts are not able to be updated by Entra Connect.
Fortunately, we have some ways to troubleshoot this!
Test-ADProtectedGroupMembership function
I wrote this function (similar to how my Find-and-Fix broken inheritance script earlier works) to identify objects that are members of privileged groups. You can copy it to a notepad window and execute it in a PowerShell window. Then, you can use Entra Connect’s built-in CSExport tool to list all of the errors in your connector and run them through this function to determine if the objects are still members of protected groups.
function Test-ADProtectedGroupMembership {
<# .SYNOPSIS Determines whether an AD user is a member (direct or nested) of a group protected by AdminSDHolder / SDProp. .DESCRIPTION Uses the computed tokenGroups attribute (which expands nested security group membership) and intersects it against the well-known SIDs of the SDProp-protected groups. Also reports the adminCount flag and whether it appears stale (adminCount=1 but no current protected membership). .EXAMPLE Test-ADProtectedGroupMembership -Identity jdoe .NOTES - Schema Admins (RID 518) and Enterprise Admins (RID 519) are rooted in the FOREST ROOT domain SID, not necessarily the user's domain. - The operator groups (Account/Server/Print/Backup Operators) can be excluded from SDProp via the dsHeuristics 16th character; this script assumes default behavior. - krbtgt and the built-in Administrator (RID 500) are protected directly, independent of group membership. #>
[CmdletBinding()]
param(
[Parameter(Mandatory, ValueFromPipeline)]
[string]$Identity
)
process {
$user = Get-ADUser -Identity $Identity -Properties adminCount, nTSecurityDescriptor
# tokenGroups is a constructed attribute: only returned on a BASE-scope
# search, so query the user's DN directly with -SearchScope Base
$tokenGroups = (Get-ADObject -SearchBase $user.DistinguishedName `
-SearchScope Base -Filter * -Properties tokenGroups
).tokenGroups |
ForEach-Object { [System.Security.Principal.SecurityIdentifier]$_ }
$domainSid = (Get-ADDomain).DomainSID.Value
$forestRootSid = (Get-ADDomain -Identity (Get-ADForest).RootDomain).DomainSID.Value
# Well-known SIDs/RIDs of SDProp-protected groups
$protectedSids = @(
'S-1-5-32-544' # Administrators (built-in)
'S-1-5-32-548' # Account Operators
'S-1-5-32-549' # Server Operators
'S-1-5-32-550' # Print Operators
'S-1-5-32-551' # Backup Operators
'S-1-5-32-552' # Replicator
"$domainSid-512" # Domain Admins
"$domainSid-516" # Domain Controllers
"$domainSid-521" # Read-only Domain Controllers
"$domainSid-526" # Key Admins (2016+)
"$forestRootSid-518" # Schema Admins (forest root)
"$forestRootSid-519" # Enterprise Admins (forest root)
"$forestRootSid-527" # Enterprise Key Admins (forest root, 2016+)
)
# tokenGroups (retrieved above via base-scope search) includes all
# nested security groups
$protectedMemberships = $tokenGroups |
Where-Object { $_.Value -in $protectedSids } |
ForEach-Object {
try { (Get-ADGroup -Identity $_.Value).Name }
catch { $_.Value } # fall back to SID if cross-domain lookup fails
}
$isProtected = [bool]$protectedMemberships
$inheritanceOff = $user.nTSecurityDescriptor.AreAccessRulesProtected
[PSCustomObject]@{
User = $user.SamAccountName
IsProtectedMember = $isProtected
ProtectedGroups = $protectedMemberships
AdminCount = $user.adminCount
InheritanceDisabled = $inheritanceOff
StaleAdminCount = (-not $isProtected -and $user.adminCount -eq 1)
}
}
}
Identifying error objects using CSExport
You’ll need to open up the Entra Connect Synchronization Manager and look on the Connectors tab to identify the name of your Active Directory Connector or return the value using Get-ADSyncConnector. Then, you can send the name to CSEXport and then run it through the function above:
$Connector = (Get-ADSyncConnector | ? { $_.Name -notmatch ' - aad' }).Name
"C:\Program Files\Microsoft Azure AD Sync\bin\csexport.exe" $($Connector) C:\Path\To\Output\ErrFile.xml /f:e
[xml]$errors = Get-Content C:\Path\To\Output\ErrFile.xml
[array]$errorobjects = ($errors.'cs-objects'.'cs-object').'account-name'
$errorobjects | Test-ADProtectedGroupMembership | FT -auto

