Use Azure KeyVault to securely manage credentials for your PowerShell scripts

Disclaimer (as always)

I just documented what I did real quick. There’s a lot of room for improvement especially around error handling and possibly logging. Why don’t you go ahead and put your suggestions in a comment?

Working with different credentials in PowerShell can be a headscratcher. You don’t want the passwords to be too easy so you have some reasonably strong passwords on your service accounts. In some scenarios (e.g. SP installation via DSC), you will have a lot of service accounts in a lot of places. How can we approach this?
Hard-code passwords? Err… No.
Use Windows Credential Manager? Well, yes – provided you’re sure that it’s going to be the same user executing the script every time.

Now how about a modern approach: Azure KeyVault

First things first, your’re going to need a KeyVault.
Creating one in the portal is as easy as it gets. Select a unique name, stick it in a resource group and leave the rest of the settings on default. (KeyVault permissions and premium tier won’t be in the scope of this post.)

Pricing

Now wait a moment! Azure Resources usually come with a price! How much is this stunt going to cost me?

At the time I’m writing this: $0.03/10,000 transactions

That’s insane. How long does it take you to enter a sufficiently complex password? Let’s say you can do it in 15 seconds.
If you just entered passwords for an hour straight, you’d come out at 240 passwords. Microsoft will hit you with a hefty bill of $0.00072 if you use KeyVault instead. If you’re hourly rate is lower than that, you should keep doing this manually. (Also call me, I have work for you.)

Why do I want that?

Wait! You’re not convinced by the price? Well, ok then.
Assume you have an external consultant. You want them to be able to use specific credentials to do their job, but they don’t really need to know the credentials or you don’t want to send them in an email. There you go.

  1. Set up a KeyVault
  2. Punch in the necessary credentials once
  3. Give the external consultant read access (or the app, depending on your scenario)
  4. ???
  5. Profit

Set-KeyVaultCredential

This will add a secret to your KeyVault or update them if the already exists.

For clarification: KeyVault let’s you store encryption keys, certificates and secrets in a secure manner. It also has a lot of nice features around certificate renewal, life cycle management for keys and more. This solution will use only secrets as a means of storing passwords.

A secret consists of two components: a name and a secret value.
(There’s some options around activation and expiration that will fall in the aforementioned life cycle management capabilities. I didn’t need them for my script but you can easily add them in if you need them.)
I will be using the username as the name and store the password as the secret value.

Parameters:

  • KeyVaultName – identifies the KeyVault you want to add a secret to
  • UserName – this will be the name of the secret. If you don’t provide one when calling the function, you’ll be prompted for it. Most of the time you’ll likely use this in a script and specify a username.
  • Force – Setting force will override a secret with the same name without confirmation. If you don’t set it, you’ll be asked to confirm.

Why can’t you provide a password? Because you cant trust people not to use this in a script and hard-code their passwords in. This would kind of defeat the purpose… Instead you’ll be prompted with the default Get-Credentials dialogue when setting a password to ensure it’s at least a secure String.

Caution: Be careful with your usernames. KeyVault will only allow alphanumeric characters and dashes!

function Set-KeyVaultCredential {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][String]$KeyVaultName,
        [Parameter(Mandatory=$false)][String]$UserName,
        [Parameter(Mandatory=$false)][switch]$Force
    )

    #Ensure connection to Azure
    if (-not (Get-AzSubscription -ErrorAction SilentlyContinue)) {
        Connect-AzAccount
    }

    #Initialize SetConfirmed
    if ($Force) {
        $SetConfirmed = $true
    }
    else {
        $SetConfirmed = $false
    }

    #No UserName specified
    if ($UserName.Length -eq 0) {
        $CredObj = Get-Credential -Message "Please provide credentials."
        $UserName = $CredObj.UserName
    }
    else {
        $CredObj = Get-Credential -UserName $UserName -Message "Please provide the password for $UserName."
    }

    #Check for existing items
    $ExistingSecrets = (Get-AzKeyVaultSecret -VaultName $KeyVaultName).Name
    if ($UserName -in $ExistingSecrets) {
        if (-not $SetConfirmed) {
            $confirmation = Read-Host "This username already exists. Do you want to override it? (y/n)"
            if ($confirmation -eq 'y') {
                $SetConfirmed = $true
            }
            else {
                $SetConfirmed = $false
                Write-Host "Process halted by user. No change was made." -ForegroundColor Yellow
            }
        }
    }
    else {
        $SetConfirmed = $true
    }
    
    #Set Secret if confirmed
    if ($SetConfirmed) {
        Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $UserName -SecretValue $CredObj.Password
        Write-Host "Credentials for $UserName successfully set."
    }
}

Get-KeyVaultCredential

This will get either all secrets in a KeyVault or a specific one. Results will be returned as PowerShell credential objects.

Parameters:

  • KeyVaultName – identifies the KeyVault you want to get secrets from
  • UserName – if you specify this, you’ll get the a single credential object back, if you don’t specify it, you get a hashtable with the usernames as keys and the credential objects as values.

I think, we’ve established that the cost per transaction is negligible. Know, however, that getting all credentials in the store will use one transaction per secret and a single one on top for the whole KeyVault (n+1 transactions). If you really want to optimize your transaction usage, get the individual secrets at the start of your script (n transactions) and reuse them across it.

function Get-KeyVaultCredential {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][String]$KeyVaultName,
        [Parameter(Mandatory=$false)][String]$UserName
    )

    #Ensure connection to Azure
    if (-not (Get-AzSubscription -ErrorAction SilentlyContinue)) {
        Connect-AzAccount
    }

    #Get all credentials
    if ($UserName.Length -eq 0) {
        #Initialize result variable
        $Result = @{}
		
        #Get all secrets
        $Secrets = Get-AzKeyVaultSecret -VaultName $KeyVaultName
        foreach ($Secret in $Secrets) {
        	#Get individual secret to access the value
            $SecretDetail = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $Secret.Name
            $CredObj = New-Object System.Management.Automation.PSCredential ($SecretDetail.Name, $SecretDetail.SecretValue)
            $Result.Add($SecretDetail.Name,$CredObj)
        }
        #Return all credentials as a hashtable
        return $Result
    }
    #Get specific credential
    else {
        $SecretDetail = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $UserName
        $CredObj = New-Object System.Management.Automation.PSCredential ($SecretDetail.Name, $SecretDetail.SecretValue)
        #Return single credential object
        return $CredObj
    }
}

Examples

Add the credentials for ‘sp-farm’ to the KeyVault, overwrite existing secret

Import-Module KeyVaultCredentials.psm1
$KeyVaultName = '**********'
$UserName = 'sp-farm'
Set-KeyVaultCredential -KeyVaultName $KeyVaultName -UserName $UserName -Force

Get all credentials from the KeyVault

Import-Module KeyVaultCredentials.psm1
$KeyVaultName = '**********'
$Credentials = Get-KeyVaultCredential -KeyVaultName $KeyVaultName
<#
Access individual credentials by name like so:
$Credentials.UserName
#>

Get credentials for ‘sp-farm’ from the KeyVault

Import-Module KeyVaultCredentials.psm1
$KeyVaultName = '**********'
$UserName = 'sp-farm'
$Farm_Cred = Get-KeyVaultCredential -KeyVaultName $KeyVaultName -UserName $UserName

We want it all!

Well then. Here’s the whole thing.
You can save it as a module (e.g. ‘KeyVaultCredentials.psm1’) or just throw the functions in your script if you’re a slob / in a hurry.

function Get-KeyVaultCredential {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][String]$KeyVaultName,
        [Parameter(Mandatory=$false)][String]$UserName
    )

    #Ensure connection to Azure
    if (-not (Get-AzSubscription -ErrorAction SilentlyContinue)) {
        Connect-AzAccount
    }

    #Get all credentials
    if ($UserName.Length -eq 0) {
        #Initialize result variable
        $Result = @{}

        #Get all secrets
        $Secrets = Get-AzKeyVaultSecret -VaultName $KeyVaultName
        foreach ($Secret in $Secrets) {
            #Get individual secret to access the value
            $SecretDetail = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $Secret.Name
            $CredObj = New-Object System.Management.Automation.PSCredential ($SecretDetail.Name, $SecretDetail.SecretValue)
            $Result.Add($SecretDetail.Name,$CredObj)
        }
        #Return all credentials as a hashtable
        return $Result
    }
    #Get specific credential
    else {
        $SecretDetail = Get-AzKeyVaultSecret -VaultName $KeyVaultName -Name $UserName
        $CredObj = New-Object System.Management.Automation.PSCredential ($SecretDetail.Name, $SecretDetail.SecretValue)
        #Return single credential object
        return $CredObj
    }
}

function Set-KeyVaultCredential {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)][String]$KeyVaultName,
        [Parameter(Mandatory=$false)][String]$UserName,
        [Parameter(Mandatory=$false)][switch]$Force
    )

    #Ensure connection to Azure
    if (-not (Get-AzSubscription -ErrorAction SilentlyContinue)) {
        Connect-AzAccount
    }

    #Initialize SetConfirmed
    if ($Force) {
        $SetConfirmed = $true
    }
    else {
        $SetConfirmed = $false
    }

    #No UserName specified
    if ($UserName.Length -eq 0) {
        $CredObj = Get-Credential -Message "Please provide credentials."
        $UserName = $CredObj.UserName
    }
    else {
        $CredObj = Get-Credential -UserName $UserName -Message "Please provide the password for $UserName."
    }

    #Check for existing items
    $ExistingSecrets = (Get-AzKeyVaultSecret -VaultName $KeyVaultName).Name
    if ($UserName -in $ExistingSecrets) {
        if (-not $SetConfirmed) {
            $confirmation = Read-Host "This username already exists. Do you want to override it? (y/n)"
            if ($confirmation -eq 'y') {
                $SetConfirmed = $true
            }
            else {
                $SetConfirmed = $false
                Write-Host "Process halted by user. No change was made." -ForegroundColor Yellow
            }
        }
    }
    else {
        $SetConfirmed = $true
    }
    
    #Set Secret if confirmed
    if ($SetConfirmed) {
        Set-AzKeyVaultSecret -VaultName $KeyVaultName -Name $UserName -SecretValue $CredObj.Password
        Write-Host "Credentials for $UserName successfully set."
    }
}

Export-ModuleMember 'Get-KeyVaultCredential'
Export-ModuleMember 'Set-KeyVaultCredential'

Leave a Reply

Your email address will not be published. Required fields are marked *