DEV Community

Cover image for Enriching Vault OIDC Tokens with SPIFFE Identity Metadata using Terraform
DarkEdges
DarkEdges

Posted on

Enriching Vault OIDC Tokens with SPIFFE Identity Metadata using Terraform

In modern microservices architectures, machine identity is just as critical as human identity. When services communicate, they often need to prove not just who they are, but what they are—their environment, business unit, or SPIFFE ID.

HashiCorp Vault is excellent for this. Its Identity Secrets Engine can issue OIDC tokens that serve as verifiable credentials for your workloads. However, getting those tokens to contain rich, custom metadata requires connecting a few specific dots: AppRole, Identity Entities, and OIDC Templates.

In this article, I'll walk through how to implement a robust machine identity pipeline using Terraform, where an application authenticates via AppRole and receives an OIDC token enriched with custom metadata.

The Goal

We want an application (e.g., a "ChatBot") to:

  1. Authenticate to Vault using the AppRole method.
  2. Request an OIDC token.
  3. Receive a token containing custom claims like spiffe_id, business_unit, and environment.

Step 1: Define the Identity Entity

First, we define the "who". In Vault, an Entity represents a unique identity. This is where we store the metadata we want to eventually see in our token.

# identities.tf

resource "vault_identity_entity" "application" {
  for_each = local.application_identities_map
  name     = each.key

  # This metadata is what we'll inject into the token later
  metadata = {
    environment   = each.value.identity.environment
    business_unit = each.value.identity.business_unit
    spiffe_id     = "spiffe://vault/application/${each.value.identity.environment}/${each.value.identity.business_unit}/${each.value.identity.name}"
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Configure AppRole Authentication

Next, we configure the AppRole auth backend. This is the standard way for machines to authenticate.

# approle.tf

resource "vault_approle_auth_backend_role" "applications" {
  for_each       = local.application_identities_map
  backend        = vault_auth_backend.approle.path
  role_name      = each.key
  token_ttl      = 3600
  bind_secret_id = true
}
Enter fullscreen mode Exit fullscreen mode

The "Secret Sauce": Binding AppRole to the Entity

This is the most critical step. By default, when you log in with AppRole, Vault creates a generic entity for that role. To use our custom metadata defined in Step 1, we must explicitly bind the AppRole to our specific Entity using an Entity Alias.

Important Gotcha: When creating an alias for AppRole, the name of the alias must be the Role ID, not the Role Name.

# approle.tf

resource "vault_identity_entity_alias" "approle_applications" {
  for_each       = local.application_identities_map

  # CRITICAL: Use role_id, not role_name
  name           = vault_approle_auth_backend_role.applications[each.key].role_id

  mount_accessor = vault_auth_backend.approle.accessor
  canonical_id   = vault_identity_entity.application[each.key].id
}
Enter fullscreen mode Exit fullscreen mode

We also need to ensure the AppRole inherits the entity's properties. We can enforce this via a generic endpoint configuration:

resource "vault_generic_endpoint" "approle_entity_inherit" {
  for_each   = local.application_identities_map
  depends_on = [vault_approle_auth_backend_role.applications]
  path       = "auth/approle/role/${each.key}"

  data_json = jsonencode({
    entity_alias_sole_inherit = true
  })
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Configure the OIDC Template

Finally, we configure the OIDC provider and role. The template field is where the magic happens. We use Vault's template syntax to pull values dynamically from the authenticated entity's metadata.

# identity_tokens.tf

resource "vault_identity_oidc_role" "application_identity" {
  name      = "application_identity"
  key       = vault_identity_oidc_key.application_identity.name
  client_id = "spiffe://vault.darkedges.au/gateway"
  ttl       = 86400

  # The Template: Injecting metadata into the JSON payload
  template = <<EOT
{
  "azp": {{identity.entity.metadata.spiffe_id}},
  "nbf": {{time.now}},
  "groups": {{identity.entity.groups.names}},
  "appinfo": {
    "business_unit": {{identity.entity.metadata.business_unit}},
    "environment": {{identity.entity.metadata.environment}}
  }
}
EOT
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Testing the Flow

Now, let's see it in action. We'll use PowerShell to simulate the application workflow.

1. Get Credentials & Login

First, we retrieve the Role ID and Secret ID, then authenticate to get a Vault token.

# Get Role ID and Secret ID
$ROLE_ID = docker-compose exec vault vault read -field=role_id auth/approle/role/ChatBot/role-id
$SECRET_ID = docker-compose exec vault vault write -f -field=secret_id auth/approle/role/ChatBot/secret-id

# Login
$APPTOKEN = docker-compose exec vault vault write -field=token auth/approle/login role_id="$ROLE_ID" secret_id="$SECRET_ID"
Enter fullscreen mode Exit fullscreen mode

2. Request the OIDC Token

Using the Vault token we just got, we request the OIDC token.

$OIDC_TOKEN = docker-compose exec -e VAULT_TOKEN="$APPTOKEN" vault vault read -field=token identity/oidc/token/application_identity
Enter fullscreen mode Exit fullscreen mode

3. The Result

When we decode the JWT, we see our rich metadata is successfully populated!

{
  "appinfo": {
    "business_unit": "engineering",
    "environment": "production"
  },
  "aud": "spiffe://vault.darkedges.au/gateway",
  "azp": "spiffe://vault/application/production/engineering/ChatBot",
  "exp": 1764848993,
  "groups": [
    "ChatBot group"
  ],
  "iat": 1764762593,
  "iss": "https://vault.darkedges.au/v1/identity/oidc",
  "sub": "ea0e0006-d4f5-cbde-3cb6-c013d4dba5f2"
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

By combining Identity Entities, AppRole Aliases, and OIDC Templates, we've turned Vault into a powerful identity provider for our internal services. This setup allows downstream systems (like API gateways or service meshes) to make authorization decisions based on trusted, verifiable metadata like business_unit or environment, rather than just a raw IP address or generic token.

a complete example is available at https://github.com/darkedges/spiffe-vault-terraform

Inspiration

Top comments (0)