How to securely generate and store IAM Secret Access Keys with Terraform

A solution running in the cloud that stores a Secret Access Key in SSM Parameter Store

Author's image
Tamás Sallai
4 mins

IAM identities

Generally, it's a bad idea to use IAM users in cases when roles are also an option. This is because roles provide a secret-less way for systems to gain permissions to AWS resources, while a user's Secret Access Key is a sensitive information that must be protected.

AWS provides built-in support for roles for most services. For example, a Lambda function has an execution role that allows attaching permissions and the runtime automatically makes the credentials avaiable to the function. Also, EC2 instances can get permissions via their instance profiles. Using these approaches provides a secure solution: all the keys are short-lived and there is no secret to lose.

That's the general rule, but there are exceptions. I needed to use an IAM user instead of a role when I wanted the credential part of an S3 signed URL to be always the same. In that case, roles are not a good solution as their Access Key ID changes every time the role is assumed.

In this article, we'll look into how create a Secret Access Key in a secure way. Note though that a solution without secrets is always more secure than the one with them, so opt for IAM roles whenever possible.

Permissions

First, let's create a user and attach permissions to it! After all, an access key for a user is only as useful as the policies attached to it.

Generate the user:

resource "aws_iam_user" "signer" {
	name = "signer-${random_id.id.hex}"
}

Then attach some policies:

resource "aws_iam_user_policy" "signer" {
	user = aws_iam_user.signer.name

	policy = jsonencode({
		Version = "2012-10-17"
		Statement = [
			{
				Action = [
					"s3:GetObject",
				]
				Effect   = "Allow"
				Resource = "${aws_s3_bucket.images.arn}/*"
			},
		]
	})
}
IAM user

Generating credentials

While it's tempting to generate the access key and pass the resulting Access Key ID and the Secret Access Key as environment variables, don't do this. Environment variables are not secure, for example, the ReadOnlyAccess managed policy allows reading them. What to do instead is to store the Secret Access Key in SSM Parameter Store and only pass a reference to it via environment variables.

Then the next question is: who is generating the user credentials?

The safest way is to deploy a Lambda function with the necessary permissions to generate and store the credentials and call it during deployment. This way even the person (or process) who is doing the deployment does not have access to the secret value.

There is a Terraform module that handles the boilerplate of configuring and calling the Lambda function as well as managing the SSM parameter: ssm-generated-value.

To use it, implement the custom part of generating and deleting access keys:

import {IAMClient, CreateAccessKeyCommand, ListAccessKeysCommand, DeleteAccessKeyCommand} from "@aws-sdk/client-iam";

const client = new IAMClient();
const UserName = "${aws_iam_user.signer.name}";

export const generate = async () => {
	const result = await client.send(new CreateAccessKeyCommand({
		UserName,
	}));
	return {
		value: result.AccessKey.SecretAccessKey,
		outputs: {
			AccessKeyId: result.AccessKey.AccessKeyId,
		}
	};
}

export const cleanup = async () => {
	const list = await client.send(new ListAccessKeysCommand({
		UserName,
	}));
	await Promise.all(list.AccessKeyMetadata.map(async ({AccessKeyId}) => {
		await client.send(new DeleteAccessKeyCommand({
			UserName,
			AccessKeyId,
		}));
	}));
}

Then define what extra permissions the Lambda function needs:

extra_statements = [
	{
		"Action": [
			"iam:CreateAccessKey",
			"iam:ListAccessKeys",
			"iam:DeleteAccessKey"
		],
		"Effect": "Allow",
		"Resource": aws_iam_user.signer.arn
	}
]

The module adds these extra permissions to the ones needed to manage the SSM parameter:

Permissions of the Lambda

When it's deployed, it creates an SSM parameter with the Secret Access Key:

The Secret Access Key stored in an SSM parameter

It also outputs the Access Key ID (that is public) and the SSM parameter's name and ARN. Then any component that needs access can use its IAM permissions to fetch the secret and send requests as the IAM user:

resource "aws_lambda_function" "backend" {
	# ...
	environment {
		variables = {
			SECRET_ACCESS_KEY_PARAMETER = module.access_key.parameter_name
			ACCESS_KEY_ID = jsondecode(module.access_key.outputs).AccessKeyId
		}
	}
}
October 31, 2023
In this article