AWS PowerShell Terraform Vault

Hashicorp Vault AWS auth backend role Terraform example, then access secret from the userdata instance.

In Hasicorp Vault (the open-source tool for securely storing and accessing sensitive information, such as API keys, passwords, and encryption keys) one of the authentication methods is AWS authentication. This is very useful when you need one/all of your instances to access a secret or certificate etc.. from Vault.

The process

  1. AWS auth role in Vault is specifically designed for applications running on Amazon Web Services (AWS) infrastructure. It leverages AWS IAM roles to authenticate and authorize applications to access secrets in Vault. The key components of the AWS auth role are:
    • IAM Role: An IAM role is created in AWS IAM and assigned to the application or EC2 instance. It defines the set of permissions that the application or instance has in AWS.
    • Vault Role: A Vault role is created and associated with the IAM role. It maps the IAM role to a set of Vault policies, which determine the access and permissions within Vault.
  2. When an application running in AWS wants to authenticate with Vault using the AWS auth role, it assumes its assigned IAM role and retrieves temporary AWS credentials. These temporary credentials are then used to authenticate with Vault, presenting them to Vault’s AWS authentication endpoint. If the provided credentials are valid and the association between IAM and Vault roles is correct, Vault grants an access token to the application with the associated policies and permissions.

Setting up AWS Authentication Role in Terraform

If you want to set this up in Terraform. In your Terraform code add the following. You may need to refactor to match your setup.

#VAULT REPO - This would be part of your Vault code, usually managed in a separate code repository to your projects repo. I would split this code up as well, but for this example I've put it in one file.

# Variable for your vault token, I would put this as a environment variable.
variable "vault_token" {}

# Register your Vault as a provider so you can acccess it.
provider "vault" {
  alias = "projectname"
  namespace = "root/projectname"
  address = "https://vault.yourcompanydomain.com"
  token = var.vault_token
}

#This is another provider for vault, it uses the namespace under the projectname/development. There could also be /preproduction /production etc.. but  these are normally kept on a separate vault.
provider "vault" {
  alias = "development"
  namespace = "root/projectname/development"
  address = "https://vault.yourcompanydomain.com"
  token = var.vault_token
}

#This is the policy document, it defines what can be accessed ifthe policy is used.
data "vault_policy_document" "projectname_instances" {
  rule {
    path = "kv/data/projectname/*"
    capabilities = ["read"]
    description = "to read anything under projectname i.e. passwords/secrets"
  }
  rule {
    path = "kv/data/cert"
    capabilities = ["read"]
    description = "to read pfx password"
  }
  provider =vault.development # note this is using the development - so the secrets are within this namespace.
}

#This the aws is already created so using a data provider to access it.
data "vault_auth_backend" "development_aws" {
  path = "aws"
  provider = vault.development
}

#This creates a policy in vault and links the policy document to it.
resource "vault_policy" "development_aws_instances" {
  name = "projectname_instances"
  policy = data.vault_policy_document.projectname_instances.hcl
  provider = vault.development
}

#This is the aws auth backend role
resource "vault_aws_auth_backend_role" "development_aws_instances" {
  role = "projectname_ec2servicerole_development"
  backend = data.vault_auth_backend.development_aws.path
  token_polices = ["default", vault_policy.development_aws_instances.name]
  token_period = 30 * 24 * 60 * 60
  provider = vault.development
# THe XXXXXXXXXXXX is to be replaced with your AWS account number
  bound_iam_instance_profile_arns = ["arn:aws:iam::XXXXXXXXXXXX:instance-profile/projectname_EC2ServiceRole_Development"]
  auth_type = "iam"
  inferred_entity_type = "ec2_instance"
  inferred_aws_region  = "eu-west-2"
}

Now, in your instance code, Instance code is managed in a separate repo. Add the following code so the instances can use the auth backend role.

#PROJECT REPO - This is separate from the Vault code.

# This is the referring to the policy built in to AWS
data "aws_iam_policy_document" "assume_role" {
  version = "2012-10-17"
  statement {
    effect = "Allow"
    actions = ["sts:AssumeRole"]
    principals {
      identifers = ["ec2.amazonaws.com"]
      type = "Service"
    }
  }
}

# This is the Role we will create to use for the instances
resource "aws_iam_role" "ec2servicerole_projectname" {
  assume_role_policy = data.aws_iam_policy_document.assume_role.json
  name = "projectname_EC2ServiceRole_development"
}

# This is the instance profile
resource "aws_iam_instance_profile" "projectname" {
  name = aws_iam_role.ec2servicerole_projectname.name
  role = aws_iam_role.ec2servicerole_projectname.name
}

# This is the bit that attaches the policy/policies to the Role
resource "aws_iam_role_policy_attachement" "projectname" {
  for_each = toset([
    "EC2InstanceProfile_S3FullPolicy" # This will be different for your setup. Use a policy that has the rights to whatever you are trying to access. In this example, I'm suggesting that the instances are trying to access data on a bucket.
   ])
   role = aws_iam_role.ec2servicerole_projectname.name
   polciy_arn = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/${each.key}"
}

# This is getting the AWS account information
data "aws_caller_identity" "current" {}

# This is the snippet of the instance - tells the instance which instance profile to use.
resource "aws_instance" "projectname_instance" {
...
iam_instance_profile = local.projectname_instance_profile.name
...
}


# Locals
locals {
  projectname = {
    instance_profile = aws_iam_instance_profile.projectname
    role = aws_iam_role.ec2servicerole_projectname
  }
}

Then, the final piece is in the userdata.tpl or it could be in another PowerShell script that you are running inside the instance. The code below will use vault to login and access the password or certificate. Vault.exe will need to be installed on the instance. Ideally, you would install this as part of the build image. But in case you haven’t, I’ve included the code to download it and extract it from a binaries/artifact repo.

<powershell>
$vaultURL = "https://storeage.companyname.com/vault_1.10.1_windows_amd64.zip"
#Download the vault zip
Invoke-webrequest -uri $vaultURL -outfile "C:\download\vault_1.10.1_windows_amd64.zip"
#Unzip the zip
expand-archive "C:\download\vault_1.10.1_windows_amd64.zip" -destination "C:\download\vault_1.10.1_windows_amd64"

#Used a function to assign the environment variable at all scopes. This helps if you need to access the token in other PowerShell scripts.
function setenvvars {
  param (
    [Parameter(Mandatory=$true)][string]$varname,
    [Parameter(Mandatory=$true)][string]$varvalue
  )
  $scopes = @("User","Machine","Process")
  Foreach ($scope in $scopes) { [Environment]::SetEnvironmentVariable("$varName", "$varValue", "$scope")}
}
#Set the environment variable - this uses the function above to make it easier to add for multiple scopes. This is not requested, but if you run extra PowerShell scripts from the userdata, this makes the variable available when new terminals are ran. 
# What this does it log's in to vault using the aws auth method that you setup in Terraform and allows you to access what you defined in the policy. You can see we are telling vault the role we are using for it.
setenvvars -varName "VAULT_TOKEN" -varValue "$(Invoke-Expression C:\download\vault_1.10.1_windows_amd64\vault.exe login -method=aws -field=token role=ec2servicerole_projectname_development"

#Run vault.exe and specify the location of the secret/password you want to access.
$pfxpw = invoke-expression "c:\download\vault_1.10.1_windows_amd64\vault.exe kv get -field=vertificate kv/cert"


</powershell>