How to use SSM Parameter Store for sensitive inputs in Terraform

Reference a parameter and programmatically fetch the current value

Author's image
Tamás Sallai
4 mins

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.

The SSM parameter

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.

November 15, 2022