Ensuring secure values by private keys in AWS (KMS, SSM, Secrets Manager)
Private keys, access credentials, and sensitive information such as passwords or any kind of confidential data are essential in application's logic when interacting with other services or even databases. Storing those values can be risky if proper security measures are not in place.
In this post we will dig into one way to accomplish secure access to sensitive information in AWS environments. For simplicity we will be using:
- A ready to use AWS account.
- Serverless Framework - to delegate the CloudFormation creation and deployment.
This implementation focuses on the use of specifics services and functionalities provided by AWS services. Implementations will vary depending on the provider (Azure, GCP).
Let's start by creating a basic AWS Lambda
This function will try to access our "secure" and use it. By using npx
command we get access to serverless executable. I recommend you select the basic settings to keep it simple, but don't forget there are options for different types of uses cases, even different languages options in case Node.js
is not your language.
Some operations used on the lambda function will require some permissions later. Due to this, it's recommended to include and enable those permissions before the IAM role of the lambda gets created. Here are the permissions we need the lambda to have:
provider:
# ...
iam:
role:
statements:
- Effect: Allow
Action:
- "s3:*"
Resource: "*"
- Effect: Allow
Action:
- "cloudwatch:*"
- "logs:*"
Resource: "*"
- Effect: Allow
Action:
- "ssm:GetParameter"
- "ssm:GetParametersByPath"
Resource: "*"
- Effect: Allow
Action:
- "secretsmanager:GetSecretValue"
Resource: "*"
- Effect: Allow
Action:
- "kms:Decrypt"
Resource: "*"
Then, with your aws account already set up, let's deploy it.
npx sls deploy
๐ After a correct deployment, the code should have been created successfully and be ready to use. Now we need to configure the most important part: security.
Security
Permissions will be managed via IAM policies with the objective of only allow the lambda to get and decrypt the secured value. Plus, the approach used will be based on: prevent IAM entities from accessing the KMS key and allow the root user account to manage it (also preventing the root user account from losing access to the KMS key).
This "protection" layer of our sensitive value is based on the composition of two AWS services: Key Management Service is the one responsible for creating the private key which will be used to encrypt our value, then Secret Manager or System Manager: Parameters Store allow us to accomplish the same functionality but they differ on some caveats, since the chosen one will be used for saving the secured encrypted value. Here is a quick graphic representation of the implementation:
Now lets translate these concepts into code. The first thing we need after having the lambda functional and usable is to create the key used to encrypt our precious value...
So lets create the key using the "root" account and give it access to lambda just for "kms:Decrypt"
, not other operations!
Please notice how we control and specify that only a specific ARN can call this value by:
kms:CallerArn: !Sub arn:aws:lambda:region:${AWS::AccountId}:function:myfunction
resources:
MyPrivateKey:
Type: AWS::KMS::Key
Properties:
Description: "My KMS private key"
KeyUsage: ENCRYPT_DECRYPT
KeyPolicy:
Version: "2012-10-17"
Id: "my-key-1"
Statement:
- Sid: "Enable root user permissions"
Effect: "Allow"
Principal:
AWS: !Sub arn:aws:iam::${AWS::AccountId}:root
Action: "kms:*"
Resource: "*"
- Sid: "Allow use of the key"
Effect: "Allow"
Principal:
Service: lambda.amazonaws.com
Action:
- "kms:Decrypt"
Resource: !Sub arn:aws:kms:region:${AWS::AccountId}:key/my-key-1
Condition:
StringEquals:
kms:CallerArn: !Sub arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:myfunction
MyPrivateKeyAlias:
Type: "AWS::KMS::Alias"
Properties:
AliasName: alias/myprivatekey
TargetKeyId: !Ref MyPrivateKey
Outputs:
# we will need this value later
MyKeyId:
Value: !Ref MyPrivateKey
Description: The ID of the KMS key used to encrypt
In this stage you should be able to have the lambda function and a secret key which both the root user and the lambda ARN can access and use it. Now, how about creating our encrypted values?
For simplicity I personally prefer AWS CLI to handle the process of encrypting and saving instead of creating another lambda script that would need to use AWS SDK, ending in the same result ๐คท๐ฝ
So first, we need to encrypt our secret using the previous key already created, so that even if someone is able to access the value, they will need our key to be able to see the content,
Some of the values used below are not exposed in the serverless deploy output, so those values are gotten via the stack output previously defined as part of serverless.yml
:
npx sls info --verbose
--plaintext (data to be encrypted) is required to be base64 text or a blob from a plain text file
aws kms encrypt \
--region <same region used by serverless> \
--key-id <key id from output (MyPrivateKeyId)> \
--plaintext "<base64 of value> or <fileb://path>" \
--output text \
--query CiphertextBlob
๐ You should be able to get a base64 output from the command above with the encrypted value, now what next?
Store the values
We need to save those values inside a secure service, either a AWS System Manager (using Parameter Store) or a AWS Secret Manager creating directly a key/value. Why not try both?
For Parameter Store
For reusability use the previous encrypted output directly with the same command, getting as result:
aws ssm put-parameter \
--name "my-parameter-name" \
--type "SecureString" \
--value "$(aws kms encrypt --region us-east-1 --key-id <key id> --plaintext "<value>" --output text --query CiphertextBlob | base64 --decode)" \
--region us-east-1
If everything works, you should receive a json payload like this, which means it works, and that the value was created successfully ๐๐ฝ
Access via AWS Systems Manager -> Parameter Store to see it.
{
"Version": 1,
"Tier": "Standard"
}
For Secrets Manager
Secret manager use the same approach as Parameter store, but with a different command, let's see:
aws secretsmanager create-secret --name "my-secret" --secret-string "my-secret-value"
Running this will create a new secret in AWS Secrets Manager with the name my-secret
and a secret value of my-secret-value
. If the command is successful, it will return a JSON object containing metadata about the newly created secret, including its Amazon Resource Name (ARN).
Let's encrypt and send it to secretsmanager
as follow:
aws secretsmanager create-secret --name "my-secret" --secret-string "$(aws kms encrypt --region <region> --key-id alias/myprivatekey --plaintext "<value>" --output text --query CiphertextBlob)" --region <region>
๐ฅณ Getting the following payload means it works:
{
"ARN": "arn:aws:secretsmanager:us-east-1:<account id>:secret:my-secret-j3MXy5",
"Name": "my-secret",
"VersionId": "<version id>"
}
Access via AWS Secrets Manager -> Secrets to see it.
Now with both scenarios covered, let's move on and try to access those values from code. Going back to the lambda function, we need to update the code to get and use the value as follow but before let's make sure we can retrieve and decrypt both saved values:
Check that you are able to decrypt the values, use the command below:
# Using system manager (parameter store)
aws kms decrypt \
--ciphertext-blob fileb://<(aws ssm get-parameter --region <region> --name "my-parameter-name" --with-decryption --query Parameter.Value --output text | base64 --decode) \
--key-id alias/myprivatekey \
--output text \
--query Plaintext | base64 --decode
# For secrets manager
aws kms decrypt \
--ciphertext-blob fileb://<(aws secretsmanager get-secret-value --region <region> --secret-id "my-secret" --query SecretString --output text | base64 --decode) \
--key-id alias/myprivatekey \
--output text \
--query Plaintext | base64 --decode
โจ You should be able to get you real value, in our case was my precious value
(base64 encoded at start). Here is what the test code implementation could look like:
This should be the output from logs:
START RequestId: xxxxxxx Version: $LATEST
2023-04-15T21:51:48.684Z xxxxxxx INFO from parameters store: my precious value
2023-04-15T21:51:48.684Z xxxxxxx INFO from secrets manager: my precious value
END RequestId: xxxxxxx
REPORT RequestId: xxxxxxx Duration: 255.71 ms Billed Duration: 256 ms Memory Size: 1024 MB Max Memory Used: 97 MB Init Duration: 699.66 ms
All the code is available here if you want to test it.
Hope it helps, cheers ๐ป