Part 2: Initializing Vault, Enabling IAM Authentication, and Securing Access This is part 2 of the Vault configuration where I will cover how we can enable IAM authentication for simplicity and because for example if we heavily using AWS services. I will cover: - How to create & configure AWS roles across organization for Vault - Create example role and provide test policy for it - Configure external-secrets to fetch the secret from the Vault As in the part 1 we succesfully deployed our EKS cluster and initialized our Vault cluster, not it's time to confiugre access to the Vault server. Let's imagine that you're using AWS provider as a main cloud provider and do not reinvent the wheel we need to stick with IAM roles we don't want to manage Vault tokens and we will use AWS roles instead for authentication and autherization it's 100% achiveable but before starting we need to create AWS roles across AWS acounts where the Vault will be used. In the part one we deployed our Vault server as a helm chart and we provided to the vault base role using IRSA approach, for example there was vault AWS acount, now we want access vault from the Dev account Firstly we need to create this role in Dev account. Again as it was previous I will use terragrunt / tofu for this purposes. create the policy first
include "root" {
path = find_in_parent_folders()
}
include "aws" {
path = find_in_parent_folders("aws.hcl")
}
terraform {
source = "tfr:///terraform-aws-modules/iam/aws//modules/iam-policy?version=${local.version}"
}
locals {
config = jsondecode(read_tfvars_file(find_in_parent_folders("config.tfvars")))
version = read_terragrunt_config(find_in_parent_folders("versions.hcl")).locals.terraform.iam_policy
}
inputs = {
name = "vault-policy"
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow",
Action = [
"ec2:DescribeInstances",
"iam:GetInstanceProfile",
"iam:GetUser",
"iam:GetRole"
],
Resource = "*"
}
]
})
}
An the role itself which allows to assume from IAM role from account 11111111111
include "root" {
path = find_in_parent_folders()
}
include "aws" {
path = find_in_parent_folders("aws.hcl")
}
dependency "management_vault_role" {
config_path = "${get_path_to_repo_root()}/aws/infrastructure/services/global/iam/role/vault/role"
}
dependency "policy" {
config_path = "${get_original_terragrunt_dir()}/../policy"
}
terraform {
source = "tfr:///terraform-aws-modules/iam/aws//modules/iam-assumable-role?version=${local.version}"
}
locals {
version = read_terragrunt_config(find_in_parent_folders("versions.hcl")).locals.terraform.iam_oidc
config = jsondecode(read_tfvars_file(find_in_parent_folders("config.tfvars")))
}
inputs = {
create_role = true
role_name = "vault"
role_requires_mfa = false
trusted_role_arns = [
#Dev account Vault role
dependency.management_vault_role.outputs.iam_role_arn
]
custom_role_policy_arns = [
dependency.policy.outputs.arn
]
}
After we have created `Vault` role from the Dev account, we need somehow tell the Vault that it can use this role to check all the roles from the account 22222222222 and for this purpose again I will create custom terraform module.
It's pretty straight forward but I will put the code snippet from this module.
#main.tf
resource "vault_auth_backend" "aws" {
type = "aws"
}
resource "vault_aws_auth_backend_sts_role" "accounts" {
for_each = var.aws_accounts
backend = vault_auth_backend.aws.path
account_id = each.value.account_id
sts_role = each.value.sts_role
}
resource "vault_policy" "example" {
for_each = var.policies
name = each.key
policy = each.value.policy
}
resource "vault_aws_auth_backend_role" "aws" {
for_each = var.roles
role = each.key
auth_type = "iam"
backend = vault_auth_backend.aws.path
bound_iam_principal_arns = each.value.role_arn
token_policies = each.value.policies
token_max_ttl = each.value.max_ttl
}
resource "vault_mount" "kvv2" {
path = "secret"
type = "kv"
description = "Key-Value secret engine v2 at secret/"
options = {
version = "2"
}
}
#variables.tf
variable "policies" {
description = "List of policies to attach to the Vault role"
type = map(object({
policy = string
}))
default = {}
}
variable "roles" {
description = "List of roles to attach which allows access Vault"
type = map(object({
role_arn = list(string)
policies = list(string)
max_ttl = number
}))
default = {}
}
variable "aws_accounts" {
description = "List of AWS accounts to serve as a trust relationship for the Vault server"
type = map(object({
account_id = string
sts_role = string
}))
default = {}
}
#versions.tf
terraform {
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 4.8.0"
}
}
required_version = ">= 1.3.0"
}
After we added this module we can use it
include "root" {
path = find_in_parent_folders()
}
include "vault" {
path = find_in_parent_folders("vault.hcl")
}
include "aws" {
path = find_in_parent_folders("aws.hcl")
}
dependency "dev-external-secret-role" {
config_path = "${get_path_to_repo_root()}/aws/workloads/dev/global/iam/role/external-secrets/role"
}
dependency "dev-vault-role" {
config_path = "${get_path_to_repo_root()}/aws/workloads/dev/global/iam/role/vault/role"
}
terraform {
source = "${get_path_to_repo_root()}//modules/vault"
}
locals {
config = jsondecode(read_tfvars_file(find_in_parent_folders("config.tfvars")))
}
inputs = {
aws_accounts = {
dev = {
account_id = local.config.accounts[index(local.config.accounts.*.name, "dev")].id
sts_role = dependency.dev-vault-role.outputs.iam_role_arn
}
stage = {
account_id = local.config.accounts[index(local.config.accounts.*.name, "stage")].id
sts_role = dependency.stage-vault-role.outputs.iam_role_arn
}
prod = {
account_id = local.config.accounts[index(local.config.accounts.*.name, "prod")].id
sts_role = dependency.prod-vault-role.outputs.iam_role_arn
}
}
policies = {
dev-external-secrets = {
policy = <<-EOF
path "/secret/data/dev/*" {
capabilities = ["read", "list"]
}
EOF
}
}
roles = {
dev-external-secrets = {
role_arn = [dependency.dev-external-secret-role.outputs.iam_role_arn]
policies = ["dev-external-secrets"]
max_ttl = 120
}
}
}
After we succesfully configured our vault we can test it, code snippet below contains yaml manifest where we can test that external-secret can communicate with Vault and
fetch the secrets, I'm assuming that you already deployed external-secrets operator with CRD's.
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
name: vault-store
spec:
provider:
vault:
server: "https://vault.services.internal.com"
path: "secret"
version: "v2"
auth:
iam:
path: aws
region: us-east-1
vaultRole: dev-external-secrets
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: vault-example
spec:
refreshInterval: "15s"
secretStoreRef:
name: vault-store
kind: SecretStore
target:
name: example-sync
data:
- secretKey: example-key
remoteRef:
key: dev/k8s/test/username
After we applied our manifest we can check it that everything is wokring fine.
kubectl describe secretstore,externalsecret
Name: vault-store
Namespace: default
Labels:
Annotations:
API Version: external-secrets.io/v1beta1
Kind: SecretStore
Metadata:
Creation Timestamp: 2025-05-23T10:53:46Z
Generation: 1
Resource Version: 87589679
UID: ab6c4fce-3c08-46e9-aa90-1dff1583d2b2
Spec:
Provider:
Vault:
Auth:
Iam:
Path: aws
Region: us-east-1
Vault Role: dev-external-secrets
Path: secret
Server: https://vault.services.internal.com
Version: v2
Status:
Capabilities: ReadWrite
Conditions:
Last Transition Time: 2025-05-23T10:53:47Z
Message: store validated
Reason: Valid
Status: True
Type: Ready
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Normal Valid 2s (x2 over 2s) secret-store store validated
Normal Valid 2s (x2 over 2s) secret-store store validated
Name: vault-example
Namespace: default
Labels:
Annotations:
API Version: external-secrets.io/v1beta1
Kind: ExternalSecret
Metadata:
Creation Timestamp: 2025-05-23T10:53:47Z
Generation: 1
Resource Version: 87589687
UID: c8e7ff26-ca33-4b0d-a00a-f5600eeafab7
Spec:
Data:
Remote Ref:
Conversion Strategy: Default
Decoding Strategy: None
Key: dev/k8s/test/username
Metadata Policy: None
Secret Key: example-key
Refresh Interval: 15s
Secret Store Ref:
Kind: SecretStore
Name: vault-store
Target:
Creation Policy: Owner
Deletion Policy: Retain
Name: example-sync
Status:
Binding:
Name: example-sync
Conditions:
Last Transition Time: 2025-05-23T10:53:47Z
Message: secret synced
Reason: SecretSynced
Status: True
Type: Ready
Refresh Time: 2025-05-23T10:53:47Z
Synced Resource Version: 1-af61a938cce70a6fbb5c7dae966c7503
Events:
Type Reason Age From Message
---- ------ ---- ---- -------
Warning UpdateFailed 2s (x5 over 2s) external-secrets error retrieving secret at .data[0], key: dev/k8s/test/username, err: SecretStore "vault-store" is not ready
Warning UpdateFailed 2s (x5 over 2s) external-secrets error retrieving secret at .data[0], key: dev/k8s/test/username, err: SecretStore "vault-store" is not ready
Normal Created 2s external-secrets Created Secret
Warning UpdateFailed 2s external-secrets secrets "example-sync" already exists
As you can see secret was created and your application can consumed it, I undersand security concern here, better to directly support into your application with Vault communication but as a starter point it's ready to go.
Powered by Golang net/http package