Active Directory

Get-ADTargetServer

Forest-aware domain controller resolver. Queries the Global Catalog to locate which domain an identity belongs to, then discovers a writable DC for that domain — no hardcoded server references, full multi-domain forest support with site-preference and RODC avoidance.

PowerShell v1.0.0 ~155 lines Apr 2025

Script Details

Version 1.0.0
Last Updated April 2025
Compatible With PowerShell 5.1+ · Windows PowerShell + RSAT
Required Modules ActiveDirectory (RSAT)
Required Permissions Domain User read (AD) · GC port 3268 reachable
Outputs PSCustomObject — DomainController, Domain, SamAccountName, DistinguishedName
↓ Download Get-ADTargetServer.ps1
POWERSHELL
function Get-ADTargetServer {
    <#
    .SYNOPSIS
        Resolves the appropriate Active Directory domain controller for a given identity.

    .DESCRIPTION
        Queries the Global Catalog to locate which domain an AD object belongs to,
        then discovers and returns a suitable domain controller for that domain.
        Supports forest root and all child domains without hardcoded server references.
        This is a reusable helper function consumed by other module functions such as
        Get-QAUser and any future functions requiring domain-aware DC resolution.

    .PARAMETER Identity
        The identity to look up. Accepts SamAccountName, UPN, email address, display name,
        DistinguishedName, or CanonicalName.

    .PARAMETER GlobalCatalog
        Optional. The Global Catalog server (port 3268) to use for initial forest-wide lookup.
        If omitted, the function auto-discovers a GC in the current site.

    .PARAMETER PreferredSite
        Optional. AD site name to prefer when discovering a domain controller.
        Useful in multi-site environments to avoid cross-site DC resolution.

    .OUTPUTS
        PSCustomObject with properties: DomainController, Domain, SamAccountName, DistinguishedName

    .EXAMPLE
        Get-ADTargetServer -Identity "jdoe"

    .EXAMPLE
        Get-ADTargetServer -Identity "john.doe@yourcompany.com"

    .EXAMPLE
        Get-ADTargetServer -Identity "jdoe" -PreferredSite "SiteA"

    .EXAMPLE
        Get-ADTargetServer -Identity <distinguishedName>

    .EXAMPLE
        Get-ADTargetServer -Identity <samaccountname>

    .NOTES
        Module      : MyModule
        Author      : Infrastructure Team
        Version     : 1.0.0
        Requires    : ActiveDirectory module
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string]$Identity,

        [Parameter(Mandatory = $false)]
        [string]$GlobalCatalog,

        [Parameter(Mandatory = $false)]
        [string]$PreferredSite
    )

    process {

        # ── GC Resolution ─────────────────────────────────────────────────────
        # Auto-discover a GC in the current forest if none is explicitly provided.

        if (-not $GlobalCatalog) {
            try {
                Write-Verbose "No GlobalCatalog specified — discovering one in the current forest."
                $discoveredGC = Get-ADDomainController -Discover -Service GlobalCatalog -ErrorAction Stop |
                    Select-Object -ExpandProperty HostName |
                    Select-Object -First 1

                $GlobalCatalog = '{0}:3268' -f $discoveredGC
                Write-Verbose "Using auto-discovered Global Catalog: $GlobalCatalog"
            }
            catch {
                throw "Failed to discover a Global Catalog server. Provide one explicitly via -GlobalCatalog. Error: $_"
            }
        }
        else {
            if ($GlobalCatalog -notmatch ':\d+$') {
                $GlobalCatalog = '{0}:3268' -f $GlobalCatalog
                Write-Verbose "Appended GC port — using: $GlobalCatalog"
            }
        }

        # ── Step 1: Forest-wide identity resolution via Global Catalog ────────
        # The GC holds a partial replica of every domain in the forest, making it
        # the correct and only reliable place to search across all domains.

        Write-Verbose "Searching Global Catalog '$GlobalCatalog' for identity '$Identity'."

        $mailWildcard = "$Identity@*"

        try {
            $gcResult = Get-ADUser -Filter {
                (SamAccountName    -eq $Identity)        -or
                (UserPrincipalName -eq $Identity)        -or
                (EmailAddress      -eq $Identity)        -or
                (Mail              -like $mailWildcard)  -or
                (Name              -eq $Identity)        -or
                (ObjectGUID        -eq $Identity)        -or
                (DistinguishedName -eq $Identity)
            } -Properties UserPrincipalName, SamAccountName, DistinguishedName, CanonicalName `
              -Server $GlobalCatalog `
              -ErrorAction Stop
        }
        catch {
            throw "Global Catalog query failed against '$GlobalCatalog' for '$Identity'. Error: $_"
        }

        if (-not $gcResult) {
            Write-Warning "Identity '$Identity' was not found in the forest via Global Catalog '$GlobalCatalog'."
            return $null
        }

        if ($gcResult.Count -gt 1) {
            $matchList = ($gcResult | ForEach-Object { $_.UserPrincipalName }) -join ', '
            throw "Ambiguous identity '$Identity' matched multiple objects: $matchList. Provide a more specific identity."
        }

        # ── Step 2: Extract the domain from the DistinguishedName ─────────────
        # Example DN:  CN=John Doe,OU=Users,DC=child,DC=root,DC=local
        # Extracted:   child.root.local

        $dn = $gcResult.DistinguishedName

        $domainDNS = ($dn -split ',' |
            Where-Object { $_ -match '^DC=' } |
            ForEach-Object { ($_ -split '=')[1] }
        ) -join '.'

        Write-Verbose "Resolved domain '$domainDNS' from DN: $dn"

        if (-not $domainDNS) {
            throw "Could not derive a domain DNS name from DistinguishedName: '$dn'"
        }

        # ── Step 3: Discover a writable DC in the resolved domain ─────────────
        # -Writable ensures we never land on a Read-Only DC (RODC).

        try {
            $discoverParams = @{
                DomainName  = $domainDNS
                Writable    = $true
                ErrorAction = 'Stop'
            }

            if ($PreferredSite) {
                Write-Verbose "Preferring AD site '$PreferredSite' for DC discovery in domain '$domainDNS'."
                $discoverParams['SiteName'] = $PreferredSite
            }

            $targetDC = Get-ADDomainController -Discover @discoverParams |
                Select-Object -ExpandProperty HostName |
                Select-Object -First 1
        }
        catch {
            if ($PreferredSite) {
                Write-Warning "No DC found in site '$PreferredSite' for domain '$domainDNS'. Falling back to any available DC."
                try {
                    $targetDC = Get-ADDomainController -Discover -DomainName $domainDNS -Writable -ErrorAction Stop |
                        Select-Object -ExpandProperty HostName |
                        Select-Object -First 1
                }
                catch {
                    throw "Failed to discover any writable DC for domain '$domainDNS'. Error: $_"
                }
            }
            else {
                throw "Failed to discover a writable DC for domain '$domainDNS'. Error: $_"
            }
        }

        Write-Verbose "Resolved domain controller: $targetDC (Domain: $domainDNS)"

        # ── Output ────────────────────────────────────────────────────────────
        [PSCustomObject]@{
            DomainController  = $targetDC
            Domain            = $domainDNS
            SamAccountName    = $gcResult.SamAccountName
            DistinguishedName = $dn
        }
    }
}

How to Use

Dot-source the file or include it in your module, then call it with any supported identity format. The function resolves across all domains in the forest — no domain parameter needed.

Parameters

ParameterTypeRequiredDescription
-Identity [string] Required SamAccountName, UPN, email address, display name, DistinguishedName, or ObjectGUID.
-GlobalCatalog [string] Optional GC server for forest-wide lookup. Port 3268 is appended automatically. If omitted, a GC is auto-discovered in the current site.
-PreferredSite [string] Optional AD site name to prefer for DC discovery. Falls back to any available writable DC if no DC exists in the specified site.

Usage Examples

POWERSHELL
# Resolve by SamAccountName
Get-ADTargetServer -Identity "jdoe"

# Resolve by UPN
Get-ADTargetServer -Identity "john.doe@root.local"

# Resolve by email address
Get-ADTargetServer -Identity "j.doe@yourcompany.com"

# Resolve with site preference (multi-site environment)
Get-ADTargetServer -Identity "jdoe" -PreferredSite "London-HQ"

# Supply a specific GC — useful when auto-discovery is unreliable
Get-ADTargetServer -Identity "jdoe" -GlobalCatalog "gc01.root.local"

# Use the resolved DC in a downstream AD command
$target = Get-ADTargetServer -Identity "jdoe"
Get-ADUser -Identity $target.SamAccountName -Server $target.DomainController -Properties *

# Pipeline — resolve multiple identities
"jdoe", "asmith", "bwilson" | Get-ADTargetServer

Sample Output

OUTPUT
DomainController  : dc02.child.root.local
Domain            : child.root.local
SamAccountName    : jdoe
DistinguishedName : CN=John Doe,OU=Users,DC=child,DC=root,DC=local

Production Notes

GC Port Reachability

Port 3268 must be reachable from the host running the script. In environments with strict firewall segmentation between sites, verify before relying on auto-discovery: Test-NetConnection <gc-hostname> -Port 3268

RODC Avoidance

The function explicitly requests a writable DC (-Writable) at every discovery step. This prevents routing write operations to Read-Only Domain Controllers in branch office deployments. Ensure at least one writable DC is reachable per domain in the forest.

Ambiguous Identity Handling

If the supplied identity matches more than one AD object — common with display name lookups — the function throws with all matching UPNs listed. Always use SamAccountName, UPN, or email in automated pipelines to avoid ambiguous matches.

Module Integration Pattern

Designed as a helper consumed by other module functions. In QAModule v1.7, every AD write operation calls Get-ADTargetServer first to resolve the correct DC — this eliminates cross-domain write failures caused by hardcoded or assumed DC references.