In Active Directory when you change something, it's replicated to other Domain Controllers regularly. It's a standard procedure that happens automatically in the background for you. It's a handy feature because you can have multiple DC's all over the world and have your users data in sync. You can change almost anything on DC nearest to you and be sure it will be the same value all over the place. But is it always the same? Well, it should be unless it isn't. Today I was given a new migration from Exchange to Office 365. I started with ADConnect installation and wanted to make sure that UserPrincipalNames have all UPNSuffixes in place.
Adding UPNSuffixes is a relatively simple process. Just open Active Directory Domain And Trusts, right-click on the Active Directory Domains and Trust on the left side, choose Properties and that's where you add a new UPN. It's a pretty straightforward process.
If you prefer to add UPN via PowerShell, it's also possible of course, but even thou I play with PowerShell daily I do resort to GUI for one time changes. For the sake of completeness here's how you would add new UPN with PowerShell
Set-ADForest -Identity 'ad.evotec.xyz' -UPNSuffixes @{Add='newUPN@com'}
Now that we've UPN added, I open up Active Directory Users and Computers to add newly added UPN to the user, and it's not there. Only the default UPN that has been there since forever. I opened up PowerShell to do some verification (maybe my mind is playing tricks on me). So I run this little command which should show me a bunch of Active Directory data including UPNSuffixes defined for my forest.
Get-ADForest
And, on the first try, everything seems to be correct. I can see the UPN's on the list, so I go and check some stuff, go back to PowerShell to try and debug it from PowerShell, and now Get-ADForest on the very same session is showing UPN's are not there. I'll be honest here, for a moment I was stuck in a state of disbelief because things are not supposed to disappear in Active Directory.
That's when I remembered that I'm not logged in to PDC and most likely I'm logged to one of the DC's within other sites. By default, Active Directory doesn't replicate everything all the time. It does so only for Domain Controllers within the same site. If you have more sites such as between different cities, countries, or server rooms, it synchronizes less often. By default, DEFAULTIPSITELINK has replication set to replicate every 180 minutes. That's a long time in today's world.
Fortunately, you can change that 180 minutes value, and that's what I would most of the time do. I would always change it to the lowest possible value, which is 15 minutes. It's not as bad as 180 minutes, but still, 15 minutes is a lot of time when you want to do something fast and make sure that every DC in the world has the same data.
That's where having a way to force sync comes into play and something I would often use after making changes to Group Policies, user settings to make sure it's rapidly available to users in other sites. When you search the internet for a way to force synchronization between sites/Domain Controllers you will often find one of the two commands
repadmin /syncall /AdeP
Or this one
repadmin /syncall /AdePq
While those commands do work, those are not magic bullets. It's a fast way to force synchronization, but the synchronization is effected only on Domain Controller it's executed on. It means that if you force sync like that, right after the change you did, it will work just fine, but it won't notice the difference someone else did on other sites. To adequately address this issue, you need to force synchronization on every DC. For that, you need to find all Domain Controllers and execute the command.
function Sync-DomainController { [CmdletBinding()] param( [string] $Domain = $Env:USERDNSDOMAIN ) $DistinguishedName = (Get-ADDomain -Server $Domain).DistinguishedName (Get-ADDomainController -Filter * -Server $Domain).Name | ForEach-Object { Write-Verbose -Message "Sync-DomainController - Forcing synchronization $_" repadmin /syncall $_ $DistinguishedName /e /A | Out-Null } }
The code above takes care of my needs for an instant, but manual sync. But just running the command isn't always a sign that everything worked just fine. What if some Domain Controllers are down? What if something went wrong? You can confirm this by verifying replication with the following function (which is also part of PSWinDocumentation.AD)
function Get-WinADForestReplicationPartnerMetaData { [CmdletBinding()] param( [switch] $Extended ) $Replication = Get-ADReplicationPartnerMetadata -Target * -Partition * -ErrorAction SilentlyContinue -ErrorVariable ProcessErrors if ($ProcessErrors) { foreach ($_ in $ProcessErrors) { Write-Warning -Message "Get-WinADForestReplicationPartnerMetaData - Error on server $($_.Exception.ServerName): $($_.Exception.Message)" } } foreach ($_ in $Replication) { $ServerPartner = (Resolve-DnsName -Name $_.PartnerAddress -Verbose:$false -ErrorAction SilentlyContinue) $ServerInitiating = (Resolve-DnsName -Name $_.Server -Verbose:$false -ErrorAction SilentlyContinue) $ReplicationObject = [ordered] @{ Server = $_.Server ServerIPV4 = $ServerInitiating.IP4Address ServerPartner = $ServerPartner.NameHost ServerPartnerIPV4 = $ServerPartner.IP4Address LastReplicationAttempt = $_.LastReplicationAttempt LastReplicationResult = $_.LastReplicationResult LastReplicationSuccess = $_.LastReplicationSuccess ConsecutiveReplicationFailures = $_.ConsecutiveReplicationFailures LastChangeUsn = $_.LastChangeUsn PartnerType = $_.PartnerType Partition = $_.Partition TwoWaySync = $_.TwoWaySync ScheduledSync = $_.ScheduledSync SyncOnStartup = $_.SyncOnStartup CompressChanges = $_.CompressChanges DisableScheduledSync = $_.DisableScheduledSync IgnoreChangeNotifications = $_.IgnoreChangeNotifications IntersiteTransport = $_.IntersiteTransport IntersiteTransportGuid = $_.IntersiteTransportGuid IntersiteTransportType = $_.IntersiteTransportType UsnFilter = $_.UsnFilter Writable = $_.Writable } if ($Extended) { $ReplicationObject.Partner = $_.Partner $ReplicationObject.PartnerAddress = $_.PartnerAddress $ReplicationObject.PartnerGuid = $_.PartnerGuid $ReplicationObject.PartnerInvocationId = $_.PartnerInvocationId $ReplicationObject.PartitionGuid = $_.PartitionGuid } [PSCustomObject] $ReplicationObject } }
As you can see above, I have some issues in my test Domain, mostly because I've shut down some Domain Controllers to test if the code works as expected in non-optimal conditions.
If 15 minutes or manual replication are not for you and you want to instant synchronization all the time there's also a way to make it happen. And to be honest, I don't understand why in the age of 1 Gbit connections you would still not allow to change it via GUI intuitively. It won't affect the vast domains where you need to be more careful with replication settings, but it should be easy enough for those working with smaller domains. The way to do it is via ADSI Edit in Configuration, under Sites, under Inter-Site Transports, under IP. We then pick site link properties and modify options value.
You can change that value thru GUI in this place, or change it via PowerShell.
$NamingContext = (Get-ADRootDSE).configurationNamingContext Get-ADObject -LDAPFilter "(objectCategory=sitelink)" –Searchbase $NamingContext -Properties options | ForEach-Object { Set-ADObject $_ –replace @{ options = $($_.options -bor 1) } }
After running the command options value has changed and is now showing additional name next to it USE_NOTIFY. It's important to know here is that the value of options is not always empty. We need to do BITWISE OR operation on that value setting it to a proper value, which when blank is 0x1 (USE_NOTIFY). Regardless of its current value, PowerShell above takes care of that problem of doing that operation yourself.
There is one more thing to know here thou. When you set up site links manually above option doesn't apply. It only applies to those created automatically. In my case, I've three additional connections created manually for this particular DC that and we need to treat it separately.
As you see on the above screenshot only that automatically generated connection is affected by global change, we did above. If we want things to work for us, we need to change settings for all other links to different values.
We have to find their path in ADSI Edit and modify their value. But this time it's a bit more complicated because you would need to go thru each connection and change its Options value. Remember how I told you that the value needs to replace with BitOR to 1. Well, in this case, it's a different value.
In this case, number 1 means IS_GENERATED. Rest of the connections has 0 in options, which means they will respect the global replication interval (not the USE_NOTIFY thou). Things get even more complicated with RODC.
Value 0x41 (IS_Generated | RODC_Topology) gives us the number value 65. In this case, our Values mean RODC_Topology is 64, and IS_Generated is 1. In case you would find other Values here, you always need to make do that calculation yourself. Following Microsoft post contains all Values available. Since I don't want to go manually thru every connection, let's try to find some information about all our connections using PowerShell. I've written simple function Get-WinADSiteConnections which does some small cleanup over Get-ADObject results and delivers an excellent overview of those in a single view.
function Get-WinADSiteConnections { [CmdletBinding()] param( ) [Flags()] enum ConnectionOption { None IsGenerated TwoWaySync OverrideNotifyDefault = 4 UseNotify = 8 DisableIntersiteCompression = 16 UserOwnedSchedule = 32 RodcTopology = 64 } $NamingContext = (Get-ADRootDSE).configurationNamingContext $Connections = Get-ADObject –Searchbase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties * $FormmatedConnections = foreach ($_ in $Connections) { $Dictionary = [PSCustomObject] @{ <# Regex extracts AD1 and AD2 CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz #> CN = $_.CN Description = $_.Description DisplayName = $_.DisplayName EnabledConnection = $_.enabledConnection ServerFrom = if ($_.fromServer -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer } ServerTo = if ($_.DistinguishedName -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer } <# Regex extracts KATOWICE-1 CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz #> SiteFrom = if ($_.fromServer -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer } SiteTo = if ($_.DistinguishedName -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer } OptionsTranslated = [ConnectionOption] $_.Options Options = $_.Options WhenCreated = $_.WhenCreated WhenChanged = $_.WhenChanged IsDeleted = $_.IsDeleted } $Dictionary } $FormmatedConnections }
We can now clearly see all our connections and sites.
Now that we have our values, with full visibility of Options property, we can do a manual test and see how that changes. USE_Notify, which is responsible for the immediate notification process, has a value of 8. That means that for out RODC Connection we can change it from 65 to 73 and that should solve it.
Accurate result? Yes. But we don't want to do it manually, right?
We can do automatic change with this little command
function Set-WinADReplicationConnections { [CmdletBinding()] param( [switch] $Force ) [Flags()] enum ConnectionOption { None IsGenerated TwoWaySync OverrideNotifyDefault = 4 UseNotify = 8 DisableIntersiteCompression = 16 UserOwnedSchedule = 32 RodcTopology = 64 } $NamingContext = (Get-ADRootDSE).configurationNamingContext $Connections = Get-ADObject –Searchbase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties * foreach ($_ in $Connections) { $OptionsTranslated = [ConnectionOption] $_.Options if ($OptionsTranslated -like '*IsGenerated*' -and -not $Force) { Write-Verbose "Set-WinADReplicationConnections - Skipping $($_.CN) automatically generated link" } else { Write-Verbose "Set-WinADReplicationConnections - Changing $($_.CN)" Set-ADObject $_ –replace @{ options = $($_.options -bor 8) } } } }
If we run this command above, it will go thru each of the connections and add that 8 value in there. If we rerun our Get-WinADSiteConnections function, we can confirm that Options values did change has changed to their instant, proper setting. Keep in mind it will only change links created manually and skip those that are auto-generated. If you want to force change on all you need to use Force switch.
Below you can find six functions that I've used above, and something that can be useful if you don't want to spend time doing a thing manually.
function Sync-DomainController { [CmdletBinding()] param( [string] $Domain = $Env:USERDNSDOMAIN ) $DistinguishedName = (Get-ADDomain -Server $Domain).DistinguishedName (Get-ADDomainController -Filter * -Server $Domain).Name | ForEach-Object { Write-Verbose -Message "Sync-DomainController - Forcing synchronization $_" repadmin /syncall $_ $DistinguishedName /e /A | Out-Null } } function Set-WinADReplication { [CmdletBinding( )] param( [int] $ReplicationInterval = 15, [switch] $Instant ) $NamingContext = (Get-ADRootDSE).configurationNamingContext Get-ADObject -LDAPFilter "(objectCategory=sitelink)" –Searchbase $NamingContext -Properties options | ForEach-Object { if ($Instant) { Set-ADObject $_ -replace @{ replInterval = $ReplicationInterval } Set-ADObject $_ –replace @{ options = $($_.options -bor 1) } } else { Set-ADObject $_ -replace @{ replInterval = $ReplicationInterval } } } } function Set-WinADReplicationConnections { [CmdletBinding()] param( [switch] $Force ) [Flags()] enum ConnectionOption { None IsGenerated TwoWaySync OverrideNotifyDefault = 4 UseNotify = 8 DisableIntersiteCompression = 16 UserOwnedSchedule = 32 RodcTopology = 64 } $NamingContext = (Get-ADRootDSE).configurationNamingContext $Connections = Get-ADObject –Searchbase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties * foreach ($_ in $Connections) { $OptionsTranslated = [ConnectionOption] $_.Options if ($OptionsTranslated -like '*IsGenerated*' -and -not $Force) { Write-Verbose "Set-WinADReplicationConnections - Skipping $($_.CN) automatically generated link" } else { Write-Verbose "Set-WinADReplicationConnections - Changing $($_.CN)" Set-ADObject $_ –replace @{ options = $($_.options -bor 8) } } } } function Get-WinADSiteConnections { [CmdletBinding()] param( ) [Flags()] enum ConnectionOption { None IsGenerated TwoWaySync OverrideNotifyDefault = 4 UseNotify = 8 DisableIntersiteCompression = 16 UserOwnedSchedule = 32 RodcTopology = 64 } $NamingContext = (Get-ADRootDSE).configurationNamingContext $Connections = Get-ADObject –Searchbase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties * $FormmatedConnections = foreach ($_ in $Connections) { $Dictionary = [PSCustomObject] @{ <# Regex extracts AD1 and AD2 CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz #> CN = $_.CN Description = $_.Description DisplayName = $_.DisplayName EnabledConnection = $_.enabledConnection ServerFrom = if ($_.fromServer -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer } ServerTo = if ($_.DistinguishedName -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer } <# Regex extracts KATOWICE-1 CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz #> SiteFrom = if ($_.fromServer -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer } SiteTo = if ($_.DistinguishedName -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer } OptionsTranslated = [ConnectionOption] $_.Options Options = $_.Options WhenCreated = $_.WhenCreated WhenChanged = $_.WhenChanged IsDeleted = $_.IsDeleted } $Dictionary } $FormmatedConnections } function Get-WinADSiteLinks { [CmdletBinding()] param( ) $NamingContext = (Get-ADRootDSE).configurationNamingContext $SiteLinks = Get-ADObject -LDAPFilter "(objectCategory=sitelink)" –Searchbase $NamingContext -Properties * foreach ($_ in $SiteLinks) { [PSCustomObject] @{ Name = $_.CN Cost = $_.Cost ReplicationFrequencyInMinutes = $_.ReplInterval Options = $_.Options #ReplInterval : 15 Created = $_.WhenCreated Modified = $_.WhenChanged #Deleted : #InterSiteTransportProtocol : IP ProtectedFromAccidentalDeletion = $_.ProtectedFromAccidentalDeletion } } } function Get-WinADForestReplicationPartnerMetaData { [CmdletBinding()] param( [switch] $Extended ) $Replication = Get-ADReplicationPartnerMetadata -Target * -Partition * -ErrorAction SilentlyContinue -ErrorVariable ProcessErrors if ($ProcessErrors) { foreach ($_ in $ProcessErrors) { Write-Warning -Message "Get-WinADForestReplicationPartnerMetaData - Error on server $($_.Exception.ServerName): $($_.Exception.Message)" } } foreach ($_ in $Replication) { $ServerPartner = (Resolve-DnsName -Name $_.PartnerAddress -Verbose:$false -ErrorAction SilentlyContinue) $ServerInitiating = (Resolve-DnsName -Name $_.Server -Verbose:$false -ErrorAction SilentlyContinue) $ReplicationObject = [ordered] @{ Server = $_.Server ServerIPV4 = $ServerInitiating.IP4Address ServerPartner = $ServerPartner.NameHost ServerPartnerIPV4 = $ServerPartner.IP4Address LastReplicationAttempt = $_.LastReplicationAttempt LastReplicationResult = $_.LastReplicationResult LastReplicationSuccess = $_.LastReplicationSuccess ConsecutiveReplicationFailures = $_.ConsecutiveReplicationFailures LastChangeUsn = $_.LastChangeUsn PartnerType = $_.PartnerType Partition = $_.Partition TwoWaySync = $_.TwoWaySync ScheduledSync = $_.ScheduledSync SyncOnStartup = $_.SyncOnStartup CompressChanges = $_.CompressChanges DisableScheduledSync = $_.DisableScheduledSync IgnoreChangeNotifications = $_.IgnoreChangeNotifications IntersiteTransport = $_.IntersiteTransport IntersiteTransportGuid = $_.IntersiteTransportGuid IntersiteTransportType = $_.IntersiteTransportType UsnFilter = $_.UsnFilter Writable = $_.Writable } if ($Extended) { $ReplicationObject.Partner = $_.Partner $ReplicationObject.PartnerAddress = $_.PartnerAddress $ReplicationObject.PartnerGuid = $_.PartnerGuid $ReplicationObject.PartnerInvocationId = $_.PartnerInvocationId $ReplicationObject.PartitionGuid = $_.PartitionGuid } [PSCustomObject] $ReplicationObject } }
And if you would like to do it all in one go, with a single PowerShell script, here you go:
# Get currrent settings so that you can see what those are and change it back if needed Get-WinADSiteLinks Get-WinADSiteConnections -Verbose | Format-Table -Autosize # Set command - BE CAREFUL, I would run read only commands first Set-WinADReplication -ReplicationInterval 15 -Instant # you can use both or only one parameter. Set-WinADReplicationConnections -Verbose # Confirming the settings have applied correctly Get-WinADSiteLinks -Verbose | Format-Table -Autosize Get-WinADSiteConnections -Verbose | Format-Table -Autosize # Syncing changes so that those spread around quickly Sync-DomainController # Verify sync Get-WinADForestReplicationPartnerMetaData
After we enable replication thru notifications, all changes don't have to wait for their replication interval and should happen instantly. Of course, this can vary from Domain to Domain. In my case, before the change, it was 15 minutes, now it's around 10 seconds. For more complicated setup this time may vary.
For easy use and installation, I've added it to a small PowerShell Module called ADEssentials. Installing it is as easy as it gets.
Install-Module ADEssentials -AllowClobber -Force
Code as always is stored on GitHub and is free to use and take. After you install it, all commands become available without you having to do put them in a script. Bonus point is you'll get a few other functions that are useful for Active Directory management.