One of the most common tasks out in the field is the need to run PowerShell scripts that require credentials to be saved in some form of another so that they can be fed into scripts to be executed autonomously.  This is particularly common in cloud environments where the current user context in which the scripts are run (e.g. within a scheduled task) are insufficient or not appropriate for the remote execution.   Office 365 management is a perfect example of this, where often a credential object must be passed in order to connect and execute the cmdlets.

The quick and dirty (and all too common) approach is to simply have some code like the following:

Here we are simply writing the username and password directly into the PowerShell script as plaintext and then creating a PowerShell credential object that can then be passed onto things like Connect-MsolService.

It works, but obviously terribly insecure.  Particularly since these accounts are often high powered administrator accounts.

A better approach is to leverage the inherent ‘secure string’ nature of PowerShell credential objects and the way it represents passwords.  Specifically, you can export the ‘secure string’ into a readable string by doing something like the following:

This will give you a nice long, complex looking string like so (shortened for conciseness):  1000000d08c9ddf0115d1…..d3d491bb6d740864122a041d11

You can store that string into a text file, and when needed, read it back in and reverse it back into a ‘Secure String’ object and feed into a credential object creation by doing the following:

What is effectively happening here is that PowerShell is using the native Windows Data Protection API (DAPI) functionality to encrypt the password from the ‘secure string’ into a text string.  This string can be written to a plain text file, but the way that DAPI works is that the encryption is such that only the original user on the original machine the encryption was performed on can decrypt the string back into a ‘Secure string’ to be reused.

While not a 100% foolproof solution, it is a pretty effective way to secure the password as it reduces the attack vectors significantly.  At a minimum as long as the credentials to the account that originally ran the encryption are protected, even if a would be malicious administrator had access to the machine (and thus would have had access to your plaintext passwords stored in your script), they wouldn’t be able to reverse engineer the password that has been stored.

From that perspective your process to have a PowerShell script with a secure ‘saved’ password would be as follows:

  1. Run the Get-Credential command to prompt an administrator to provide the credentials they wish to save
  2. Convert the secure-string object that is part of that credential object into a text string (which is now encrypted) and store that in a file
  3. For scripts that need the saved credentials, read in the file, decrypt the string and recreate the credential object and feed to the appropriate cmdlets.

There a few key caveats with this approach:

  • The script that runs and reads the saved credentials, must be run on the same machine and in the same user context.
  • This means you can’t just copy the ‘saved credential’ file to other machines and reuse it.
  • In the scenario where your scripts are run as scheduled tasks under a service account, be aware that in order to prompt an admin to provide the credentials in the first place, the service account requires ‘Interactive’ ability.  This means the service account, at least temporarily, needs ‘log on locally’ to give you that interactive session.
  • In order for DAPI to work, the GPO setting Network Access: Do not allow storage of passwords and credentials for network authentication must be set to Disabled (or not configured).  Otherwise the encryption key will only last for the lifetime of the user session (i.e. upon user logoff or a machine reboot, the key is lost and it cannot decrypt the secure string text)

Now, assuming the above caveats are no good for you.  Say, for whatever reason, you can’t give your service accounts the ability to log on locally, even temporarily.  Or, the scripts that need these saved passwords need to be run on a bajillion machines and you can’t afford to log onto each one to create a secure string unique for that machine.  There is one alternative, albeit a less secure one, and realistically speaking, only slightly more secure then storing it as plain text.

In the above example, when you are performing the Convert-FromSecureString cmdlet with no parameters, you are effectively telling PowerShell to do the encryption using DAPI. You can however also provide a specific AES Key for it to use to perform the encryption instead.  In the below example, I’m generating a random AES Key to use:

To re-read the password, the following is done:

In that scenario, the AES Key is your secret, and anyone who knows the AES Key can decrypt your password, so it’s not a great idea to simply embed the AES key into your script.   So for the above example, I export the AES Key as a separate text file, that I would then recommend you secure using something like NTFS ACLs.  That way at a minimum, your security barrier is access to that ‘password file’.

Arguably you could have just done that with a plain text password file, but adding the extra layers of encryption/decryption is intended to stop the generic lazy malicious/curious/whatever administrator (or user!) from easily seeing your passwords.

With this, the AES approach to encrypting your password the process now becomes:

  1. Run the Get-Credential command to prompt an administrator to provide the credentials they wish to save
  2. Generate a random AES Key to convert the secure-string object.  Store the AES key and the secure string text as separate files.  Secure these files with NTFS permissions.
  3. For scripts that need the saved credentials, read in both files and recreate the credential object and feed to the appropriate cmdlets.

The main differences with this approach are:

  • The password files can be generated once and subsequently used on any machine by any user, as long as they can read in the files
  • The security barrier here are the NTFS permissions on the password file.

Script Template

Now, if you’ve made it this far, I congratulate you and reward you with this – all that mumbo jumbo that I spoke about above has been packaged together into a nice, easy to use PowerShell script template. This template is structured so that you can simply embed your core ‘worker’ code into the main function, and the template will handle all the rest.  It includes the following handy features:

  • Two ‘modes’:  One to collect and prepare the credential files (i.e. interactively), and another to execute the core worker code (i.e. the mode you run in the scheduled task)
  • Provides the user the option of which encryption method they wish to use, the more secure DPAPI (default) way or the more flexible AES way.
  • Code will determine automatically which method to use for decryption based on what credential files have been created
  • PLUS all those boring error handling and logging functions are included for no extra charge

Enjoy!

Download Link via GitHub

Category:
Office 365, PowerShell, Security
Tags:
,

Join the conversation! 30 Comments

  1. C:\Scripts\Script-Template-WithCreds.ps1
    At C:\Scripts\Script-Template-WithCreds.ps1:55 char:1
    + [CmdletBinding()]
    + ~~~~~~~~~~~~~~~~~
    Unexpected attribute ‘CmdletBinding’.
    At C:\Scripts\Script-Template-WithCreds.ps1:56 char:1
    + Param(([switch]$PrepareCredentials, [switch]$Execution, $param1, $par …
    + ~~~~~
    Unexpected token ‘Param’ in expression or statement.
    At C:\Scripts\Script-Template-WithCreds.ps1:56 char:74
    + … m(([switch]$PrepareCredentials, [switch]$Execution, $param1, $param2)
    + ~
    Missing closing ‘)’ in expression.
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : UnexpectedAttribute

    • Hi HiltonT,

      Thanks for the heads up! It looks like a rogue ‘(‘ character snuck in 🙂
      I’ve fixed the gist repo now, but if you want to modify your own version, you’ll note that in line 46 of the code it has param(([switch]….. please remove one of the ‘(‘ characters and you’ll be good to go!

      Cheers,
      Dave.

  2. Great article. I was happy to see you pointing out that rebooting can cause the encrypted file to not read anymore. Sadly my server has the network access policy set to disabled already. I will post back if I find a solution, I would rather stick with this one if possible.

    • Hi Chris,

      Glad you found the post helpful. Unfortunately I couldn’t find a way to work around the DPAPI issue with that GPO setting applied. Many hairs was lost doing so 🙂 It’s actually specifically the reason why I added the ‘less secure’ AES key file approach.

      One other technique that was suggested to me via a colleague was that the AES key approach could be further improved by utilising the private key of a PKI certificate to actually be the source of that AES key. The process would basically involve:

      1) Generate/obtain a certificate from any PKI cert authority
      2) Import the certificate with the private key (for security, do not allow it to be exported) in the computer certificate store
      3) Grant ACLs to the certificate to allow your service account to read the private key of that certificate
      4) Update the scripts to look for that specific certificate and read the private key and use that as the AES key for encrypting the password
      5) This approach effectively secures your password under the cryptographic certficiate store mechanisms within Windows

      Never got around to streamlining this approach to make it practical though, but you’re welcomed to give it a crack 🙂

      Regards,
      Dave.

  3. Hi,
    I have a question. I got to create the AES.key and file.txt, but now I would want to execute my script .PS1. How do I do that using this script ??

    • Hi Anderson,

      Simply fill in the part of the script that I have commented and use the $credObject as your creoudential parameter. Then when you execute the script using the -Execution parameter – it will trigger that part of the script to read in the credentials and exectute your code accordingly.

  4. Hi David,

    Fantastic article. I’ve decided to try portions of the code and during my testing I’ve come across an issue I’m hoping you can help with.
    I’ve successfully exported the secure string to a csv and I import it back in.
    I can verify that the variable contains the string I’ve previously exported, but when I attempt to ConvertTo-SecureString I get the following error:
    ConvertTo-SecureString : Cannot bind argument to parameter ‘String’ because it is null.
    Any suggestions?

    sreck

    • Hi Sreck

      The error your described is most commonly due to the variable name being wrong (i.e. typo and hence referring to a non-existent variable, giving a null value) or the parameter is not correctly defined and auto-parameter mapping is not working.

      Which example set of code are you trying? If you show me your sample code, can probably work it out from here 🙂

      Cheers,
      Dave.

  5. Hi David,

    Just dropped by to say great article!

    Cheers,

    Nick

  6. This has helped me get my head around the storing of secure passwords. However when I am trying your sample scripts I get to this part
    $password = $passwordSecureString | ConvertFrom-SecureString -Key $AESKey and get the following error
    “ConvertFrom-SecureString : Cannot bind argument to parameter ‘SecureString’ because it is null.”

    Looking through all the previous parts of the sample I can’t see a reference to $passwordSecureString anywhere. I am just wondering what I am missing or what should be the variable that is entered at this point? I appreciate your assistance.

    • Hey Angelo!

      Glad I could help!
      In regards to your question, that’s a bit of sloppy code consistency across my examples there 🙂

      So here’s a more practical example of how you would use my example code:

      # Prompt you to enter the username and password
      $credObject = Get-Credential

      # The credObject now holds the password in a ‘securestring’ format
      $passwordSecureString = $credObject.password

      # Define a location to store the AESKey
      $AESKeyFilePath = “C:\temp\aeskey.txt”
      # Define a location to store the file that hosts the encrypted password
      $credentialFilePath = “C:\temp\credpassword.txt”

      # Generate a random AES Encryption Key.
      $AESKey = New-Object Byte[] 32
      [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($AESKey)

      # Store the AESKey into a file. This file should be protected! (e.g. ACL on the file to allow only select people to read)

      Set-Content $AESKeyFilePath $AESKey # Any existing AES Key file will be overwritten

      $password = $passwordSecureString | ConvertFrom-SecureString -Key $AESKey

      Add-Content $credentialFilePath $password

      Hope that helps!
      Also don’t forget to check out the link at the end of the post – I have a powershell template that has all this wrapped up in an easy to use format so you don’t have to do all the extra coding! 🙂

  7. Really great article.. in fact I was looking for the approach to follow to store the credentials and reuse in my windows forms projects I prepare for my team…

    I have got few queries…

    1. I understand how to create/store the credentials using . \MyCredentials.ps1 -PrepareCredentials but reuse part is not clear for me.. for exmaple, if I have a buttton in my form against which the credentalls should be validated once clicked ?

    2. I want to store multiple credentials… is there a way to store multiple credentials instead of getting same place file overwritten ?

    • Hi Aamir,

      Glad I could be of assistance! In answer to your questions:

      1. So if you’re referring to my script template, you’ll see under the Execution chunk of the code the following bits of code:

      $credFiles = Get-Content $credentialFilePath
      $userName = $credFiles[0]
      if($decryptMode -eq “DPAPI”)
      {
      $password = $credFiles[1] | ConvertTo-SecureString
      }
      elseif($decryptMode -eq “AES”)
      {
      $password = $credFiles[1] | ConvertTo-SecureString -Key $AESKey
      }
      else
      {
      # Placeholder in case there are other decrypt modes
      }
      $credObject = New-Object System.Management.Automation.PSCredential -ArgumentList $userName, $password

      From here I’m simply reading in the files (after working out which files exist to determine what mode we are using) and using the ConvertTo-SecureString cmdlet to regenerate the password secure string to feed into the creation of the PSCredential object. You can then feed that credential object into any other cmdlets, e.g.
      Connect-MsolService -credential $credObject

      2. In my script template, I assume a single credential. But you could modify it so that instead of overwriting the file, you append. But I guess the trick is for reuse, you’ll need to know which position your stored credentials are – so you’ll need to code in some extra logic to handle that.

  8. My only issue with this approach is the fact that you can display the actual password through the $securePwd variable.

    Granted, you have bigger problems if someone with malicious intentions has access to the box and is able to execute the script locally.

    • Hi Seal,

      Spot on! This approach is certainly not fool proof – it simply provides a barrier of entry. You’ll notice that I’ve even been a bit lax by having debug code that allows the password to be written out to files as well (terrible practice!) 🙂

      Ultimately the principals of least privilege still need to apply – don’t grant your svc accounts more permissions than necessary, don’t allow unnecessary administrators onto machines that hold scripts, don’t allow access to modify scripts etc.

      Cheers,
      Dave.

  9. You are just awesome. You made my day. I was struggling to fix this issue since 4 days but couldn’t succeeded but now with help of this article, it’s fixed now. A big thanks to you again 🙂

  10. What if we have the account has MFA enable? Can we we use the app password with secure string?

    • In theory yes, but in general you should probably start de-emphasising the use of app passwords due to their lower security profile. Also app passwords don’t work when you’re using things like Azure AD to control MFA.

      What I’d recommend is that in scenarios where you need to use a dedicated service account it should be excluded from MFA policies and have a long, secured password. You could even consider using Azure AD to apply other controls such as ensuring that service account is only used from Trusted Locations (as an example).

  11. Hi,
    im struggling with this script that i would like get working. the main problem is that the automatic insert of credentials does not work at all, it does not even insert the username but hte credential window pops up. the first part is a file copy that works just fine but when installation starts thats were the problem comes. any advise would be great. Thanks.

    remove-item c:\temp\* -recurse -ErrorAction Ignore
    new-item -ItemType Directory -Path “c:\temp” -ErrorAction Ignore
    $sourcePath = ‘\\192.168.7.11\Download\Etos’
    $destinationPath = ‘C:\temp\Etos’
    $files = Get-ChildItem -Path $sourcePath -Recurse
    $filecount = $files.count
    $i=0
    $host.privatedata.ProgressForegroundColor = “white”;
    $host.privatedata.ProgressBackgroundColor = “blue”;
    Foreach ($file in $files) {
    $i++
    Write-Progress -activity “KOPIERAR FILER…….. INSTALLATIONEN STARTAR AUTOMATISKT………” -status “($i of $filecount) $file” -percentcomplete (($i/$filecount)*100)

    # Determine the absolute path of this object’s parent container. This is stored as a different attribute on file and folder objects so we use an if block to cater for both
    if ($file.psiscontainer) {$sourcefilecontainer = $file.parent} else {$sourcefilecontainer = $file.directory}

    # Calculate the path of the parent folder relative to the source folder
    $relativepath = $sourcefilecontainer.fullname.SubString($sourcepath.length)

    # Copy the object to the appropriate folder within the destination folder
    copy-Item $file.fullname ($destinationPath + $relativepath)
    }
    $User = “admin”
    $PasswordFile = “\\192.168.7.11\Download\Scripts\test2.txt”
    $KeyFile = “\\192.168.7.11\Download\Scripts\test2.key”
    $key = Get-Content $KeyFile
    $MyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList $User, (Get-Content $PasswordFile | ConvertTo-SecureString -Key $key)
    Start-Process setup.exe -filepath C:\temp\Etos -Credential $MyCredential

  12. Still referring to this article at the end of 2018! Thank you

  13. Had intended to also mention that you can add these scripts to your PowerShell Profile to further save time and automate your processes: my profile.ps1 contains several Global Variables, such as $usrname, $pwdTxt, $securePwd, and $credObject. If I need to update my password, I created a Save-Password function that will prompt for new credentials, update my cred.file, and then Exit. Upon re-launching Shell, my new password is obtained. From there, I can run my other functions, such as Connect-Exch which uses my saved creds to launch a remote PowerShell session to our On-Prem Exchange server and my Connect-365 function to use my saved creds to connect to Office 365 and Exchange Online. Storing my creds to a file makes connecting to any of these services and apps so much faster and easier!

  14. Great source for my requirement. much appreciated.

  15. I really appreciated the step-by-step explanation. It helped me understand an existing script that uses the key-file technique. However I do have some sort of subtle Windows permissions error going on, perhaps you may have some insight or an inspired guess.

    To provide a PowerShell service to a Unix host, we installed Cygwin and setup SSH so that an inbound connection, from the right user on the right host, with the right public/private key pair, causes the automatic execution of a PowerShell script that sends back the requested information. This works surprisingly well. The Posh script itself needs to have credentials to access the cloud to get the info, so we use the stored password technique described here. While doing development, everything was working fine. But when put into production, at some point, the execution of the script started failing with:

    “ConvertTo-SecureString : Key not valid for use in specified state.”

    The actual command is:

    $pas = Get-Content $pwdSpec |
    ConvertTo-SecureString -key (Get-Content $SITE_KEY_SPEC)

    So I remote session into the server using the service account that owns the script, poke around, and for good measure, re-save the account’s password. The service starts working again.

    Days later, it starts failing again with the above error. I run the query from the Unix system several times to confirm it is consistently failing with this error. I then remote session into the account — and doing nothing else — and the service starts working again.

    Any ideas about what security voodoo is happening here?

  16. Param(
    [Parameter(Mandatory=$true)]
    [ValidateNotNullOrEmpty()]
    [Security.SecureString]$secureStringPassword = $(Throw “Password required”)
    )

    Function Convert-ByteArrayToHex {
    [cmdletbinding()]
    param(
    [parameter(Mandatory=$true)]
    [Byte[]]
    $Bytes
    )
    $HexString = [System.Text.StringBuilder]::new($Bytes.Length * 2)
    ForEach($byte in $Bytes){
    $HexString.AppendFormat(“{0:x2}”, $byte) | Out-Null
    }
    $HexString.ToString()
    }

    Function Convert-HexToByteArray {
    [cmdletbinding()]
    param(
    [parameter(Mandatory=$true)]
    [String]
    $HexString
    )
    $Bytes = [byte[]]::new($HexString.Length / 2)
    For($i=0; $i -lt $HexString.Length; $i+=2){
    $Bytes[$i/2] = [convert]::ToByte($HexString.Substring($i, 2), 16)
    }
    $Bytes
    }

    # Generate a random AES Encryption Key.
    $arrayByteKey = New-Object Byte[] 32
    [Security.Cryptography.RNGCryptoServiceProvider]::Create().GetBytes($arrayByteKey)

    # Write to key file
    $stringHexKey = Convert-ByteArrayToHex($arrayByteKey)
    Set-Content “key.txt” $stringHexKey

    # Write to encoded password file
    $stringHexEncodedPassword = $secureStringPassword | ConvertFrom-SecureString -Key $arrayByteKey
    Set-Content “credential.txt” $stringHexEncodedPassword

    # Read from key file
    $stringHexKeyRead = Get-Content “key.txt”
    $arrayByteKeyRead = Convert-HexToByteArray($stringHexKeyRead)
    Write-Host($byteArrayKeyRead)

    #read from encoded password file
    $stringHexEncodedPasswordRead = Get-Content “credential.txt”

    $secureStringPasswordRead = $stringHexEncodedPasswordRead | ConvertTo-SecureString -Key $arrayByteKeyRead

    Write-Host([System.Net.NetworkCredential]::new(“”, $secureStringPasswordRead).Password)

  17. Very slick demo — just what I needed. Thanks!

  18. hi
    i want to save credentials and pass it to i.e Facebook “username” and “password”, is it possible?

Comments are closed.