Using a certificate to encrypt credentials in automated PowerShell scripts

PowerShell is a great way to help automate frequent or repetitive tasks and every now and then these tasks require some form of authentication. You could just store the service account password in the script, but I’m really not a fan of doing that and I’m sure you’d agree it really isn’t a very good way to do it. I was working on a script recently which was to be scheduled to run at various times by different service accounts on different servers so I wanted a way to ensure that a single copy of the script could be portable to any server and would still securely connect to Exchange with the correct permissions no matter which service account actually executed the script. Remotely connecting to Exchange/Exchange Online via PowerShell isn’t difficult to do and you could just use Get-Credential cmdlet with ConvertFrom-SecureString and Set-Content to securely save your password to file which could then be read by your script without subsequent intervention. The trouble with this solution is that it isn’t very portable and that password can only be ready by the user that created the file so it will work great if you used it only on your own machine, but not so well when trying to distribute it to a bunch of servers as a scheduled job. I wanted a solution that would use a particular certificate to decrypt a password stored in the script, in that way if the script was executed on a machine that did not have my certificate installed it would not be able to decrypt the password and would fail. I ended up creating an encrypted password using the public key of a certificate and storing that in the script, the only way to decrypt that password is with the private key of the same certificate. Let’s look at this in more detail.

The first component of the solution is a certificate. Since I already had access to a internal Windows CA, I wanted to use a certificate signed by that CA but I also tested it with a self-signed certificate that was generated using “makecert.exe”. PowerShell 4.0 includes a New-SelfSignedCertificate cmdlet that makes generating a self-signed certificate really easy, but for some reason I wasn’t able to use one of those certificates for encryption (more specifically the decryption would not work) and since I planned to use a CA signed certificate anyway I didn’t spend a whole lot of time trying to figure it out. The key thing to remember is that you need to install the certificate AND the private key, the certificate doesn’t have to be trusted. I decided to create a new certificate template for my “Script Authentication” certificate by duplicating the “Web Server” template and making a few changes to it.

Firstly, launch the Certification Authority MMC, locate “Certificate Templates”, right-click and select “Manage”

Capture1

Next, locate the “Web Server” template, right-click and select “Duplicate Template”

Capture2

I called my new template “Script Authentication”

Capture3

And you need to ensure that you “Allow private key to be exported”. Once done, apply those changes.

Capture4

We then need to publish the new template to ensure that it can be used when requesting a new certificate. Right-click “Certificate Templates”, select “New” and then “Certificate Template to Issue” and select your newly created template from the list

Capture5

Once done, you should be able to create and submit an advanced certificate request using the newly created template directly on your CA. You are not required to complete all the fields, but it is useful to give your certificate a descriptive name. I called mine “PowerShell Automation”.

Capture6

Once you have the installed the certificate, you can export it (don’t forget the private key) for use on other machines.

Capture8

I recommend storing it in a safe place and not marking the private key as exportable when moving it around, in this way you have some control over which machines can actually decrypt the password in your script.

Capture9

Here’s what my certificate looks like

Capture7

I decided to store my certificate in the computer store, it probably doesn’t matter where you store it but you would need to update the following PowerShell cmdlets appropriately. If you are going to have multiple service accounts executing your script, you need to ensure that all these accounts have permission to read the private key. This is done by right-clicking the certificate, selecting “All Tasks” and then “Manage Private Keys”.

We can use the Get-ChildItem cmdlet to locate our certificate:

Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "CN=PowerShell Automation*"}

Next, I need to encrypt my password. To do this, I define the password as a variable, encode it and then encrypt the encoded password using my certificate’s public key:

$Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "CN=PowerShell Automation*"}
$Password = 'MyPassword'
$EncodedPwd = [system.text.encoding]::UTF8.GetBytes($Password)
$EncryptedBytes = $Cert.PublicKey.Key.Encrypt($EncodedPwd, $true)
$EncryptedPwd = [System.Convert]::ToBase64String($EncryptedBytes)

Capture10

Now that I have the encrypted password, I can store it in my script and decode it using my certificate’s private key each time the script is executed. To do this, I pretty much reverse the process:

$EncryptedPwd = "ts32rCLLdZl3/6wINHtLD6bQO65ub….. <shortened for simplicity>"
$EncryptedBytes = [System.Convert]::FromBase64String($EncryptedPwd)
$DecryptedBytes = $Cert.PrivateKey.Decrypt($EncryptedBytes, $true)
$DecryptedPwd = [system.text.encoding]::UTF8.GetString($DecryptedBytes)

You can build this into any scripts you have that currently require credentials, it works great for automating Office 365/Exchange Online scripting. To illustrate this, I put together a quick (and dirty!) script that can be used to provide an automated daily “Top Mail Recipient” report via email. This script can be scheduled to run daily and it will connect to Exchange Online, generate a list of the top mail recipients and email that report to the address you specify. It’s not very useful as it is, but it does show how easily you could automate things using PowerShell and serves as a great example for certificate password encryption.

# Function to create report email
function SendReport{
 $Msg = New-Object Net.Mail.MailMessage
 $Smtp = New-Object Net.Mail.SmtpClient($ExchangeServer)
 $Msg.From = $FromAddress
 $Msg.To.Add($ToAddress)
 $Msg.Subject = "Top Mail Recipient Report for $Date"
 $Msg.Body = $EmailBody
 $Msg.IsBodyHTML = $true
 $Smtp.Send($Msg)
}

# Define local Exchange server info for message relay. Ensure that any servers running this script have permission to relay.
$ExchangeServer = "yourexchange.domain.com"
$FromAddress = "Office 365 Reports <reports@yourdomain.com>"
$ToAddress = "you@yourdomain.com"

# Some basic HTML styling
$Header = "<style>"
$Header = $Header + "BODY{background-color:#FFFFFF}"
$Header = $Header + "TABLE{border-width: 1px;border-style: solid;border-color: black;border-collapse: collapse;}"
$Header = $Header + "TH{border-width: 1px;padding: 0px;border-style: solid;border-color: black;background-color:#BDBDBD}"
$Header = $Header + "TD{border-width: 1px;padding: 0px;border-style: solid;border-color: black}"
$Header = $Header + "</style>"

# Connect to Exchange Online
# First decrypt the password using the certificate
$Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like "CN=PowerShell Automation*"}
$EncryptedPwd = "ts32rCLLdZl3/6wINHtLD6bQO65ubeQ3sHj9zXbhsaQDjihQmdyoja+iL0NGXQX0DicQdXWQRu+P8dSy96ux1tLQR9ZT8WPRq8rHsR3gNXDmipCK/4CHoc5Ki7nbMKUSReprtIrnwjlXZNBocTzurBQ+LtAHvAYipD37AXVjjpwwwqud5HCXk+E4OrJGe+yIx/87neRAunqdKvyuaxUYaxeBdx2R/hpLZhxywinjjVMx+0N2RNk7H3fBEite7uuANcAg+ElAssi4DAQYYDOviIrvbjdpKogKcevAh5xEx4Wm2WBzM5XqXmj1O9TUzB9BOiUVQhDwwqCcUpb2bTNW7g=="
$EncryptedBytes = [System.Convert]::FromBase64String($EncryptedPwd)
$DecryptedBytes = $Cert.PrivateKey.Decrypt($EncryptedBytes, $true)
$DecryptedPwd = [system.text.encoding]::UTF8.GetString($DecryptedBytes) | ConvertTo-SecureString -AsPlainText -Force
# Then define Credentials and create session
$Username = "account@yourdomain.onmicrosoft.com"
$Credential = New-Object System.Management.Automation.PSCredential ($Username,$DecryptedPwd)
$Session = New-PSSession -ConfigurationName Microsoft.Exchange -ConnectionUri https://outlook.office365.com/powershell-liveid/ -Credential $Credential -Authentication Basic -AllowRedirection
Import-PSSession $Session

# Generate report data
$Date = Get-Date -DisplayHint Date
$EmailBody = Get-MailTrafficSummaryReport -Category TopMailRecipient | select @{expression={$_.C1};label=”User”}, @{expression={$_.C2};label=”Item Count”} | ConvertTo-HTML -head $Header -body "<H2>Top Mail Recipient Report</H2>"

# Send report
SendReport

Here is an example of what the final result looks like:

Capture11

Share this?Print this pageEmail to someoneTweet about this on TwitterShare on LinkedInShare on FacebookDigg thisGoogle+Pin on PinterestShare on StumbleUponShare on Redditshare on TumblrBuffer this pageFlattr the author

Post navigation


Comments

  • Tim Pringle

    Really nice article Chris, looking forward to trying this out.

  • Dave Wyatt

    Hi Chris,

    You might be interested in two options that make this easier. In the v5 preview of PowerShell (and in PowerShell 4.0 on fully-patched Windows 8.1 / 2012 R2 computers), there are new cmdlets called Protect-CmsMessage and Unprotect-CmsMessage. You give them a certificate and the data you want to encrypt / decrypt, and they take care of the rest (while producing standards-based CMS output to represent the encrypted data.)

    Also, I wrote a module called ProtectedData (available on GitHub and PowerShellGet) which does much the same thing, but is compatible all the way back to PowerShell 2.0, and has a few other features that the CmsMessage cmdlets don’t. (However, its output is not the CMS standard format, which means there’s little to no chance for cross-platform utility.) Under the hood, it does almost exactly what you’re doing here, but the calling code is much simplified:

    $Cert = Get-ChildItem Cert:\LocalMachine\My | Where-Object {$_.Subject -like “CN=PowerShell Automation*”}

    # To encrypt:
    Protect-Data ‘MyPassword’ -Certificate $Cert -SkipCertificateVerification | Export-Clixml .\encrypted.xml

    # To decrypt:
    Unprotect-Data (Import-Clixml .\encrypted.xml) -Certificate $Cert -SkipCertificateVerification

  • Chris

    Thanks Tim!

  • Chris

    Thanks for the great info Dave.

  • Jason

    Really like this Chris. I can see some changes to my script cache over the coming days.

Leave a Reply

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

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>