If account credential could be avoided being stored in the script, attempt that first. If the predefined / privileged account used is a Windows account (eg. local or domain account), and its process is executed through some task like Windows task scheduler, the privileged account could be setup as the running account of the task and let Windows handles the credential encryption securely during the task setup. When the delegated user initiate the task on demand or the task is executed in a schedule automatically, the predefined account as the running account is used to access all the required resources with Windows integrated authentication.
However often cases different Windows account(s) or third party software account(s) are used to access different type of resource within the execution, In these cases, it is likely that there will be a need to manage the account credential encryption ourselves.
Powershell provides some native command for encryption. The commonly use command is the ConvertTo-SecureString and ConvertFrom-SecureString.
Take an example. this script is to obtain some server information of a remote server. However, the user who run the Powershell script does not have the access to a remote server.
Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA
As the executing account (user Windows account) doesn't have enough privilege, the access denied error is encountered
Get-WmiObject : Access is denied. (Exception from HRESULT: 0x80070005 (E_ACCESSDENIED))
At line:1 char:2
+ Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA
+ CategoryInfo : NotSpecified: (:) [Get-WmiObject], UnauthorizedAccessException
+ FullyQualifiedErrorId : System.UnauthorizedAccessException,Microsoft.PowerShell.Commands.GetWmiObjectCommand
To incorporate a predefined Windows account which has been granted permission to the server to obtain this information,
$User = 'DomainA/TestUser' $Password = 'Password123' $SecurePassword = $Password | ConvertTo-SecureString -AsPlainText -Force $UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword) Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
With the password in plain text, we know we need to encrypt the password here.
The ConvertTo-SecureString cmdlet converts the password into a System.Security.SecureString object. The password has been encrypted in this case (by default) using the user account during the script execution. We will discuss some issues with this default setting later in this post.
To pre-encrypt the password, we could use the same command and hard code the encrypted password in the script, save it in another file, in the registry or some other places.
Store encrypted password in a file (txt file)
To save the encrypted password text into a file,
$User = 'TestUser' $Password = 'Password123' $Password | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString | Out-File C:\encrypted.txt
ConvertFrom-SecureString cmdlet convert the System.Security.SecureString into text (not the original password, but encrypted text). The encrypted password text (eg. '01000000d08c9ddf0115d1118c7a00c04fc29...') is then save into a file.
A better approach is not displaying the password in clear text at all,
$UserCred = Get-Credential $UserCred.Password | ConvertFrom-SecureString | Out-File C:\encrypted.txt
The Get-Credential prompt for user name and password as directly save into a secure PSCredential object.
Decrypt encrypted password in a file (txt file)
With the password encrypted as stored in the file, now the script simply have to extract the encrypted password and pass it to the PSCredential object,
$User = 'TestUser' $SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString $UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword) Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Store encrypted password in a file using Export-Clixml (xml file)
Another option is to save it as xml file. We could save the PSCredential object with both user and encrypted password to a xml file.
Get-Credential | Export-Clixml -Path C:\encrypted.xml
Decrypt encrypted password in a file using Import-Clixml (xml file)
To load the xml directly back into a PSCredential object
$UserCred = Import-Clixml -Path C:\encrypted.xml Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Note that the ConvertTo-SecureString converts the encrypted password text into a SecureString object. A lot of (but not all) Windows and 3rd party Powershell cmdlet utilize the SecureString object for authentication purpose like PSCredential object or Compellent Get-SCConnection. In general, the password does not need to be decrypted back to plain text as string object as it is exposed in the memory until it is removed by the garbage collector.
However, some cmdlet does not utilize the SecureString and require password in plain text. The SecureString object in this case will need to be decrypted into to plain text. If the account information has been constructed into a PSCredential object, the password could be extracted in plain text,
$Password = $UserCred.GetNetworkCredential().password
Store encrypted password in a file for 3rd party password (txt file)
In the case of encrypting a third party account password,
$SecurePassword = Read-Host -AsSecureString $SecurePassword | ConvertFrom-SecureString | Out-File C:\encrypted.txt
Decrypt encrypted password in a file for 3rd party password (txt file)
Decrypt 3rd party password into plain text by converting the SecureString into Binary String object,
$User = 'TestUser' $SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString $Marshal = [System.Runtime.InteropServices.Marshal] $Bstr = $Marshal::SecureStringToBSTR($SecurePassword) $Password = $Marshal::PtrToStringAuto($Bstr) $Marshal::ZeroFreeBSTR($Bstr)
The last line ZeroFreeBSTR is to clear the unmanaged memory.
Store encrypted password in registry
Another method of saving encrypted password is to save it in registry. The example below uses HKCU (HKEY_CURRENT_USER) hive which the registry only applicable to the current user. You may want to use other more appropriate hives (eg. HKLM) for all other users access.
$SecurePasswordText = 'Password123' | ConvertTo-SecureString -AsPlainText -Force | ConvertFrom-SecureString New-Item -Path HKCU:\Software\Test -Value $SecurePasswordText
$UserCred = Get-Credential New-Item -Path HKCU:\Software\Test -Value ($UserCred.Password | ConvertFrom-SecureString)
OR storing both user name and password in the registry,
New-Item -Path HKCU:\Software\Test New-ItemProperty -Path HKCU:\Software\Test -Name User -Value ($UserCred.UserName New-ItemProperty -Path HKCU:\Software\Test -Name Password -Value ($UserCred.Password | ConvertFrom-SecureString)
To decrypt encrypted password stored in registry
With the password encrypted as stored in the registry, here is how to extract it for PSCredential in the script,
$User = 'TestUser' $SecurePassword = (Get-ItemProperty -Path HKCU:\Software\Test).'(Default)' | ConvertTo-SecureString $UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword) Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
$User = (Get-ItemProperty -Path HKCU:\Software\Test).User $SecurePassword = (Get-ItemProperty -Path HKCU:\Software\Test).Password | ConvertTo-SecureString $UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword) Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Issue with default encryption using user account
By default, ConvertTo-SecureString cmdlet uses current user's password to generate an encryption key, and it is stored within the user profile (eg. %Userprofile%\Application Data\Microsoft\Crypto\RSA\User SID for RSA key). The encryption key is then used to encrypt the intended string. The same user's user profile is created independently on different computer. Unless the particular person's user account has been set as roaming profile, the encryption key on his user profile on one computer does not synchronize with his user profile on another computer. This creates some issues. If a person pre-encrypt the password on his computer, then deploy that to another server. First, the encrypted password text can't be decrypted because the encryption key is not present on the person' user profile of the server. Needless to say, other users won't be able to decrypt the password as well because they don't have the encryption key. This is the error received.
ConvertTo-SecureString : Key not valid for use in specified state.
At line:1 char:336
+ ... bf6ff4d7ae3" | ConvertTo-SecureString
+ CategoryInfo : InvalidArgument: (:) [ConvertTo-SecureString], CryptographicException
+ FullyQualifiedErrorId : ImportSecureString_InvalidArgument_CryptographicError,Microsoft.PowerShell.Commands.Conv
If it is only to resolve the issue locally on one server, a task could be setup in a way that always use the same running account which perform execute the script. This way, we could encrypt the predefined account password using the running account. As the running account has the encryption key stored in its user profile, it is able to decrypt the password during task execution regardless who initiated the task. However, that also means a person will be the developer of the script, the admin of the server, and have access to the running account credential.
To perform encryption with a running account, we could login to the server as the running account, or use the RunAs command in our own remote session to open a Powershell session as the running account to perform the encryption,
runas /profile /user:RunningAcctA powershell.exe
Note that running account profile needs to be loaded in order to store the encryption key as discussed earlier. /Profile is a default parameter. The /profile parameter is listed as an explicit example.
In the Powershell session. use whoami to identify the account of that session. Once verified, generate the encrypted password file from that session.
This trick applies to using LocalSystem account as well. The easiest way to run a Powershell session as LocalSystem is using PsExec by Mark Russinovich from Microsoft. You can find this download from this Windows internal link or I have some PsExec example here in this post as well toward the middle section.
psexec -s -i powershell.exe
Keep in mind that if the script is migrated to other server, the encryption step need to be re-performed again as the previous encryption key generated only store in the running account profile of that particular server.
Custom Encryption Key
One other way to address the multiple server and different user issue is to use a specific encryption key. ConvertTo-SecureString cmdlet allows a key to be provided for the encryption. The valid encryption key lengths are 16, 24, and 32 bytes. With the use of encryption key, it allows the encrypted password to be decrypted on different server with different account.
To generate a valid key, we could use RNGCryptoServiceProvider class to generate random number for the key.
$EncryptKey = New-Object Byte 16 #An example of 16 bytes key [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($EncryptKey)
In this example, a simple 1 to 16 array is used as the key.
[bytes] $EncryptKey = (1..16) #An example of a simple key (1. 2, 3,...,14, 15, 16) $UserCred = Get-Credential $UserCred.Password | ConvertFrom-SecureString -Key $EncryptedKey | Out-File C:\encrypted.txt
In the script,
[bytes] $EncryptedKey = (1..16) $User = 'TestUser' $SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString -Key $EncryptedKey $UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword) Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
However, by hard cording the encryption key in the script, it exposes the key and allows unintended person to potentially decrypt the encrypted password. There are a few options to manage the key.
Store the key in a file with access privilege granted only to intended user or executing/service account, this should be done with the encrypted password file as well. For example,
$User = 'TestUser' $SecureKey = Get-Content C:\Key.txt | ConvertTo-SecureString $SecurePassword = Get-Content C:\encrypted.txt | ConvertTo-SecureString -SecureKey $SecureKey $UserCred = New-Object System.Management.Automation.PSCredential ($User, $SecurePassword) Get-WmiObject -Class win32_OperatingSystem -ComputerName RemoteServerA -Credential $UserCred
Just a reminder that if the process is going to be executed by a delegated user, the user account will need the read access to the encrypted password and key file. This method prevent unintended person from obtaining the key. However, if the intent is to prevent the delegated user to obtain the password, this method would not be sufficient. One way to address this is setup a task / service to execute the script with a service account (executing account) and only that account has the read access (through Access Control List, ACL) to the encrypted password and key file. When the delegated user initiates the task / service, the process utilizes the service (executing account) to access the files. This limits the exposure of the encrypted password and key file only to the service account.
There are other option like using a certificate to encrypt the key file. Dave Wyatt has a good post of this here. The way it works is the user needs to have a private key of the certificate in order to decrypt the encryption key. This option faces the similar concern like the key file.
Using DPAPI ProtectedData Class for encryption
DPAPI ProtectedData class provides another method to encrypt and decrypt data. As we discussed earlier that ConvertTo-SecureString uses user account by default, this Protected class provide encryption options as CurrentUser or LocalMachine (LocalSystem profile). For scenario like having delegated users to run the script on the server, the predefined account password could be encrypted with LocalMachine option (scope) and any user could decrypt the password on that machine.
$Password = "Password123" $PasswordBytes = [System.Text.Encoding]::Unicode.GetBytes($Password) $SecurePassword = [Security.Cryptography.ProtectedData]::Protect($PasswordBytes, $null, [Security.Cryptography.DataProtectionScope]::LocalMachine) $SecurePasswordStr = [System.Convert]::ToBase64String($SecurePassword)
To decrypt the encrypted password,
#$SecurePasswordStr $SecureStr = [System.Convert]::FromBase64String($SecurePasswordStr) $StringBytes = [Security.Cryptography.ProtectedData]::Unprotect($SecureStr, $null, [Security.Cryptography.DataProtectionScope]::LocalMachine) $PasswordStr = [System.Text.Encoding]::Unicode.GetString($StringBytes)
Note that ProtectedData class is returning byte array object (and later convert to string) as opposed to SecureString object. If other cmdlet need the SecureString as its parameter, the password will need to be converted to the SecureString object.