AWS Lambda for PowerShell Encrypted Environment Variables

Standard

Recently I was developing a AWS Lambda for PowerShell function to read a table from a Microsoft SQL on RDS Instance within a VPC. Using the .NET Core SQL client I need to provide credentials to access the database. For the password it should be stored encrypted at rest, never in plain text, and when passed to the function should be done so as a secure string. We can configure the function to use KMS for encryption at rest. This presents us with two challenges. How do I encrypt a AWS Lambda environment variable and how will my AWS Lambda for PowerShell function securely get the decrypted value in memory as a secure string when the Lambda function is invoked.

Encrypting an AWS Lambda Environment Variable

Utilizing the AWS Lambda console when configuring the function, under environment variable, select the option to “Enable helpers for encryption in transit”. You’ll need to provide what KMS key you’ll want to encrypt the value with. When we enter a value and select encrypt the console encrypts the value with KMS and stores the value as an encrypted base 64 text string.

 

Enter the unencrypted environment variable and click Encrypt


 

Save and refresh the console to see the value as a encrypted base 64 text string

 

Securely Decrypting Variables with AWS Lambda for PowerShell

So far this is easy however if we want to access the variable we have a problem. The console provides a “code” link for the Lambda function but our PowerShell function is executed in a .NET Core environment and the console displays the helper code as C# which isn’t going to work in my PowerShell function. Thankfully since PowerShell is just a layer of abstraction from C# I was able to create a PowerShell function to convert the encrypted variable to a secure string utilizing the .NET Core KMS client.

function ConvertFrom-KmsEncryptedString
{
<#
    .EXAMPLE
    $SecureString = ConvertFrom-KmsEncryptedString -EncryptedBase64String $env:password
    .PARAMETER EncryptedBase64String
    Encrypted base 64 string to be decrypted.
    .PARAMETER ToPlainText
    Default output is a secure string object, us this to output as a plain text string.
#>
param(
    [Parameter(Mandatory=$true)][string]$EncryptedBase64String,
    [switch]$ToPlainText = $false
)
begin {
    #KMS Client Object
    $KmsClient = New-Object Amazon.KeyManagementService.AmazonKeyManagementServiceClient
    #Convert encrypted base 64 text string to and encrypted bytes array. 
    $EncryptedBytes = [System.Convert]::FromBase64String($EncryptedBase64String)
    $utf8 = [System.Text.UTF8Encoding]::UTF8
    $SecureString = [System.Security.SecureString]::new()
}
process {
    foreach ($byte in $KmsClient.Decrypt($EncryptedBytes,$null)) {
        $SecureString.AppendChar($utf8.GetString($byte))
    }
    #Disposing the KMS client to release its resources.
    $kmsClient.Dispose()
    #Makes the secure string value immutable.
    $SecureString.MakeReadOnly()   
}
end {
    if ($ToPlainText.IsPresent -eq $true) {
        #Returns the plain text string of the encrypted environment variable when -PlainText switch is used.
        Return [System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecurestringToBstr($SecureString))
    }
    else {
        #Returns a secure string of the decrypted environment variable.
        Return $SecureString
    }
}
}

The function input will be the $env:password environment variable key containing the encrypted base 64 text string. The default output will be a secure string object unless you use the flag for output as plain text. Here’s an example of the function being used to pass a secure string to my other function which connects to the Microsoft SQL database.

$SecureString = ConvertFrom-KmsEncryptedString -EncryptedBase64String $env:password
$Datatable = Get-SqlToDataTable -Instance $env:instance -Database $env:database -Table $env:table -UserName $env:username -Password $SecureString

Conclusion

Utilizing this method our secure value is never stored as plain text. It’s passed to the Lambda execution environment as an encrypted base 64 text value. The ConvertFrom-KmsEncryptedString function uses the KMS client over HTTPS and loads the decrypted bytes into a secure string so it’s protected in memory. I experimented with lots of methods to do this as securely and easily as possible. One consideration is that if I used the KMS client async decrypt the value is returned as a unencrypted memory stream object in the response.I looked at using crypto streams but was overly complicated and had more overhead. Using the KMS client synchronous decrypt call and appending the bytes to the secure string gave me the desired results with the least amount of complexity.

Let me know if you have any recommendations or suggestions. Thank you!

5 thoughts on “AWS Lambda for PowerShell Encrypted Environment Variables

  1. I had this working yesterday following this tutorial, but today it was no longer working. I was wondering if something on AWS changed? I get the following error message when my lambda executes: “[Error] – Exception calling “Decrypt” with “2” argument(s): “A task was canceled.”” The error is arising from this block of code:

    foreach ($byte in $KmsClient.Decrypt($EncryptedBytes,$null)) {
    $SecureString.AppendChar($utf8.GetString($byte))
    }

    Like

    • Chris Fitch

      TaskCancelledException usually occurs when the Lambda function duration hit the function timeout. Being that it worked yesterday and not today it’s probably because of function cold start. Try increasing the timeout value on the Lambda function.

      Like

    • Chris Fitch

      Interesting. When I looked at the API documentation for the Decrypt API call the exception you mentioned isn’t listed as one of the possible options. I searching for task canceled exception and found there’s an issue log for the .NET SDK task waiter that might be the issue. Are you using the latest version of PS Core and the AWS SDK for .NET Core? If you used the PowerShell command to build the Lambda function it does it locally and then pushed to S3. Which could explain why it’s happening local and on Lambda. The only other thing I can think of is you’re having permission issues to KMS. Maybe the encryption helper got changed, the CMK used by the function to decrypt the base64encoded text. Also if you set encryption context that could be the issue, but unlikely since you said it did work then it didn’t.

      Like

      • Chris Fitch

        I just tested it on Lambda function I have deployed and on my local box and it’s working for me. I’m not sure what could have changed that it worked one day then not the next. Do you have the log data from the Lambda function in one logs in CloudWatch logs?

        Like

Leave a Reply to Drew (@dbudwin) Cancel reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.