Skip to main content

Access Vault Secrets From AWS

·1242 words·6 mins
Hcp Vault Aws

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.

Mattias Fjellström
Author
Mattias Fjellström
Cloud architect · Author · HashiCorp Ambassador