DEV Community

Cover image for Using AWS CLI with Powershell's SecretStore Module to protect your Access keys
Fady GA 😎
Fady GA 😎

Posted on

Using AWS CLI with Powershell's SecretStore Module to protect your Access keys

Secrets stores are awesome! 🤩
They are those "Secrets Management Systems" where you can store the secret (like a DB password) as key/value pair with the key is a specific name, "POSTGRESQL_SERVER_PASSWORD" for example and its value is the actual password. Some secrets stores even encrypts those secrets at rest!
I think by now you are starting to see why they are super useful! As your app's source code won't contain your credentials any more. Just you make a call to the Secrets Store and if your app has enough permissions, then it will receive the secret it requested!
How cool is that! 😎

CSPs have also their version of "managed" Secrets Stores. On AWS it's "Secrets Manager" and on Azure it's "Key Vault". And they are extremely useful if you are developing a cloud native app so all you have to do is just tinkering with some permissions and your app is leaks-free (hopefully 😅)

Now after that we saw what are the secrets stores and why we should use them, most likely you are thinking "secrets stores are great! but what they have to do with the AWS CLI?".
My problem with AWS CLI that it is (by default) uses AWS Access keys that you generate from AWS IAM. This is a very dangerous credential specially if it's linked to a user with high privileges inside the AWS account! And not just that, it stores in plain text too! 😮
The Access key is stored as plain text in either the AWS CLI configuration file located at ~/.aws/credentials or if you prefer to not use the config file (or the command aws configure) and use environment variables instead (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY), again you will store them in plain text probably in your shell's "profile" file (for bash, it could be the .bash_profileor .bashrc file and for powershell it could be your profile $PROFILE). But I have a solution for that...

I work mainly on a Windows machine and I use Powershell a lot. Powershell can create a "SecretStore" for your local machine and you can use it via the "SecretManagent" module. So the idea is that I'll use the Powershell's SecretStore in a similar manner to the dedicated secrets stores described earlier to store the AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY credentials to be used by the AWS CLI. And as a bonus, we will encrypt them too! All that is done locally using only Powershell. Piece of cake 😁

Process flow

1- Powershell modules installation:

The following instructions are executed on a Windows machine and Powershell v7.3.2. Some prior knowledge of Powershell will be very helpful.

The SecretsStore module:

Install-Module -Name Microsoft.PowerShell.SecretStore
Enter fullscreen mode Exit fullscreen mode

Module page on PSGallery

The SecretManagement module:

Install-Module -Name Microsoft.PowerShell.SecretManagement
Enter fullscreen mode Exit fullscreen mode

Module page on PSGallery

2- Setup the SecretStore:

The first thing that we want to do after its installation is to configure it. The most important parameters in this setup are the -Password and -PasswordTimeout. Whenever you use the SecretStore, it will ask you for a password and the store will stay unlocked during your Powershell session for the timeout duration. So for my use case, I'll set the timeout to 1 sec so it will lock after that we receive the needed secret(s) and let the cmdlet read the password from the terminal as as SecureString. This way when you enter the password, it will appear as stars (******) and won't appear in your host history.

Set-SecretStoreConfiguration -PasswordTimeout 1
Enter fullscreen mode Exit fullscreen mode

3- Register vaults:

Vaults are a logical containers for your secrets. You can have as many as you want. You can think of them as folders for your secrets for organizational purposes. In my case, I'll create 2 vaults. My main (default) vault MyVault and another one for AWS secrets AWSVault.

Register-SecretVault -Name MyVault -ModuleName Microsoft.Powershell.SecretStore -DefaultVault
Register-SecretVault -Name AWSVault -ModuleName Microsoft.PowerShell.SecretStore

Get-SecretVault    # To list the vaults we have created.
Enter fullscreen mode Exit fullscreen mode

Get-SecretVault

4- Generate a random encryption key and store it the default vault:

An encryption key is necessary as we will store the credentials encrypted. Powershell doesn't have a native way to do that so we will have to be creative! The encryption is done using ConvertFrom-SecureString cmdlet as we will see and the key must be either 128, 196 or 256 bits. We will use a 256 bits key generating an 16 characters long random string (16 * 2 * 8 = 256) (powershell treats characters as 2 byte long in this code snippet. That explains the 2 in the middle)

$chars = ("a".."z") + ("A".."Z") + (0..9) + "@#%[]{}=+"    # CharArray for the permitted characters for the key.
$mykey = ""                                                # Initiate an empty key string.
(1..16) | ForEach-Object { $myKey += $chars | Get-Random } # Looping 16 times and each time appends a random character to the key.
Enter fullscreen mode Exit fullscreen mode

5- Storing the encryption key in the default vault:

Now that we have the encryption key, we will store it in MyVault to be used later in encrypting and decrypting the credentials.

Set-Secret -Vault MyVault -Name AWS_ENCRYPTION_KEY -Secret $myKey
Enter fullscreen mode Exit fullscreen mode

6- Parsing the AWS credentials from a JSON file, encrypt them and store them in the AWS vault:

I don't want to write my credentials as plain text in the shell. It will appear in the shell's history and I'll have to delete it manually. That's a lot of work for me 😂.
One way to avoid that is to write my credential in a disposable JSON file and parse it in powershell then store the parsed credential in the AWSVault

$myCredentials = Get-Content .\MyCredentials.json | ConvertFrom-Json -AsHashtable

$awsAccessKeyIDSecure = ConvertTo-SecureString -String ($myCredentials.AWS_ACCESS_KEY_ID) -AsPlainText
$awsSecretAccessKey = ConvertTo-SecureString -String ($myCredentials.AWS_SECRET_ACCESS_KEY) -AsPlainText
Enter fullscreen mode Exit fullscreen mode

The reason that we are converting the credentials to SecureString is that this is the only input acceptable to the ConvertFrom-SecureString to encrypt them.

$encryptionKey = Get-Secret -Vault MyVault -Name AWS_ENCRYPTION_KEY # secure string

$awsAccessKeyIDEncrypted = ConvertFrom-SecureString $awsAccessKeyIDSecure -SecureKey $encryptionKey   # Normal string but encrypted
$awsSecretAccessKeyEncrypted = ConvertFrom-SecureString $awsSecretAccessKey -SecureKey $encryptionKey
Enter fullscreen mode Exit fullscreen mode

Now, storing them

Set-Secret -Vault AWSVault -Name AWS_ACCESS_KEY_ID -Secret $awsAccessKeyIDEncrypted
Set-Secret -Vault AWSVault -Name AWS_SECRET_ACCESS_KEY -Secret $awsSecretAccessKeyEncrypted
Enter fullscreen mode Exit fullscreen mode

7- Retrieving the AWS credentials from the vault:

As for retrieving the credentials, first we will have to get them from the AWSVault then decrypt them.

$encryptionKey = Get-Secret -Vault MyVault -Name AWS_ENCRYPTION_KEY # secure string

$awsAccessKeyIDVault = Get-Secret -Vault AWSVault -Name AWS_ACCESS_KEY_ID -AsPlainText    # We need them as plain text to be decrypted by ConverTo-SecureString.
$awsSecretAccessKeyVault = Get-Secret -Vault AWSVault -Name AWS_SECRET_ACCESS_KEY -AsPlainText

$awsAccessKeyIDSecure = ConvertTo-SecureString $awsAccessKeyIDVault -SecureKey $encryptionKey                   # Secure string but decrypted.
$awsAccessKeyIDDecrypted = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($awsAccessKeyIDSecure)  # Allocates the secure string to an unmanaged Binary string in memory.
$awsAccessKeyIDDecrypted = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($awsAccessKeyIDDecrypted)  # Allocates the unmanged binary string to a managed string in memory.

$awsSecretAccessKeySecure = ConvertTo-SecureString $awsSecretAccessKeyVault -SecureKey $encryptionKey
$awsSecretAccessKeyDecrypted = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($awsSecretAccessKeySecure)
$awsSecretAccessKeyDecrypted = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($awsSecretAccessKeyDecrypted)
# Sanity check
Write-Host "AWS_ACCESS_KEY_ID: $awsAccessKeyIDDecrypted`nAWS_SECRET_ACCESS_KEY: $awsSecretAccessKeyDecrypted"    # You should get the original credentials from the JSON file.
Enter fullscreen mode Exit fullscreen mode

8- Tying it all up:

So in order to link what have done with the AWS CLI, we will create two functions in the $PROFILE file for powershell to retrieve the AWS key Id and the AWS secret key and store them in two environment variables (AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY) so that they will be retrieved from the AWSVault and won't be hardcoded in either the $PROFILE nor the aws cli credentials file. Also they will be available only for the current session and only if I've entered the SecretVault master password that we set in the beginning.
If you use VSCode you can type in code $PROFILE or you can just open it in any text editor.

# $PROFILE
function Get-AWSAccessKeyID {
    $encryptionKey = Get-Secret -Vault MyVault -Name AWS_ENCRYPTION_KEY
    $awsAccessKeyIDVault = Get-Secret -Vault AWSVault -Name AWS_ACCESS_KEY_ID -AsPlainText

    $awsAccessKeyIDSecure = ConvertTo-SecureString $awsAccessKeyIDVault -SecureKey $encryptionKey
    $awsAccessKeyIDDecrypted = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($awsAccessKeyIDSecure)
    $awsAccessKeyIDDecrypted = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($awsAccessKeyIDDecrypted)
    return $awsAccessKeyIDDecrypted
}

function Get-AWSSecretAccessKey {
    $encryptionKey = Get-Secret -Vault MyVault -Name AWS_ENCRYPTION_KEY
    $awsSecretAccessKeyVault = Get-Secret -Vault AWSVault -Name AWS_SECRET_ACCESS_KEY -AsPlainText

    $awsSecretAccessKeySecure = ConvertTo-SecureString $awsSecretAccessKeyVault -SecureKey $encryptionKey
    $awsSecretAccessKeyDecrypted = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($awsSecretAccessKeySecure)
    $awsSecretAccessKeyDecrypted = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($awsSecretAccessKeyDecrypted)
    return $awsSecretAccessKeyDecrypted
}

$ENV:AWS_ACCESS_KEY_ID = Get-AWSAccessKeyID
$ENV:AWS_SECRET_ACCESS_KEY = Get-AWSSecretAccessKey
Enter fullscreen mode Exit fullscreen mode

For me, I've used the following JSON file

{
  "AWS_ACCESS_KEY_ID": "myAwsAccessKeyId",
  "AWS_SECRET_ACCESS_KEY": "myAwsSecretKey"
}
Enter fullscreen mode Exit fullscreen mode

Don't forget to delete it after you parse it into powershell. If you don't, then you are left out with your credentials written in plain text which will defeat the purpose of this whole post 😉.

And here is how it looks like after you add the previous functions to the $PROFILE and query the configuration of the AWS CLI using aws configure list command. (don't forget to close your terminal and reopen it after you edit the $PROFILE file)

Image description
The credentials are redacted as you can see but the last four letters matches the ones I've used in the JSON file and their type is env. So no credentials in the history, no hardcoded plain-text stored credentials anywhere on my drive and they are stored encrypted! Mission complete! 🥳🎉🎊✨

A little warning though, I use this in my local machine and remember you are storing the credentials in environment variables that could be easily retrieved if someone knows where to look. 😉
In my next post, I'll show a far superior and more secure method to use the AWS CLI locally via SSO (single sign on) like what Azure CLI does.

I've have all the code used in this post available in this repo

That's all folks! Stay secure!

Top comments (0)