How to run a PowerShell script against multiple Active Directory domains with different credentials

05_collageI was working with a customer recently who needed to execute the same script against servers in different Active Directory domains. They had administrative privileges in each domain, but each domain used a different account. You could apply this same scenario to running one query against domain controllers in different domains. Today we’ll explore one way to do that.

Multiple Credentials

A while back I stumbled onto this handy blog post by Jaap Brasser, a PowerShell MVP from the Netherlands. He was using a hash table to store multiple credentials in a script, and then cache them to disk securely using Export-CliXML. The XML export is a quick way to dump the secure string passwords to disk for later re-use. This has been a popular technique for several years now, but he is the first one I saw doing this with a hash table of credentials. Nice touch! In this post I am building on his technique.

Passwords on disk?!

Many people have blogged about securely storing PowerShell credentials for reuse later. There are many techniques, even Azure Key Vault. However, the most common technique out-of-the-box is with secure strings. I do not have time to rehash these topics here. For more information check out Get-Help for these cmdlets: Get-Credential, ConvertFrom-SecureString, ConvertTo-SecureString.

The main point to understand here is that PowerShell encrypts secure string passwords by default using the DPAPI. This encryption is tied to one user on one computer. If you export credentials to a file, then you can only read them on the computer where the file was generated and with the same user account.

Managing Multiple Credentials

Many customers have multiple-domain Active Directory environments and need a way to manage all of those credentials in a single script. Think about the following use cases:

  • Run the same query against every domain controller in three domains or forests
  • Run the same command against 100 servers residing in 10 different domains

Some people may have simply run the same script multiple times, prompting for credentials of each domain and targeting only those servers. Then repeat that for each domain. This is inefficient and produces multiple data sets to be joined at the end for one view of the output.

To do this efficiently and securely is an advanced scripting challenge.

One Solution

As I approached this problem I broke it down into the following steps:

  • Store the target server FQDNs in an array (from file, AD query, etc.)
  • Parse out the unique domain names from the FQDNs
  • Prompt once for credentials of each unique domain
  • Store the credentials in a hash table
  • Export the credential hash table to XML for reuse later
  • Import the credentials from XML (for subsequent runs of the script)
  • Using a loop, pass each server FQDN and respective domain credential to Invoke-Command

Caveat Emptor (“Buyer Beware”)

A comment on Jaap’s blog post cautions against storing highly privileged account credentials in an encrypted secure string using Export-CliXML. Anyone with access to the one account on the one computer used to encrypt the secure string would be able to retrieve all the different domain credentials stored there. They could decrypt all the passwords into plain text! This is a valid concern. However, regardless of how you store the credentials they will all be available to the one user account while the script is running on this one server. If you choose to run any script with multiple credentials, then this concern will always exist. At that point it is an HR issue making sure your staff is trustworthy.

If you choose to store credentials in a file, then you must secure it appropriately and know the risks even when they are encrypted. With that warning we will proceed.

Clever Credential Controls

Here is a sample script for implementing multiple credentials:

 Function Get-DomainCreds {            
[CmdletBinding()]            
Param(            
    [Parameter(            
        Mandatory=$true,            
        ParameterSetName='Fresh'            
    )]            
    [ValidateNotNullOrEmpty()]            
    [string[]]            
    $Domain,            
    [Parameter(            
        Mandatory=$true,            
        ParameterSetName='File'            
    )]            
    [Parameter(            
        Mandatory=$true,            
        ParameterSetName='Fresh'            
    )]            
    [ValidateNotNullOrEmpty()]            
    [string]            
    $Path            
)            
            
    If ($PSBoundParameters.ContainsKey('Domain')) {            
            
        # https://www.jaapbrasser.com/quickly-and-securely-storing-your-credentials-powershell/            
        $Creds = @{}            
        ForEach ($DomainEach in $Domain) {            
            $Creds[$DomainEach] = Get-Credential `
                -Message "Enter credentials for domain $DomainEach" `
                -UserName "$DomainEach\username"            
        }            
        $Creds | Export-Clixml -Path $Path            
            
    } Else {            
            
        $Creds = Import-Clixml -Path $Path            
               
    }            
            
    Return $Creds            
}            
            
Function Split-FQDN {            
Param(            
    [string[]]$FQDN            
)            
    ForEach ($Server in $FQDN) {            
            
        $Dot = $Server.IndexOf('.')            
        [pscustomobject]@{            
            FQDN     = $Server            
            Hostname = $Server.Substring(0,$Dot)            
            Domain   = $Server.Substring($Dot+1)            
        }            
            
    }            
}            
            
            
# Array of server FQDNs from your favorite data source            
$Servers = 'server1.contoso.com','dc1.alpineskihouse.com',`
    'dc2.wideworldimporters.com','dc3.contoso.com'            
# Take a server list of FQDNs and separate out the domain and hostname            
$ServerList = Split-FQDN -FQDN $Servers            
            
# Extract the unique domain names            
# ONLY THE FIRST RUN: Get credentials for each domain            
$Domains = $ServerList | Select-Object -ExpandProperty Domain -Unique            
$DomCreds = Get-DomainCreds -Domain $Domains -Path C:\deploy\creds.xml            
            
# LATER: Get credentials for each domain from the XML file            
$DomCreds = Get-DomainCreds -Path C:\deploy\creds.xml            
            
ForEach ($Server in $ServerList) {            
            
    Invoke-Command -ComputerName $Server.FQDN `
        -Credential $DomCreds[$Server.Domain] -ScriptBlock {            
            
        "Hello from $(hostname)"            
            
    }            
            
}            

The clever part here is using the domain portion of the FQDN as the hash table key to retrieve the domain-specific credential for each server FQDN in the list.

Insert your own code inside the ScriptBlock parameter of Invoke-Command. Or use the FilePath parameter instead to run the same script on each remote server.

If you do not want to store the credentials in a file, then comment out the Export-CliXML line in the Get-DomainCreds function. You will need to enter the credentials manually each time you run the script if that is your choice.

Now you can schedule this to run unattended using the credentials in the XML file. Note that you must run the script to build the credential XML file under the same user account for the scheduled task.

Here is an example of executing one command across multiple domains:

 # Query a list of domain controllers using stored credentials (include functions above)            
# List the Domain Admin group membership for all domains            
$Servers = 'dc1.alpineskihouse.com',`
    'dc2.wideworldimporters.com','dc3.contoso.com'            
$ServerList = Split-FQDN -FQDN $Servers            
$DomCreds = Get-DomainCreds -Path C:\deploy\creds.xml            
ForEach ($Server in $ServerList) {            
    '*' * 40            
    $Server.Domain            
    Invoke-Command -ComputerName $Server.FQDN `
        -Credential $DomCreds[$Server.Domain] -ScriptBlock {            
        Get-ADGroupMember -Identity 'Domain Admins' |             
            Select-Object -ExpandProperty distinguishedName            
    }            
}            

You could just as easily loop through Get-ADObject queries passing a unique Server and Credential parameter instead of using Invoke-Command. But it would not be any fun if I wrote all the code for you.

Summary

This is a common scenario, so I wanted to offer a solution using built-in PowerShell features. Now you have one technique for running the same code under multiple credentials against servers in multiple Active Directory domains.

How have you solved this challenge in your environment? How can you use this technique? Comments welcome.