Workload identity federation allows for credential-free authentication between platforms. The HashiCorp Cloud Platform (HCP) has support for workload identity with multiple platforms, one of them being Amazon Web Services (AWS).
Vault Secrets is a streamlined SaaS offering of Vault where you get some of the functionality of Vault, but with no need to manage a Vault cluster. In the following demo I will use key/value secrets, but Vault Secrets also has support for dynamic secrets along with a few other features.
In this blog post I will cover how to set up workload identity federation for workloads running on AWS. We will see how a workload on AWS (an EC2 instance) can access secrets from Vault Secrets without explicitly configuring any credentials for HCP.
Prerequisites#
To follow along you will need:
- Terraform installed locally (of course!).
- An AWS account. Also install the AWS CLI and authenticate it in your local terminal.
- An HCP organization and a project to use. Also install the HCP CLI and authenticate it in your local terminal.
Configure HCP Resources#
To avoid hardcoding organization names, projects and so on I like to use data sources. I use data sources for the HCP organization and project, and for the AWS caller identity:
data "hcp_organization" "current" {}
data "hcp_project" "current" {}
data "aws_caller_identity" "current" {}
Workload identity federation uses service principals on HCP. Create a new service principal and give it the Vault Secrets App Secrets Reader role on the HCP project:
resource "hcp_service_principal" "aws" {
name = "aws"
parent = data.hcp_project.current.resource_name
}
resource "hcp_project_iam_binding" "aws" {
principal_id = hcp_service_principal.aws.resource_id
role = "roles/secrets.app-secret-reader"
}
The workload identity federation is configured through a workload identity provider. For this provider we can configure that only our AWS account can use the identity provider and that the AWS ARN that is allowed to use it corresponds to the assumed IAM role (see the AWS section for how this role is configured):
locals {
arn_expression = join(":", [
"arn",
"aws",
"sts",
"",
data.aws_caller_identity.current.account_id,
"assumed-role/${aws_iam_role.hcp.name}"
])
}
resource "hcp_iam_workload_identity_provider" "aws" {
name = "aws"
service_principal = hcp_service_principal.aws.resource_name
aws = {
account_id = data.aws_caller_identity.current.account_id
}
conditional_access = "aws.arn matches `^${local.arn_expression}`"
}
Finally, we will need some secret data to work with on HCP so we create a new Vault Secrets app and add two secrets to it:
resource "hcp_vault_secrets_app" "aws" {
app_name = "aws-app"
}
resource "random_password" "aws" {
length = 20
lower = true
upper = true
numeric = true
special = false
}
resource "hcp_vault_secrets_secret" "username" {
app_name = hcp_vault_secrets_app.aws.app_name
secret_name = "username"
secret_value = "admin"
}
resource "hcp_vault_secrets_secret" "password" {
app_name = hcp_vault_secrets_app.aws.app_name
secret_name = "password"
secret_value = random_password.aws.result
}
That was all the configuration we needed on the HCP side.
Configure AWS Resources#
I will create an Ubuntu EC2 instance running in a public subnet in a new AWS VPC. The full details of the VPC and related resources are left out of this post. Here we will focus on how the IAM role and the EC2 instance are configured.
For the IAM role, configure an assume role policy allowing the EC2 service to use the role and set up an IAM instance profile for the role:
data "aws_iam_policy_document" "assume_role" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["ec2.amazonaws.com"]
}
}
}
resource "aws_iam_role" "hcp" {
name_prefix = "hcp"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
resource "aws_iam_instance_profile" "hcp" {
name_prefix = "hcp"
role = aws_iam_role.hcp.name
}
If you want this role to be able to do other actions in your AWS environment you should configure these policies and attach them to the role here.
As I mentioned, I will create an Ubuntu EC2 instance so I use the aws_ami
data source to get the correct AMI ID:
data "aws_ami" "ubuntu" {
filter {
name = "name"
values = ["ubuntu/images/*ubuntu-noble-24.04-amd64-server-*"]
}
filter {
name = "root-device-type"
values = ["ebs"]
}
filter {
name = "virtualization-type"
values = ["hvm"]
}
most_recent = true
owners = ["099720109477"]
}
I want to use cloud-init to configure my EC2 instance. This configuration involves installing the HCP CLI and creating a few configuration files for the HCP CLI. I configure the required files using local values:
locals {
hcp_credentials = jsonencode({
scheme = "workload"
workload = {
provider_resource_name = "${hcp_iam_workload_identity_provider.aws.resource_name}"
aws = {
imds_v2 = true
}
}
})
hcp_profile = <<-HCP_PROFILE
name = "default"
organization_id = "${data.hcp_organization.current.resource_id}"
project_id = "${data.hcp_project.current.resource_id}"
vault_secrets {
app = "${hcp_vault_secrets_app.aws.app_name}"
}
HCP_PROFILE
hcp_active_profile = <<-HCP_ACTIVE_PROFILE
name = "default"
HCP_ACTIVE_PROFILE
cloudinit_files = {
write_files = [
{
content = local.hcp_credentials
path = "/home/ubuntu/.config/hcp/credentials/cred_file.json"
},
{
content = local.hcp_profile
path = "/home/ubuntu/.config/hcp/profiles/default.hcl"
},
{
content = local.hcp_active_profile
path = "/home/ubuntu/.config/hcp/active_profile.hcl"
},
]
}
}
The three files cred_file.json
, default.hcl
and active_profile.hcl
are files that the HCP CLI would create by itself if we interactively used the CLI to authenticate and configure it.
I use the cloudinit_config
data source to create a cloud-init script that installs the HCP CLI, creates the configuration files and sets the ubuntu
user as owner for these files:
data "cloudinit_config" "server" {
gzip = false
base64_encode = true
part {
content_type = "text/x-shellscript"
content = <<-EOF
#!/bin/bash
apt-get update
apt-get -y install gpg coreutils
wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com $(lsb_release -cs) main" | tee /etc/apt/sources.list.d/hashicorp.list
apt-get update
apt-get -y install hcp
EOF
}
part {
content_type = "text/cloud-config"
content = yamlencode(local.cloudinit_files)
}
part {
content_type = "text/x-shellscript"
content = <<-EOF
#!/bin/bash
chown -R ubuntu: /home/ubuntu/
EOF
}
}
Finally, I create the EC2 instance where I reference the AMI data source, I attach the IAM instance profile and reference the cloud-init data source for the user-data script:
resource "aws_instance" "server" {
ami = data.aws_ami.ubuntu.id
instance_type = "t3.small"
subnet_id = aws_subnet.public.id
user_data_base64 = data.cloudinit_config.server.rendered
iam_instance_profile = aws_iam_instance_profile.hcp.name
# use an existing key or create a new
# key_name = "my-ec2-key"
vpc_security_group_ids = [
aws_security_group.server.id,
]
}
As you can see, there are a few references to resources I did not show in this blog post (e.g. aws_subnet.public
), these are standard public resources with no special configuration.
Using the Workload Identity Federation#
We must authenticate to HCP. In this step the configured trust relationship between HCP and AWS is verified and if everything is OK we obtain valid temporary credentials for HCP. We could do this using hcp auth login
or directly run the hcp profile init
command and have the auth login
command run implicitly:
$ hcp profile init --vault-secrets
✓ Project with name "mattias-fjellstrom-project" and ID "2d495eed-843e-4570-a6db-9e20d5f97f9a" selected
✓ App with name "aws-app" selected
To list all the secrets in the selected Vault Secrets app, run the following command:
$ hcp vault-secrets secrets list
Secret Name Created At Latest Version Type
password 2025-05-20T04:31:22.438Z 1 kv
username 2025-05-20T04:31:22.411Z 1 kv
We can open
a specific secret to read the secret value:
$ hcp vault-secrets secrets open password
Secret Name: password
Type: kv
Created At: 2025-05-20T04:31:22.438Z
Latest Version: 1
Value: L2fjAJgxwADNjw7CuMlt
Conclusions#
I have said this many times before: workload identity federation is great! There is no need to store long-lived credentials that you somehow have to pass to the workloads on AWS (or other platforms). Instead, you set up a trust relationship beforehand and allow workloads to utilize it for credential-less authentication.
In the demo I showed how to access Vault Secrets, but the same connection could have been used to do anything on HCP - assuming you provide the service principal with an appropriate role.