If your Entra Connect (Azure AD Connect) sync cycle ends every 30 minutes with completed-export-errors on the AD connector, and the failing objects are all administrators, the cause is AdminSDHolder -- and Microsoft's documented fix for it, Set-ADSyncExchangeHybridPermissions -IncludeAdminSDHolders, will appear to succeed without fixing anything. The ACEs it writes are scoped to descendant objects. A user is a leaf object with no children, so those ACEs are inert the moment SDProp stamps them onto the protected account. You need an object-scoped ACE instead.
We hit this on a live estate this week, on a hybrid Exchange tenant where writeback had been failing silently since July -- roughly 10,500 retries across three accounts, with nobody aware, because the cloud-direction sync was never affected.
The symptom
Every sync cycle looked like this:
example.com (AD connector) Export -> completed-export-errors
AAD connector Export -> success
AAD connector Delta Synchronization -> success
example.com (AD connector) Delta Synchronization -> success
Only the AD connector export fails. That is the writeback direction -- Entra writing Exchange hybrid attributes back down to on-premises AD. Because the AAD-direction export kept succeeding, normal sync worked perfectly: new users appeared, disables propagated, licences applied. Nothing user-visible broke, which is exactly why it ran for three months unnoticed.
Per-object detail:
ErrorType : permission-issue
ErrorCode : 8344
ErrorLiteral : Insufficient access rights to perform the operation.
RetryCount : 5210
Why only the administrators
Every failing object had adminCount=1.
That attribute marks an account as AdminSDHolder-protected. Membership of Domain Admins, Administrators, Enterprise Admins and friends gets you flagged, and from then on:
- ACL inheritance is disabled on the object, and
- a background task called SDProp overwrites the object's DACL roughly every 60 minutes with a copy of the DACL on
CN=AdminSDHolder,CN=System,DC=example,DC=com.
This is a security feature. It exists so that delegated rights granted further up the tree cannot quietly accumulate on privileged accounts.
The consequence here: the AD Connect service account held Full Control at the domain root -- and it still could not write to three user objects, because protected objects do not inherit from the domain root. Permissions for those accounts come from AdminSDHolder and nowhere else.
That is also a useful general diagnostic. If an account has broad delegated rights that work on 500 users and fail on 3, check adminCount on the three.
The trap
Microsoft documents the remediation, and it is the right cmdlet:
Set-ADSyncExchangeHybridPermissions -ADConnectorAccountDN <dn> -IncludeAdminSDHolders
It runs clean. It reports success. It adds four ACEs to the AdminSDHolder DACL. SDProp duly copies them onto every protected account. Inspect the protected user afterwards and the permissions are visibly, verifiably there.
The export still fails with 8344.
Here is what those ACEs actually look like once they land on the user object:
rights : ReadProperty, WriteProperty
objectType : 00000000-0000-0000-0000-000000000000
inheritedObjType : bf967aba-0de6-11d0-a285-00aa003049e2 (user)
inheritance : Descendents
isInherited : False
inheritance : Descendents means this ACE applies to child objects of the container, not to the object carrying it. The cmdlet is built for the normal case: you point it at an OU, and the ACEs flow down to the user objects inside it. That works perfectly.
AdminSDHolder is not an OU. It is a template. SDProp does not re-evaluate inheritance semantics -- it copies the DACL verbatim onto each protected object. So a user object ends up holding four ACEs that grant rights to its children. A user object has no children. The ACEs do nothing at all.
The permissions are present. They are also meaningless. That combination is what makes this so slow to diagnose: every check you run says the fix is applied.
How to check whether you are affected
On a domain controller:
# 1. Are the failing objects protected?
Get-ADUser <sam> -Properties adminCount | Select-Object SamAccountName, adminCount
# 2. What does the connector account actually have on them?
$sid = (Get-ADUser '<MSOL_account>' -Properties objectSid).objectSid.Value
(Get-Acl "AD:\CN=<user>,CN=Users,DC=example,DC=com").Access |
Where-Object { $_.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value -eq $sid } |
Select-Object ActiveDirectoryRights, InheritanceType, IsInherited
If every returned ACE says InheritanceType : Descendents, the account has no effective rights on that object, regardless of how good the list looks.
One gotcha while testing: IdentityReference is an NTAccount (DOMAIN\user), not a SID string. Comparing it directly against a SID silently matches nothing and makes it look like there are no ACEs at all. Translate it first -- as above.
The fix
Add an ACE to AdminSDHolder that is scoped to the object itself (InheritanceType = None), then let SDProp propagate it:
$sid = New-Object Security.Principal.SecurityIdentifier(
(Get-ADUser '<MSOL_account>' -Properties objectSid).objectSid.Value)
$asdhDN = 'CN=AdminSDHolder,CN=System,' + (Get-ADDomain).DistinguishedName
$path = 'AD:\' + $asdhDN
$acl = Get-Acl $path
$ace = New-Object DirectoryServices.ActiveDirectoryAccessRule(
$sid,
[DirectoryServices.ActiveDirectoryRights]'ReadProperty, WriteProperty',
[Security.AccessControl.AccessControlType]::Allow,
[DirectoryServices.ActiveDirectorySecurityInheritance]::None)
$acl.AddAccessRule($ace)
Set-Acl -Path $path -AclObject $acl
Then trigger SDProp rather than waiting up to an hour:
$root = [ADSI]"LDAP://RootDSE"
$root.Put("runProtectAdminGroupsTask", 1)
$root.SetInfo()
Give it 30 seconds, confirm the protected users now carry an ACE with InheritanceType : None, and run a delta sync:
Start-ADSyncSyncCycle -PolicyType Delta
Result on our estate, first cycle after the change:
AD connector Export : completed-export-errors -> success
objects with export errors : 3 -> 0
First clean AD-connector export since July.
Worth knowing before you do it
This grants the sync account read/write on all attributes of your protected admin accounts. That is the same outcome Microsoft's own cmdlet intends, and in our case the account already held Full Control at the domain root, so it was not a meaningful expansion. Think about it anyway: if that service account is compromised, attribute writes on Domain Admin objects are a genuine escalation path -- servicePrincipalName for Kerberoasting, or msDS-AllowedToActOnBehalfOfOtherIdentity for delegation abuse.
A narrower version is possible: grant only the Exchange hybrid attribute set rather than all properties. We did not, because error 8344 does not name the attribute it failed on, so narrowing becomes guesswork with an hour-long SDProp feedback loop on each attempt. If you have a maintenance window and patience, it is the better end state.
The four inert Descendents ACEs can be left alone. They are harmless and they are the cmdlet's standard output, so removing them just means the next person to run the documented fix puts them back.
Also: if you are about to decommission your last on-premises Exchange server, fix this first. Hybrid writeback that silently does not work is much harder to reason about mid-teardown than it is beforehand.
The broader lesson
"Permission exists" and "permission applies" are different claims, and Windows ACL tooling is happy to show you the first while you assume the second. Any time a permission is visibly present and the operation still returns access denied, go and read the inheritance flags before you go looking for a deeper cause.
This is the sort of thing that comes out of running real estates rather than reading documentation -- the cmdlet is correct, the docs are correct, and the outcome is still wrong for one specific object class. If you want this sort of problem found before it becomes a three-month silent failure, that is the job in our managed IT service, and hybrid identity specifically is covered under how we run Microsoft 365.