How to use SSM Parameter Store for sensitive inputs in Terraform
Reference a parameter and programmatically fetch the current value
Sensitive inputs
While IAM Roles provide a great way to remove secrets from the architecture, there are still many cases when they needed. For example, the application needs to interface with a 3rd-party endpoint and that requires a token. And here comes the dilemma: the application needs to know this token, but how to deploy it so that it remains a secret?
A token like this is sensitive information. Whoever can read it can use it to impersonate the application. As such, it should not be hardcoded in the application nor should it be a simple input variable.
# don't do this
variable "secret_token" {
type = string
}
Input variables show up in the state and wherever you use them. In the most common scenario, this means an environment variable for a Lambda function. And the environment should not contain sensitive values.
A better solution is to use a service specifically made for handling secrets: SSM Parameters Store or Secrets Manager. In this article, we'll implement a solution usin the former.
Here, the sensitive value is already present in the secret storage and Terraform only gets a pointer to that. This is then passed to the Lambda's environment and the sensitive value itself is fetched only before calling the 3rd-party API.
Implementation
Let's see how to implement it!
Step 1: Add the parameter
First thing is to add the parameter itself to the account. This is usually done by someone else who has access to the sensitive value and has to be done before deploying the application.
To do this, go to the SSM Parameters Store console and add a new parameter. Give it a name and set the type to be SecureString
. This is important to make
sure it won't accidentally show up in the Terrform state.
Step 2: Add a data source
Next, we need an input variable and a data source:
variable "secret_parameter_name" {
type = string
}
data "aws_ssm_parameter" "secret" {
name = var.secret_parameter_name
with_decryption = false
}
Other resources can then use the data.aws_ssm_parameter.secret
with all its attributes.
Step 3: Permissions
Since the application will fetch the sensitive value when it needs it, the stack needs to set the correct permissions. In the case of a Lambda function, this is done via the execution role.
data "aws_iam_policy_document" "lambda_exec_role_policy" {
statement {
actions = [
"ssm:GetParameter"
]
resources = [
data.aws_ssm_parameter.secret.arn,
]
}
}
Step 4: Pass the reference
The only thing remaining for the architecture is to pass the name of the parameter to the function.
resource "aws_lambda_function" "lambda" {
# ...
environment {
variables = {
secret = data.aws_ssm_parameter.secret.name
}
}
}
Step 5: Fetch the current value
Finally, the function needs to fetch the current value.
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
const result = (await new SSMClient().send(new GetParameterCommand({
Name: process.env.secret,
WithDecryption: true,
}))).Parameter.Value;
To speed up this call and handle standard limits, see this article.
Terraform state
Let's look into the terraform.tfstate
file to see what information it contains related to the parameter!
{
"arn": "arn:aws:ssm:eu-central-1:278868411450:parameter/secret_token",
"id": "secret_token",
"name": "secret_token",
"type": "SecureString",
"value": "AQICAHijrjzaSBQmvgZokJxS+9/SxOZm+LsTyw/Jt...",
"version": 1,
"with_decryption": false
}
There is a value
field but it's encrypted due to the with_decryption: false
config. Since the encryption key is not available, this is no longer
sensitive data.
Missing parameter
What happens if the parameter does not exist?
Fortunately, Terraform handles it the right way: it throws an error.
$ terraform apply
var.secret_parameter_name
Enter a value: not_exist
random_id.id: Refreshing state... [id=1clt9emS8B4]
aws_iam_role.lambda_exec: Refreshing state... [id=terraform-20220920084014369600000001]
╷
│ Error: Error describing SSM parameter (not_exist): ParameterNotFound:
│
│ with data.aws_ssm_parameter.secret,
│ on main.tf line 12, in data "aws_ssm_parameter" "secret":
│ 12: data "aws_ssm_parameter" "secret" {
With this, there is no need for extra validation.
Conclusion
Sensitive values require great care as it's very easy to accidentally leak them. Fortunately, with SSM Parameters Store and the aws_ssm_parameter
data
source, it's straightforward to set up everything in a secure way.