Create an Assumable Identity for an AWS role
Chainguard’s assumable identities are identities that can be assumed by external applications or workflows in order to perform certain tasks that would otherwise have to be done by a human.
This procedural tutorial outlines how to create an identity using Terraform, and then create an AWS role that will assume the identity to interact with Chainguard resources. This can be used to authorize requests from AWS Lambda, ECS, EKS, or any other AWS service that supports IAM roles for service accounts.
Prerequisites
To complete this guide, you will need the following.
terraform
installed on your local machine. Terraform is an open-source Infrastructure as Code tool which this guide will use to create various cloud resources. Follow the official Terraform documentation for instructions on installing the tool.chainctl
— the Chainguard command line interface tool — installed on your local machine. Follow our guide on How to Installchainctl
to set this up.- An AWS account with the AWS CLI installed and configured. The Terraform provider for AWS uses credentials configured via the AWS CLI.
- A recent version of Go to test the identity with AWS Lambda.
Creating Terraform Files
We will be using Terraform to create an identity for an AWS role to assume. This step outlines how to create the Terraform configuration files that, together, will produce such an identity.
These files are available in Chainguard’s GitHub Repository of Platform Examples.
To help explain each configuration file’s purpose, we will go over what they do one by one. First, though, create a directory to hold the Terraform configuration and navigate into it.
mkdir ~/aws-id && cd $_
This will help make it easier to clean up your system at the end of this guide.
main.tf
The first file, which we will call main.tf
, will serve as the scaffolding for our Terraform infrastructure.
The file will consist of the following content.
terraform {
required_providers {
aws = { source = "hashicorp/aws" }
chainguard = { source = "chainguard-dev/chainguard" }
ko = { source = "ko-build/ko" }
}
}
This is a fairly barebones Terraform configuration file, but we will define the rest of the resources in the other two files. In main.tf
, we declare and initialize the Chainguard Terraform provider.
Next we’ll create lambda.tf
to define the AWS resources to run the Lambda function, and chainguard.tf
to define the Chainguard resources that the Lambda function will interact with.
lambda.tf
The lambda.tf
describes the AWS role that a Lambda function will run as.
data "aws_iam_policy_document" "lambda" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
data "aws_iam_policy" "lambda" {
name = "AWSLambdaBasicExecutionRole"
}
resource "aws_iam_role" "lambda" {
name = "aws-auth"
assume_role_policy = data.aws_iam_policy_document.lambda.json
managed_policy_arns = [data.aws_iam_policy.lambda.arn]
}
This describes an AWS role that a Lambda function will run as. The Lambda function will assume this role, and then use the Chainguard identity to interact with Chainguard resources.
The lambda.tf
file also creates an AWS Lambda function that will assume the identity you created in the previous section. This function will then use the identity to interact with Chainguard resources.
The final section defines a AWS lambda function implemented in Go that will assume the identity you created in the previous section:
resource "aws_lambda_function" "test_lambda" {
filename = "lambda_function_payload.zip"
function_name = "lambda_function_name"
role = aws_iam_role.iam_for_lambda.arn
handler = "bootstrap"
source_code_hash = data.archive_file.lambda.output_base64sha256
runtime = "go1.x"
}
Check out this basic example for configuring AWS Lambda using Terraform, and the docs for deploying Go Lambda functions with .zip file archives for more information on how to configure the filename
and source_code_hash
fields.
Below we’ll go over some of the specific details from the example Go application.
After it’s deployed, when this function is invoked, it will assume the AWS role you created in the previous section. It will then be able to present credentials as that AWS role that will allow it to assume the Chainguard identity you created in the previous section, to view and manage Chainguard resources.
Next, you can create the chainguard.tf
file.
chainguard.tf
chainguard.tf
will create a few resources that will help us test out the identity.
resource "chainguard_group" "example-group" {
name = "example-group"
description = <<EOF
This organization simulates an end-user organization, which the AWS role identity
can interact with via the identity in aws.tf.
EOF
}
This section creates a Chainguard IAM organization named example-group
, as well as a description of the organization. This will serve as some data for the identity to access when we test it out later on.
Then we’ll define a Chainguard identity that can be assumed by the AWS role created in lambda.tf
above:
resource "chainguard_identity" "aws" {
parent_id = data.chainguard_group.example-group.id
name = "aws-auth-identity"
description = "Identity for AWS Lambda"
aws_identity {
aws_account = data.aws_caller_identity.current.account_id
aws_user_id_pattern = "^AROA(.*):${local.lambda_name}$"
// NB: This role will be assumed so can't use the role ARN directly. We must use the ARN of the assumed role
aws_arn = "arn:aws:sts::${data.aws_caller_identity.current.account_id}:assumed-role/${aws_iam_role.lambda.name}/${local.lambda_name}"
}
}
The most important part of this section is the aws_identity
block. When the AWS role tries to assume this identity later on, it must present a token matching the aws_account
, aws_user_id_pattern
, and aws_arn
specified here in order to do so.
The aws_user_id_pattern
field configures the identity to be assumable only by the AWS role with the specified name, which is itself assumed by another execution role, which we’ll configure below. This role will be assumed so can’t use the role ARN directly in aws_arn
; We must used the ARN of the assumed role.
The section after that looks up the viewer
role.
data "chainguard_role" "viewer" {
name = "viewer"
}
The final section grants this role to the identity on the example-group
.
resource "chainguard_rolebinding" "view-stuff" {
identity = chainguard_identity.aws.id
group = data.chainguard_group.example-group.id
role = data.chainguard_role.viewer.items[0].id
}
After defining these resources, there are some other resources in the example directory that build and deploy a Lambda function that assumes the identity. We’ll describe that code in the next section.
After defining these resources, your Terraform configuration will be ready. Now you can run a few terraform
commands to create the resources defined in your .tf
files.
Creating Your Resources
First, run terraform init
to initialize Terraform’s working directory.
terraform init
Then run terraform plan
. This will produce a speculative execution plan that outlines what steps Terraform will take to create the resources defined in the files you set up in the last section.
terraform plan
Then apply the configuration.
terraform apply
Before going through with applying the Terraform configuration, this command will prompt you to confirm that you want it to do so. Enter yes
to apply the configuration.
...
Plan: 8 to add, 0 to change, 0 to destroy.
Changes to Outputs:
+ aws-identity = (known after apply)
Do you want to perform these actions?
Terraform will perform the actions described above.
Only 'yes' will be accepted to approve.
Enter a value:
After typing yes
and pressing ENTER
, the command will complete and will output an aws-identity
value.
...
Apply complete! Resources: 8 added, 0 changed, 0 destroyed.
Outputs:
aws-identity = "<your identity>"
This is the identity’s UIDP (unique identity path), which you configured the chainguard.tf
file to emit in the previous section. Note this value down, as you’ll need it to set up the AWS role you’ll use to test the identity. If you need to retrieve this UIDP later on, though, you can always run the following chainctl
command to obtain a list of the UIDPs of all your existing identities.
chainctl iam identities ls
Testing the Identity
When the AWS Lambda function is invoked, first it needs to get its credentials, which assert that it is the AWS IAM role defined earlier.
The example code in Go does this with aws-sdk-go-v2
:
// Get AWS credentials.
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return "", fmt.Errorf("failed to load configuration, %w", err)
}
creds, err := cfg.Credentials.Retrieve(ctx)
if err != nil {
return "", fmt.Errorf("failed to retrieve credentials, %w", err)
}
These credentials represent the AWS role assumed by the Lambda function ("aws-auth"
defined above in lambda.tf
).
You can use the Chainguard SDK for Go to generate a token that Chainguard understands, to authenticate the Lambda function as the Chainguard identity you created earlier, by its UIDP:
// Generate a token and exchange it for a Chainguard token.
awsTok, err := aws.GenerateToken(ctx, creds, env.Issuer, env.Identity)
if err != nil {
return "", fmt.Errorf("generating AWS token: %w", err)
}
exch := sts.New(env.Issuer, env.APIEndpoint, sts.WithIdentity(env.Identity))
cgtok, err := exch.Exchange(ctx, awsTok)
if err != nil {
return "", fmt.Errorf("exchanging token: %w", err)
}
The resulting token, cgtok
, can be used to authenticate requests to Chainguard API calls:
// Use the token to list repos in the organization.
clients, err := registry.NewClients(ctx, env.APIEndpoint, cgtok)
if err != nil {
return "", fmt.Errorf("creating clients: %w", err)
}
ls, err := clients.Registry().ListRepos(ctx, ®istry.RepoFilter{
Uidp: &common.UIDPFilter{
ChildrenOf: env.Group,
},
})
Removing Sample Resources
To remove the resources Terraform created, you can run the terraform destroy
command.
terraform destroy
This will destroy the role-binding, and the identity created in this guide. However, you’ll need to destroy the example-group
organization yourself with chainctl
. It will also delete all the AWS resources defined earlier in chainguard.tf
and lambda.tf
.
chainctl iam organizations rm example-group
You can then remove the working directory to clean up your system.
rm -r ~/aws-id/
Following that, all of the example resources created in this guide will be removed from your system.
Learn more
For more information about how assumable identities work in Chainguard, check out our conceptual overview of assumable identities. Additionally, the Terraform documentation includes a section on recommended best practices which you can refer to if you’d like to build on this Terraform configuration for a production environment.
Last updated: 2024-05-09 08:48