Scroll Top
Evotec Services sp. z o.o., ul. Drozdów 6, Mikołów, 43-190, Poland

Instant Replication between Active Directory sites with PowerShell

img_5d305f9999a88

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.

This changes your Active Directory replication settings - Be CAREFUL
When you follow this blost post, go thru the code before running this in production. It changes things. Review the code, run read-only commands first and confirm you see what you're supposed to see. While I've made an effort to be accurate here, if you don't feel confident in what you do, ask some Senior Admin to help you.
How it all started - Adding UPN suffixes to Active Directory and verifying it exists

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.

Changing default 180 minutes to 15 minutes replication time

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.

Forcing Active Directory synchronization

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.

Instant replication for Active Directory

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.

This changes your Active Directory replication settings - Be CAREFUL
When you follow this blog post, go thru the code before running this in production. It changes things. Review the code, run read-only commands first and confirm you see what you're supposed to see. While I've made an effort to be accurate here, if you don't feel confident in what you do, ask some Senior Admin to help you.

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.

Setting instant replication over manually created connections

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.

This changes your Active Directory replication settings - Be CAREFUL
When you follow this blog post, go thru the code before running this in production. It changes things. Review the code, run read-only commands first and confirm you see what you're supposed to see. While I've made an effort to be accurate here, if you don't feel confident in what you do, ask some Senior Admin to help you.

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?

This changes your Active Directory replication settings - Be CAREFUL
When you follow this blog post, go thru the code before running this in production. It changes things. Review the code, run read-only commands first and confirm you see what you're supposed to see. While I've made an effort to be accurate here, if you don't feel confident in what you do, ask some Senior Admin to help you.

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.

  • Sync-DomainController
  • Set-WinADReplication
  • Set-WinADReplicationConnections
  • Get-WinADSiteConnections
  • Get-WinADSiteLinks
  • Get-WinADForestReplicationPartnerMetaData
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.

Using ADEssentials as PowerShell Module

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.

Related Posts