Recently I’ve been working on several PowerShell scripts that require credentials to access REST APIs. In this blog post, I will showcase two approaches for storing credentials securely for use in PowerShell scripts.

Encrypted Password File πŸ”’

The encrypted password file leverages the Windows Data Protection API (DPAPI) to encrypt the password as a System.Security.SecureString:

$Credentials = Get-Credential
$Credentials.Password
System.Security.SecureString
$Credentials.Password | ConvertFrom-SecureString
01000000d08c9ddf0115d1118c7a00c04fc297eb01000000f5ab85d7ee9da048ae4ae797ee7eaf0a000000000200000000001066000000010000200000008c4a03d2f0731e0e7661d695fda8b441eaff31e75724931f31374a0c8292b636000000000e800000000200002000000028da885828bd627480178382ce9a1b477819e7703546ce41819d37f4e63d33ba20000000ab2c4401635ec24db9f20071e18dea0b79ce16ba38b5503ec9937b7fbc849dcf40000000155053a793c210998ef7317b0161e7344c2174b904b527c0cf24e7bbf2243b99e936df3ab67bc9e285a1be33aed37c7604fb07f5d0c44ceb7d6334ca30b0a610

By default DPAPI uses the current user context to generate an encryption key. This encryption key is then used to encrypt the PSCredential.Password property as a System.Security.SecureString (as shown above). It is possible to provide your own encryption key, but I won’t be covering that in this post. If you want to read more on this, check out Travis Gan’s blog 1.

It’s also worth noting there are several caveats to this approach:

  1. You must encrypt the password file as the user which will be accessing it.
  2. DPAPI is specific to the device which you encrypt the password file on. You cannot decrypt the password file on another system with the same user.

Generating and using the Encrypted Password File

To store the password securely in a file, we can use the Export-Clixml cmdlet which will store the System.Management.Automation.PSCredential object in XML format:

$Credentials = Get-Credential
$Credentials | Export-Clixml -Path "$(pwd)\EncryptedCreds.xml"

Once created, the EncryptedCreds.xml file will contain the XML representation of the PSCredential object. As shown below, the Password property is stored as a SecureString:

<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
  <Obj RefId="0">
    <TN RefId="0">
      <T>System.Management.Automation.PSCredential</T>
      <T>System.Object</T>
    </TN>
    <ToString>System.Management.Automation.PSCredential</ToString>
    <Props>
      <S N="UserName">User</S>
      <SS N="Password">01000000d08c9ddf0115d1118c7a00c04fc297eb01000000f5ab85d7ee9da048ae4ae797ee7eaf0a000000000200000000001066000000010000200000008c4a03d2f0731e0e7661d695fda8b441eaff31e75724931f31374a0c8292b636000000000e800000000200002000000028da885828bd627480178382ce9a1b477819e7703546ce41819d37f4e63d33ba20000000ab2c4401635ec24db9f20071e18dea0b79ce16ba38b5503ec9937b7fbc849dcf40000000155053a793c210998ef7317b0161e7344c2174b904b527c0cf24e7bbf2243b99e936df3ab67bc9e285a1be33aed37c7604fb07f5d0c44ceb7d6334ca30b0a610</SS>
    </Props>
  </Obj>
</Objs>

We can then re-create the PSCredential object for use in PowerShell scripts using the Import-Clixml cmdlet:

$Credentials = Import-Clixml -Path "$(pwd)\EncryptedCreds.xml"
$Credentials

UserName                     Password
--------                     --------
User     System.Security.SecureString

$Credentials.GetType()

IsPublic IsSerial Name                                     BaseType
-------- -------- ----                                     --------
True     True     PSCredential                             System.Object

Write-Output -InputObject "Username: ""$($Credential.UserName)"" - Password: ""$($Credential.GetNetworkCredential().Password)"""
Username: "User" - Password: "Password123!"

Tada! ✨

This approach can be good for one off scripts but, can quickly become a maintainance burden when you’re doing this for many scripts. For this type of use-case, the next approach is probably more suitable.

SecretManagement & SecretStore Modules πŸ”

The SecretManagement and SecretStore modules are a great choice for storing credentials securely. To begin, install the modules:

Install-Module -Name "Microsoft.PowerShell.SecretManagement", "Microsoft.PowerShell.SecretStore" -Verbose

Creating a SecretVault

Next, we need to create a SecretVault to store secrets using the Register-SecretVault cmdlet πŸ”:

Register-SecretVault -Name "MySecretVault" -ModuleName "Microsoft.PowerShell.SecretStore" -DefaultVault -Description "Secret vault storing my secrets." -PassThru

Name          ModuleName                       IsDefaultVault
----          ----------                       --------------
MySecretVault Microsoft.PowerShell.SecretStore True

Storing Secrets

Nice! 👍 Now lets store a PSCredential in the SecretVault with the Set-Secret cmdlet. When creating the first secret you will be prompted for a password which will be used to encrypt the SecretVault:

β„Ή You can store a SecureString, HashTable, String or Byte[] in a secret as well!

$RestAPICreds = Get-Credential
Set-Secret -Name "REST API Creds" -Vault "MySecretVault" -Secret $RestAPICreds

Creating a new MySecretVault vault. A password is required by the current store configuration.
Enter password:
********
Enter password again for verification:
********

# Bonus! Store metadata alongside the secret itself!
$Metadata = @{ SecretExpiration = ([DateTime]::New(2022, 12, 22)) }
Set-Secret -Name "REST API Creds" -Vault "MySecretVault" -Secret $RestAPICreds -Metadata $Metadata

Retrieving Secrets

Now that the secret is stored securely, we can retrieve it in another script using the Get-Secret cmdlet:

$RestAPICredentialsSecret = Get-Secret -Name "REST API Credentials" -Vault "MySecretVault"
$RestAPICredentialsSecret

UserName                     Password
--------                     --------
test     System.Security.SecureString

# Bonus! Get information about a secret too!
# https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.secretmanagement/get-secretinfo?view=ps-modules
Get-SecretInfo -Name "REST API Credentials" -Vault "MySecretVault"

Name                 Type         VaultName
----                 ----         ---------
REST API Credentials PSCredential MySecretVault

# As it's a PSCredential object, we can use the GetNetworkCredential method to finally get the password!
$RestAPICredentialsSecret.GetNetworkCredential().Password

Awesome! 😎 We’ve stored the first secret securely! βœ¨πŸš€

However, if you use the Get-Secret cmdlet in a new session, or after a certain amount of time has elapsed, you may notice that you’re prompted for the password to decrypt the SecretVault again! 😱

Get-Secret -Name "REST API Credentials" -Vault "MySecretVault"
Vault MySecretVault requires a password.
Enter password:

Obviously this is an issue if we want our scripts to work without any interaction!

Retrieving Secrets Non-Interactively

To fix this we can leverage the first approach and store our SecretVault decryption password in an encrypted password file and retrieve it later! So… lets do it! 😄

$SecretVaultPassword = Get-Credential
$SecretVaultPassword | Export-Clixml -Path "$(pwd)\SecretVaultPassword.xml"
Get-Content -Path "$(pwd)\SecretVaultPassword.xml"

...
<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04">
    <Props>
      ...
      <SS N="Password">700061007300730077006f0072006400</SS>
    </Props>
  </Obj>

Once done, we can use the Unlock-SecretStore cmdlet in our scripts to unlock the SecretVault non-interactively:

$SecretVaultPassword = (Import-Clixml -Path "$(pwd)\SecretVaultPassword.xml").Password
Unlock-SecretStore -Password $SecretVaultPassword

Automation FTW! βš™ πŸ’ͺ

Modifying SecretVault Configuration

Finally, you may also want to modify the configuration of a SecretVault. You can do this using the SecretStore cmdlets Get-SecretStoreConfiguration and Set-SecretStoreConfiguration.

Let’s check the SecretVault configuration with the Get-SecretStoreConfiguration cmdlet:

Get-SecretStoreConfiguration

      Scope Authentication PasswordTimeout Interaction
      ----- -------------- --------------- -----------
CurrentUser       Password             900      Prompt

We can see that the SecretVault configuration is set to prompt for a password, and the timeout before being prompted for the password again is 900 seconds (15 minutes) by default.

The timeout can be changed using the Set-SecretStoreConfiguration cmdlet:

Set-SecretStoreConfiguration -PasswordTimeout 1800 -Confirm:$false
# Check the configuration again
Get-SecretStoreConfiguration

      Scope Authentication PasswordTimeout Interaction
      ----- -------------- --------------- -----------
CurrentUser       Password            1800      Prompt

Conclusion

Finished! 😄 Hopefully you learned something new and found this post helpful! :slight_smile: There is a lot that I didn’t cover when it comes to the SecretManagement module. For example, extensions which create integrations with third-party secret management products like Azure KeyVault, KeePass, HashiCorp Vault and more!

Until the next one! πŸ‘‹

References