Changing the Active Directory schema is one of the few things in AD you can't undo. You can't delete what you add. Rolling back means restoring every domain controller in the forest. That's why most admins never touch it, and why the ones who do are careful.
I wanted to understand it properly, so I did it in my own lab: one custom attribute, one auxiliary class, linked to the user class, replicated to the second DC, then a deactivation drill. It follows the Contoso in-house app scenario in Unit 6 of the Microsoft Learn module Manage AD DS domain controllers and FSMO roles.
It didn't go to plan, which is why this post is worth reading. The very first health check found that one DC hadn't received changes from the Schema Master for three days. After I fixed that, I made four small mistakes during the extension itself. They're all here, with the error and the fix, because those are the parts that will save you time.
Why you'd ever extend the schema
The schema is the rulebook for the whole forest. It defines which object types exist (classes) and which properties they can hold (attributes). Out of the box, a user has fields like givenName, mail and department. If an application needs to store something AD doesn't know about, the schema has to learn it first.
You'll meet this in real life when:
- An in-house app needs its own field on user accounts, like the badge ID in this lab.
-
Exchange runs
/PrepareSchemabefore installation. -
Windows LAPS runs
Update-LapsADSchemato store local admin passwords on computer objects. -
Configuration Manager runs
extadsch.exeso clients can find their site in AD.
Those vendor tools do exactly what this lab does by hand, just with their own registered OIDs. Doing it once yourself makes those installs a lot less mysterious, and it gives you something concrete to talk about in an interview.
Goal and environment
In about 60 minutes you will extend the forest schema with one custom attribute, watch it replicate to DC02, and learn why schema changes are treated as permanent. This mirrors Contoso's in-house app scenario from Unit 6.
| Item | Value in this lab |
|---|---|
| DC01 | Windows Server 2022, Azure VM, assumed Schema Master |
| DC02 | Second DC, Hyper-V VM inside the Azure lab |
| Account | Your lab domain admin (forest root domain) |
| Test user | Any lab user, called testuser below |
| Naming prefix |
lab- / lab so every test object is easy to spot |
Prerequisites:
- Both DCs healthy and replicating (checked in Task 1)
- Run everything on DC01 from an elevated PowerShell unless a step says otherwise
- Take an Azure snapshot of DC01 and a Hyper-V checkpoint of DC02 before Task 3, as a last resort only
Safety: schema changes replicate to the whole forest within minutes and cannot be deleted. Rolling back means restoring every DC, which is why real companies test in a separate lab forest first. Use only the lab- names in this guide.
How to read this post: each task has two parts. First the steps, written so you can follow them in your own lab. Then my exercise notes: what actually happened when I ran them, including the errors.
Task 1 — Baseline: Schema Master, version, replication health
Before you change something that can't be undone, you need to know the starting point. Three questions: which DC can accept the change, what version the schema is at now, and whether the change will actually reach every DC.
Record these values before changing anything; they are your "before" screenshot for the blog post.
# Who holds the Schema Master role?
Get-ADForest | Select-Object Name, SchemaMaster, ForestMode
netdom query fsmo
# Schema version on each DC (expect 88 for Server 2019/2022)
$schemaNC = (Get-ADRootDSE).schemaNamingContext
Get-ADObject $schemaNC -Property objectVersion -Server DC01 | Select objectVersion
Get-ADObject $schemaNC -Property objectVersion -Server DC02 | Select objectVersion
# Replication must be clean before a schema change
repadmin /replsummary
repadmin /showrepl DC02
Expected result: both DCs report the same objectVersion, and /replsummary shows 0 failures. If DC02 shows errors, fix replication first. A schema change on a broken topology is a classic real-world outage.
If the Schema Master is not DC01, run the later tasks on whichever DC holds it.
Task 1 exercise notes: DC02 → DC01 replication failure (error 8524), 27 Oct 2026
I expected this step to take two minutes. It found two surprises.
The first was that DC01 wasn't the Schema Master. I'd moved the role to DC02 earlier while troubleshooting a failed FSMO transfer (that's a separate post), and the guide still assumed DC01.
The second was a replication failure in exactly the direction that mattered.
Result: the baseline check found that DC01 had not received changes from DC02, the Schema Master, since 24 Oct. It was fixed by forcing a pull once DNS resolved correctly. Both DCs ended with 0 failures, so it is now safe to continue with Task 3.
Baseline values recorded
| Item | Value |
|---|---|
| Forest / mode | lab.local / Windows2016Forest |
| Schema Master | DC02 (not DC01 as the guide assumed) |
| Domain naming, PDC, RID, Infrastructure | DC01 |
| Schema objectVersion | 88 on DC01 and DC02 |
| DC01 IPs | 10.0.0.4 (Azure NIC), 192.168.10.1 (vEthernet LabNetwork) |
| DC02 IP | 192.168.10.10 (nested Hyper-V VM) |
Symptom
repadmin /replsummary
Source DSA largest delta fails/total error
DC01 20m:45s 0 / 5
DC02 05d.21h:29m:27s 2 / 5 (8524) DNS lookup failure
Direction that failed: DC01 pulling from DC02. DC02 pulling from DC01 was healthy. With DC02 as Schema Master, any schema change would never have reached DC01.
Why this would have hurt: if I'd skipped the baseline and gone straight to Task 3, the new attribute would have been created on DC02 and simply never arrived on DC01. Nothing would have warned me. I'd have been debugging the attribute when the real problem was replication.
Investigation
Error 8524 says "DNS lookup failure", so I started with DNS, then checked ports, then the per-partition status.
| Check | Command | Result |
|---|---|---|
| DNS servers per DC | ipconfig /all | findstr /i "IPv4 DNS" |
DC01 points only to itself on both adapters |
| DC02 host record | Resolve-DnsName DC02.lab.local |
192.168.10.10 ✅ |
| DC02 replication CNAME | Resolve-DnsName "<DC02 NTDS GUID>._msdcs.lab.local" |
→ dc02.lab.local → 192.168.10.10 ✅ |
| DNS health, DC01 | dcdiag /test:dns /s:DC01 /v |
Passed; warning: dynamic IP on Azure NIC |
| DNS health, DC02 | dcdiag /test:dns /s:DC02 /v |
All PASS |
| Ports DC01 → DC02 |
Test-NetConnection 192.168.10.10 -Port 135 / 389
|
True / True |
| Ports DC02 → DC01 | Test-NetConnection 10.0.0.4 -Port 389 |
True |
| DC02 network profile | Get-NetConnectionProfile |
DomainAuthenticated ✅ |
| Per-partition status | repadmin /showrepl DC01 |
Only ForestDnsZones still failing; last success 2026-10-24 08:35 |
Everything passed, yet the error was still there. That meant either DNS had been broken earlier and recovered, or I was checking the wrong name. So I checked the name replication actually uses.
Key check: how DC01 finds DC02 for replication (the GUID CNAME)
Replication does not look up DC02.lab.local directly. It looks up DC02 by its DSA GUID, so DC01 can resolve DC02's host name and still fail with 8524.
The lookup chain:
- Each DC has an NTDS Settings object in AD. Its
objectGUIDis the DC's DSA GUID, the same valuerepadmin /showreplprints as "DSA object GUID". - Netlogon on each DC registers a CNAME in DNS:
<DSA GUID>._msdcs.<forest root>→ the DC's host name. - The replicating DC resolves that CNAME, then the host (A) record, then connects over RPC.
- If step 2 or 3 fails, the result is 8524: "DSA operation is unable to proceed because of a DNS lookup failure".
# 1. Find the DN of DC02's NTDS Settings object
$ntds = (Get-ADDomainController DC02).NTDSSettingsObjectDN
# 2. Read its objectGUID = DC02's DSA GUID
$guid = (Get-ADObject $ntds -Properties objectGUID).objectGUID
# 3. Resolve the exact name replication uses
Resolve-DnsName "$guid._msdcs.lab.local"
Output on DC01:
Name Type TTL Section NameHost
---- ---- --- ------- --------
3747608c-0a43-46b9-883d-aa90e8672f5d._msdcs.lab.local CNAME 600 Answer dc02.lab.local
How to read it:
- The GUID
3747608c-0a43-46b9-883d-aa90e8672f5dmatches "DSA object GUID" for DC02 inrepadmin /showrepl DC01, so it's the right record. - CNAME →
dc02.lab.local→ A record 192.168.10.10, so the full chain resolves. DNS was not failing at check time. - The
_msdcs.lab.localzone is normally stored in the ForestDnsZones partition, the same partition that kept failing. The records DC01 needed and the replication that delivers them depended on each other.
If the CNAME were missing, the fix on DC02 would be:
nltest /dsregdns # re-register DC DNS records
Restart-Service Netlogon
Then check DNS Manager on DC01 → _msdcs.lab.local for the GUID record, and run Resolve-DnsName again.
Cause
When the failures happened, DC01 could not resolve DC02's replication name (<GUID>._msdcs.lab.local). By the time of the check, DNS resolved correctly, but the failure stayed in the report because no new pull had run for ForestDnsZones. Likely trigger: DC02's DNS records only reach DC01 through replication from DC02, which was the broken link (a DNS "island" loop).
In plain words: DC01 needed DC02's address to replicate from DC02, but that address only arrives on DC01 by replicating from DC02. Once that loop breaks, it can stay broken until you force a fresh attempt, even after the underlying DNS is fine again.
Fix
- Confirmed DNS and ports were healthy (investigation table above).
-
Forced a pull of each partition from DC02 into DC01:
repadmin /replicate DC01 DC02 "CN=Schema,CN=Configuration,DC=lab,DC=local" repadmin /replicate DC01 DC02 "DC=ForestDnsZones,DC=lab,DC=local" repadmin /syncall DC01 /Ade # pull into DC01; no /P repadmin /replsummary
Final result
repadmin /replsummary (2026-10-27 21:09)
Source DSA largest delta fails/total
DC01 14m:06s 0 / 5
DC02 :17s 0 / 5
Destination DSA
DC01 :17s 0 / 5
DC02 14m:06s 0 / 5
Lessons learned
- Always run Task 1 before a schema change. Replication from the Schema Master must be clean.
- Read
/replsummaryby direction: the row shows which DC can't pull from which. -
repadmin /showrepl <DC>shows the failing partition and its last success time;/replsummaryalone hides that. - An error in
/replsummarycan be old. Fix the cause, then force a new attempt before trusting the result. -
repadmin /syncall <DC> /AdePpushes out from that DC. Without/P, it pulls into it. Pick based on which DC is behind. -
Restart-Service NlaSvc -Forcefails on Server 2022 (dependent service netprofm). It's harmless; reboot instead if the network profile is wrong. - Find the real Schema Master first (
netdom query fsmo) and aimldifde -sat it. In this lab that was DC02 on 27 Oct.
Follow-ups for the lab environment
- ☐ DC01 is on two networks and registers both IPs in DNS. Decide which IP DCs should use.
- ☐ Set DC01's Azure NIC private IP to Static in the Azure portal (fixes the dcdiag dynamic-IP warning).
- ☐ DNS client order on each DC: partner DC first, 127.0.0.1 second.
- ☐ Update Tasks 3–5 to use
-s DC02. (Superseded on 28 Oct: the Schema Master moved back to DC01, so the tasks below target DC01.)
Task 2 — Tools and temporary Schema Admins access
Schema Admins is one of the most powerful groups in AD: its members can change the rules for the whole forest. So the habit is to keep it empty and only join it for the change window, like taking a key out of a safe and putting it back.
Best practice is to keep Schema Admins empty and add yourself only for the change window. You remove the membership again in Task 5.
- Register the hidden Schema snap-in:
regsvr32 schmmgmt.dll - Run
mmc, then File → Add/Remove Snap-in → Active Directory Schema. Save it asSchema.mscon the desktop. - Add your account to the group:
Add-ADGroupMember -Identity "Schema Admins" -Members "<your admin account>"
- Sign out and back in, because group membership only lands in a new logon token.
- Verify:
whoami /groups | findstr /i schemashould listSchema Admins.
Interview point: membership alone is not enough. The change must also be written on the Schema Master, and the snap-in connects there automatically.
Task 3 — Create the attribute and an auxiliary class (LDIF)
This is the actual change. Two new schema objects:
- an attribute,
labEmployeeBadgeID: the new field, a short text value - an auxiliary class,
labUserExtension: a small add-on that carries the attribute and can be attached to an existing class later
Each one needs a globally unique ID (an OID), the same way every built-in attribute has one.
Every schema object needs a globally unique OID. Real vendors register their own; for a lab, generate one under Microsoft's private branch with this well-known script.
$Prefix = "1.2.840.113556.1.8000.2554"
$GUID = [System.Guid]::NewGuid().ToString()
$Parts = @()
$Parts += [UInt64]::Parse($GUID.Substring(0,4), "AllowHexSpecifier")
$Parts += [UInt64]::Parse($GUID.Substring(4,4), "AllowHexSpecifier")
$Parts += [UInt64]::Parse($GUID.Substring(9,4), "AllowHexSpecifier")
$Parts += [UInt64]::Parse($GUID.Substring(14,4), "AllowHexSpecifier")
$Parts += [UInt64]::Parse($GUID.Substring(19,4), "AllowHexSpecifier")
$Parts += [UInt64]::Parse($GUID.Substring(24,6), "AllowHexSpecifier")
$Parts += [UInt64]::Parse($GUID.Substring(30,6), "AllowHexSpecifier")
$BaseOID = "{0}.{1}.{2}.{3}.{4}.{5}.{6}.{7}" -f $Prefix, $Parts[0], $Parts[1], $Parts[2], $Parts[3], $Parts[4], $Parts[5], $Parts[6]
$BaseOID # save this value in your lab notes
$BaseOID is your own private OID "root", for example 1.2.840.113556.1.8000.2554.41250.12011.17862.38207.53142.1458291.7712345. You never use it alone. You hang numbered branches under it, like folders:
| OID | Used for |
|---|---|
<BASE>.1 |
branch for attributes |
<BASE>.1.1 |
first attribute: labEmployeeBadgeID
|
<BASE>.1.2 |
second attribute: lab-TestDefunct (Task 5 drill) |
<BASE>.2 |
branch for classes |
<BASE>.2.1 |
first class: labUserExtension
|
Run the next steps in the same PowerShell window where you generated $BaseOID.
Step 1. Save the base OID. The variable is lost when the window closes, and you need the same base for every future lab object.
New-Item -ItemType Directory -Path C:\Lab -Force | Out-Null
$BaseOID | Out-File C:\Lab\BaseOID.txt -Encoding ascii
# In a later session, load it back instead of generating a new one:
# $BaseOID = (Get-Content C:\Lab\BaseOID.txt).Trim()
Step 2. Build the two OIDs.
$AttrOID = "$BaseOID.1.1"
$ClassOID = "$BaseOID.2.1"
$AttrOID
$ClassOID
Step 3. Write the LDF with the OIDs already filled in. The double-quoted here-string replaces $AttrOID and $ClassOID with their values, so there is no manual editing.
$ldf = @"
dn: CN=lab-EmployeeBadgeID,DC=X
changetype: add
objectClass: attributeSchema
lDAPDisplayName: labEmployeeBadgeID
adminDisplayName: lab-EmployeeBadgeID
adminDescription: Lab test attribute - employee badge ID
attributeID: $AttrOID
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
rangeUpper: 32
searchFlags: 0
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
dn: CN=lab-UserExtension,DC=X
changetype: add
objectClass: classSchema
lDAPDisplayName: labUserExtension
adminDisplayName: lab-UserExtension
adminDescription: Lab auxiliary class for user objects
governsID: $ClassOID
objectClassCategory: 3
subClassOf: top
mayContain: labEmployeeBadgeID
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
"@
$ldf | Out-File C:\Lab\schema-lab.ldf -Encoding ascii
Step 4. Check the file before importing.
Get-Content C:\Lab\schema-lab.ldf
Select-String -Path C:\Lab\schema-lab.ldf -Pattern "attributeID|governsID"
Both lines must show your full OIDs ending in .1.1 and .2.1. If either shows an empty value, $BaseOID was not set in this window. Load it from BaseOID.txt and repeat Steps 2–3.
For reference, the file you just generated looks like this, with <BASE> replaced by your value:
dn: CN=lab-EmployeeBadgeID,DC=X
changetype: add
objectClass: attributeSchema
lDAPDisplayName: labEmployeeBadgeID
adminDisplayName: lab-EmployeeBadgeID
adminDescription: Lab test attribute - employee badge ID
attributeID: <BASE>.1.1
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
rangeUpper: 32
searchFlags: 0
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
dn: CN=lab-UserExtension,DC=X
changetype: add
objectClass: classSchema
lDAPDisplayName: labUserExtension
adminDisplayName: lab-UserExtension
adminDescription: Lab auxiliary class for user objects
governsID: <BASE>.2.1
objectClassCategory: 3
subClassOf: top
mayContain: labEmployeeBadgeID
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
Import it on the Schema Master. -c swaps the DC=X placeholder for your real schema partition:
# Step 5. Import on the current Schema Master (DC01 since 28 Oct; check with: netdom query fsmo)
ldifde -i -f C:\Lab\schema-lab.ldf -s DC01 -c "DC=X" "#schemaNamingContext" -v -j C:\Lab
What the key lines mean:
-
attributeSyntax 2.5.5.12+oMSyntax 64= Unicode string, capped at 32 characters byrangeUpper -
objectClassCategory: 3= auxiliary class (1 = structural, 2 = abstract) - the empty
dn:blocks writeschemaUpdateNowto RootDSE, so the next entry sees the new attribute
Check the result in the Schema snap-in under Attributes and Classes. Don't use -k, since a silent failure is exactly what you want to see in a lab.
Task 4 — Link the class to user, refresh the schema, use the attribute
Creating the attribute isn't enough on its own: no object can use it yet. Instead of redefining the built-in user class, you attach the auxiliary class to it. Every user then gains the new field, and nothing Microsoft shipped gets changed.
This is the Unit 6 pattern: the structural class user gains new attributes through an auxiliary class, without being redefined. Save as C:\Lab\schema-link.ldf:
dn: CN=User,DC=X
changetype: modify
add: auxiliaryClass
auxiliaryClass: labUserExtension
-
ldifde -i -f C:\Lab\schema-link.ldf -s DC01 -c "DC=X" "#schemaNamingContext" -v -j C:\Lab
Force a schema reload the way the unit shows (the knowledge-check answer):
- Run
adsiedit.msc, then Connect to → Schema - Right-click the Schema container → Update Schema Now
Open a new PowerShell window (the AD module caches the schema per session), then write and read the value:
Set-ADUser testuser -Replace @{labEmployeeBadgeID = "B-1001"}
Get-ADUser testuser -Properties labEmployeeBadgeID | Select Name, labEmployeeBadgeID
# LDAP filter view, the way an application would query it
Get-ADUser -LDAPFilter "(labEmployeeBadgeID=B-*)" -Properties labEmployeeBadgeID
In ADUC, turn on View → Advanced Features and open testuser → Attribute Editor. labEmployeeBadgeID now shows next to built-in attributes like givenName.
Try one invalid write too: a 40-character value should be rejected because of rangeUpper: 32. That is the schema enforcing data integrity, the point of the whole unit.
Task 5 — Verify on DC02, practise deactivation, clean up
The change was made on one DC. It's only finished when every DC has it, and when the temporary access from Task 2 is gone again.
Replication check. The change started on the Schema Master and must now exist on DC02:
repadmin /syncall DC01 /AdeP
$schemaNC = (Get-ADRootDSE).schemaNamingContext
Get-ADObject -Server DC02 -SearchBase $schemaNC -Filter "lDAPDisplayName -eq 'labEmployeeBadgeID'"
Get-ADUser testuser -Server DC02 -Properties labEmployeeBadgeID | Select labEmployeeBadgeID
Note that objectVersion from Task 1 does not change. It tracks only Microsoft's adprep updates, not custom extensions.
Why you can't simply remove it. labEmployeeBadgeID is referenced by labUserExtension, which is linked to user. An attribute can only be deactivated (isDefunct) when no active class uses it. So in practice, extensions like this one stay in the forest for good. Leave it in place; it's harmless and clearly named.
Deactivation drill. Practise on a standalone attribute that no class references:
Drill step 1. Create the attribute first. You can only deactivate an object that exists. Save as C:\Lab\schema-testdefunct.ldf, using your base OID + .1.2:
dn: CN=lab-TestDefunct,DC=X
changetype: add
objectClass: attributeSchema
lDAPDisplayName: labTestDefunct
adminDisplayName: lab-TestDefunct
adminDescription: Lab attribute for the deactivation drill
attributeID: <BASE>.1.2
attributeSyntax: 2.5.5.12
oMSyntax: 64
isSingleValued: TRUE
searchFlags: 0
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
ldifde -i -f C:\Lab\schema-testdefunct.ldf -s DC01 -c "DC=X" "#schemaNamingContext" -v -j C:\Lab
# Confirm it exists before deactivating
$schemaNC = (Get-ADRootDSE).schemaNamingContext
Get-ADObject -Server DC01 -SearchBase $schemaNC -Filter "lDAPDisplayName -eq 'labTestDefunct'" -Properties attributeID, isDefunct
Drill step 2. Deactivate it with C:\Lab\schema-defunct.ldf, imported on the Schema Master (-s DC01):
dn: CN=lab-TestDefunct,DC=X
changetype: modify
replace: isDefunct
isDefunct: TRUE
-
dn:
changetype: modify
add: schemaUpdateNow
schemaUpdateNow: 1
-
Drill step 3. Import it with the same ldifde command. In the Schema snap-in, the attribute now shows as deactivated, but the object still exists. That is what "no deletions" means.
Cleanup:
Set-ADUser testuser -Clear labEmployeeBadgeID
Remove-ADGroupMember -Identity "Schema Admins" -Members "<your admin account>" -Confirm:$false
Sign out and back in, then confirm that whoami /groups no longer lists Schema Admins.
Tasks 2–5 exercise notes: schema extension on DC01, 28 Oct 2026
The next day I ran Tasks 2 to 5. The schema change itself worked. Almost everything that went wrong was me: placeholders I typed literally, a PowerShell quirk, and doing a step in the wrong order. The run log below lists every attempt, including the failed ones.
Result: lab-EmployeeBadgeID and lab-UserExtension were created, linked to user, and tested on Bob.Tester (B-1001). The deactivation drill worked once lab-TestDefunct was created first. Schema Admins membership was removed at the end. All work ran on DC01, which now holds the Schema Master role.
Role change before the exercise
| FSMO role | 27 Oct (Task 1) | 28 Oct |
|---|---|---|
| Schema Master | DC02 | DC01 (transferred) |
| Domain naming, PDC, RID, Infrastructure | DC01 | DC01 |
All five roles are now on DC01, so every ldifde -s in this guide targets DC01.
Objects created
Base OID: 1.2.840.113556.1.8000.2554.37133.11732.61101.19326.42313.5642591.8298009, saved in C:\Lab\BaseOID.txt.
| Object | Type | OID | Status |
|---|---|---|---|
lab-EmployeeBadgeID (labEmployeeBadgeID) |
Attribute, Unicode string, max 32 | <BASE>.1.1 |
Active, used by lab-UserExtension |
lab-UserExtension (labUserExtension) |
Auxiliary class | <BASE>.2.1 |
Linked to user
|
lab-TestDefunct (labTestDefunct) |
Attribute, no class | <BASE>.1.2 |
Deactivated (drill) |
Run log
| # | Command | Result |
|---|---|---|
| 1 | OID generator script | First run failed: typo NewGui//d() gave a parser error. Rerun with NewGuid() worked. |
| 2 |
<BASE>.1.1 typed at the prompt |
Error: < is not a command. <BASE> is a placeholder in the guide, not something to run. |
| 3 | $BaseOID.1.1 |
Returned nothing: PowerShell reads .1 as a property name. Use "$BaseOID.1.1". |
| 4 |
schema-lab.ldf written in Notepad, checked with Select-String
|
Both OIDs present, no <BASE> left ✅ |
| 5 | ldifde -i -f schema-lab.ldf -s DC01 … |
Attribute and class created ✅ |
| 6 | ldifde -i -f schema-link.ldf -s DC01 … |
labUserExtension added to user ✅ |
| 7 | Set-ADUser testuser … |
Not found: no account called testuser in the lab. Used Bob.Tester (OU=Lab Users). |
| 8 | Set-ADUser Bob.Tester -Replace @{labEmployeeBadgeID = "B-1001"} |
Value written; Get-ADUser and LDAP filter (labEmployeeBadgeID=B-*) both returned Bob Tester ✅ |
| 9 | ldifde -i -f schema-defunct.ldf … |
0x208D NO_OBJECT, best match CN=Schema,…: lab-TestDefunct did not exist yet |
| 10 | Created it with schema-testdefunct.ldf, then reran step 9 |
Deactivation succeeded ✅ |
| 11 | Get-ADObject … -Filter "lDAPDisplayName -like 'lab*'" |
Listed the lab objects plus built-in labeledURI. The class shows no attributeID because classes use governsID. |
| 12 | Set-ADUser Bob.Tester -Clear labEmployeeBadgeID |
Value cleared ✅ (an earlier Set-ADUser testuser Bob.Tester … failed: no positional parameter) |
| 13 | Remove-ADGroupMember "Schema Admins" -Members adminstrator |
Removed ✅ |
The mistake worth remembering: step 9. I tried to deactivate an attribute I hadn't created yet.
NO_OBJECTwith a "best match" ofCN=Schemais AD telling you the parent container exists but the object you named doesn't. Once I read it that way, the fix was obvious.
Lessons learned
- An .ldf file is plain text.
ldifdecannot read PowerShell variables, so OIDs must be written out in full. - Inside PowerShell, build OIDs as strings:
"$BaseOID.1.1", not$BaseOID.1.1. - Names in a guide (
<BASE>,testuser) are placeholders. Check real names first:Get-ADUser -Filter "Name -like '*test*'". -
NO_OBJECTwith a "best match" means the parent exists but the object does not. Create before you modify or deactivate. - Attributes are identified by
attributeID; classes bygovernsID. - The prefix
labalso matched the built-inlabeledURI. In production, use a unique company prefix (for examplecontoso-). - Transferring the Schema Master changes which DC every schema import must target. Run
netdom query fsmobefore each change. - In Windows PowerShell 5.1, the transcript did not record the output of
ldifde,repadminandnetdom. Pipe them through| Out-Hostto capture it, and checkC:\Lab\ldif.log/ldif.err, which-j C:\Labwrites.
Still to verify (output not captured in the transcript)
That last lesson has a cost: some results I saw on screen never made it into my log. Until I re-run these checks and keep the output, I'm treating them as not verified.
- ☐
labTestDefunctshowsisDefunct = True - ☐ Lab attribute and class visible on DC02 (Task 5 replication check)
- ☐
netdom query fsmoshows DC01 for all five roles - ☐
repadmin /replsummaryshows 0 failures after the schema changes - ☐ After signing out and in,
whoami /groupsno longer lists Schema Admins
$schemaNC = (Get-ADRootDSE).schemaNamingContext
Get-ADObject -Server DC02 -SearchBase $schemaNC -Filter "lDAPDisplayName -like 'lab*'" -Properties attributeID, governsID, isDefunct |
Select Name, attributeID, governsID, isDefunct
netdom query fsmo | Out-Host
repadmin /replsummary | Out-Host
whoami /groups | findstr /i schema
Troubleshooting, checklist and blog notes
If you run this lab yourself, these are the errors you're most likely to hit, and where each one comes from.
| Symptom | Likely cause | Fix |
|---|---|---|
Insufficient access rights on import |
Old logon token without Schema Admins | Sign out and back in, check whoami /groups
|
Unwilling to perform / referral |
Import not aimed at the Schema Master | Use -s <Schema Master>
|
Entry already exists / OID conflict |
Name or OID reused | New <BASE> value, check the snap-in first |
Set-ADUser says attribute unknown |
Schema cache not refreshed | Update Schema Now, then a new PowerShell window |
| Attribute missing on DC02 | Replication delay or failure |
repadmin /syncall /AdeP, repadmin /showrepl
|
| Schema snap-in not in MMC list | DLL not registered | regsvr32 schmmgmt.dll |
Checklist:
- ☐ Task 1 baseline recorded (Schema Master, objectVersion, replsummary clean)
- ☐ Snapshot and checkpoint taken
- ☐ Schema Admins added and token refreshed
- ☐ Attribute and auxiliary class imported
- ☐ Class linked to user, value written and read back
- ☐ Value confirmed on DC02
- ☐ Deactivation drill done
- ☐ Schema Admins membership removed
Real-world takeaway: Exchange (/PrepareSchema), Windows LAPS (Update-LapsADSchema) and ConfigMgr (extadsch.exe) all do exactly what you did here, just with vendor-registered OIDs.
What I took away from this
- The baseline is the most important step. It took longer than the schema change itself, and it found the one problem that would have silently broken everything after it.
-
Know where the Schema Master is, every time. Mine moved twice in a week, and every
ldifde -sdepended on it. - "Passed" can be out of date. A replication error can stay in the report long after its cause is gone. Force a fresh attempt before you believe either the error or the fix.
-
Read placeholders as placeholders. Half my run-log errors came from typing
<BASE>ortestuserliterally. -
Permanent means permanent. The badge attribute is in my forest for good. That's fine in a lab with clear
lab-names, and it's exactly why production teams test in a separate forest first.
This came from my own Active Directory lab, so your results will differ. If you've extended the schema in production, or hit 8524 in a different way, I'd like to hear about it in the comments.
Top comments (0)