Back to blog
FILE 0xE6·EVERY READ WORKED. EVERY WRITE FAILED.

Every read worked. Every write failed.

August 31, 2026 · debugging, powershell, active-directory, automation

An offboarding automation of mine came back partly done: the account disable failed, the password rotation failed, and all seven group removals failed. Every one with the same error.

Insufficient access rights to perform the operation

Which is a boring error until you notice the other half of the run. The same script had read the user object, read its memberOf, resolved its DN — all fine. Reads passed. Writes didn't. Nothing in between.

The two theories that were wrong

The obvious suspect is AdminSDHolder. Privileged accounts get their inherited ACLs stripped every hour, and a service account that can edit ordinary users hits a wall on those. Easy to check:

Get-ADUser -Identity someuser -Properties adminCount

adminCount was null. Not protected. And it wouldn't have explained the group failures anyway — removing a member writes to the group object, and "VPN Users" is not a protected group.

Second theory: a read-only domain controller. RODCs hold a read-only replica, so writes against one fail in exactly this shape.

(Get-ADDomainController -Identity $env:COMPUTERNAME).IsReadOnly   # False

Also no. Writable DC, single domain, no trusts. So I stopped theorising and asked the box what it thought it was.

The one line that gave it away

The agent runs as NT AUTHORITY\SYSTEM, and on a domain controller that identity is a local administrator — which on a DC means real rights over the directory:

whoami /groups
# BUILTIN\Administrators
# NT AUTHORITY\Authenticated Users
# ...

So it should have been able to write. Then, on a hunch, I asked which server the AD module was actually talking to:

Get-ADDomainController -Discover -Service ADWS
# DC02

The script was running on DC03.

None of my cmdlets passed -Server. Without it the ActiveDirectory module does its own discovery and binds to whatever DC it likes — here, the one next door. And that changes the identity completely. Locally, SYSTEM is BUILTIN\Administrators. Over the network, SYSTEM authenticates as the machine account, DC03$. A domain controller's machine account can read the entire directory, which is why every read sailed through. It is not a local administrator on another DC, which is why every write came back "insufficient access rights."

Local admin on the box I was standing on. Nobody on the box I was talking to.

Why it had never come up

The same code had run clean at another site three days earlier — ten groups removed, zero failures. Nothing was different about the code. Discovery had simply returned the local machine there.

That's the part worth keeping. The bug wasn't "this doesn't work," it was "this works or doesn't depending on which DC a discovery call happens to return on that host, on that day." A coin flip in a script that reports partial success and moves on. Those don't announce themselves; they show up as an occasional weird ticket somebody re-runs by hand.

The fix

One line, at the top of every script that touches AD:

$adsrv = $env:COMPUTERNAME
try   { $null = Get-ADDomainController -Identity $env:COMPUTERNAME -ErrorAction Stop }
catch { $adsrv = $null }
if ($adsrv) { $PSDefaultParameterValues['*-AD*:Server'] = $adsrv }

$PSDefaultParameterValues with a *-AD* wildcard pins -Server on every AD cmdlet in the script — Set-ADUser, Remove-ADGroupMember, Disable-ADAccount, all of them — without touching a single call site. The probe matters as much as the pin: if the script is running on a member server with RSAT instead of a DC, there's no local identity worth preferring, so $adsrv stays null and the module's own discovery stands. Passing -Server to a machine that doesn't serve LDAP would turn a partial failure into a total one.

Same call that had failed, immediately after:

{"removed":["DuoMFA","Offline Access","Patrol","VPN Users",...],"failed":[],"groups_after":[]}

What I'd do differently

I'd stop treating "insufficient access rights" as a question about permissions. It's two questions — who am I and where am I — and I spent my first twenty minutes entirely on the first one. The tell was sitting there the whole time: reads worked. If your rights were genuinely wrong, you usually can't read either. A clean read/write split means your identity changed somewhere in the middle, and the usual place it changes is a network hop you didn't know you were making.

The test I wrote doesn't check that the pin exists. It checks that the pin appears before the first AD cmdlet in the emitted script, plus a static sweep over the source that fails the build if some future function emits an AD write without it. The next person to add a builder doesn't need to have read this post.