Skip to main content

Configure Vault for Terraform Cloud Authentication

This guide shows you how to configure HashiCorp Vault to accept OIDC authentication from Terraform Cloud (HCP Terraform) workspaces, eliminating the need to store Vault tokens in your workspaces.

Prerequisites

  • HashiCorp Vault instance (v1.9.0 or later recommended)
  • Vault admin access to configure authentication backends
  • Terraform Cloud organization and workspace
  • Self-hosted agents require v1.8.0 or later (if applicable)

Architecture Overview

┌─────────────────────┐                    ┌──────────────┐
│ Terraform Cloud │ OIDC Token (JWT) │ Vault │
│ Workspace │───────────────────>│ JWT Auth │
│ │ │ Backend │
└─────────────────────┘ └──────────────┘
│ │
│ ▼
│ ┌──────────────┐
│ │ Policies │
│ │ & Secrets │
│◀─────────────────────────────────┤ │
│ Vault Token (short-lived) └──────────────┘

Access secrets during
Terraform run

Step 1: Enable JWT Authentication Backend

Enable the JWT authentication method in Vault:

vault auth enable jwt

If you need a custom mount path:

vault auth enable -path=tfc-jwt jwt

Step 2: Configure Trust with Terraform Cloud

Configure Vault to trust tokens from Terraform Cloud:

vault write auth/jwt/config \
oidc_discovery_url="https://app.terraform.io" \
bound_issuer="https://app.terraform.io"

For Terraform Enterprise (self-hosted), replace the URL with your instance:

vault write auth/jwt/config \
oidc_discovery_url="https://terraform.example.com" \
bound_issuer="https://terraform.example.com"
OIDC Discovery

The oidc_discovery_url allows Vault to automatically fetch the OIDC provider's public keys for token validation. Include the https:// scheme but no trailing slash.

Step 3: Create Vault Policy

Create a policy defining what secrets your Terraform workspace can access. Save this as tfc-policy.hcl:

# Allow tokens to query themselves
path "auth/token/lookup-self" {
capabilities = ["read"]
}

# Allow tokens to renew themselves
path "auth/token/renew-self" {
capabilities = ["update"]
}

# Allow tokens to revoke themselves
path "auth/token/revoke-self" {
capabilities = ["update"]
}

# Access to specific secrets
path "secret/data/terraform/*" {
capabilities = ["read"]
}

# Example: Access to database credentials
path "database/creds/terraform" {
capabilities = ["read"]
}

# Example: Access to AWS dynamic credentials
path "aws/creds/terraform-role" {
capabilities = ["read"]
}

Write the policy to Vault:

vault policy write tfc-policy tfc-policy.hcl
Least Privilege

Grant only the minimum permissions necessary for your Terraform workspace. Use separate policies for different workspaces or environments.

Step 4: Create JWT Authentication Role

Create a role that maps Terraform Cloud workspaces to the Vault policy.

Option A: Single Workspace Mapping

For a specific workspace:

vault write auth/jwt/role/tfc-role \
role_type="jwt" \
bound_audiences="vault.workload.identity" \
bound_claims="sub=organization:my-org-name:project:my-project-name:workspace:my-workspace-name:run_phase:*" \
user_claim="terraform_full_workspace" \
token_ttl=20m \
token_policies="tfc-policy"

Option B: All Workspaces in a Project

To allow all workspaces in a project:

vault write auth/jwt/role/tfc-project-role \
role_type="jwt" \
bound_audiences="vault.workload.identity" \
bound_claims="sub=organization:my-org-name:project:my-project-name:workspace:*:run_phase:*" \
user_claim="terraform_full_workspace" \
token_ttl=20m \
token_policies="tfc-policy"

Option C: All Workspaces in an Organization

To allow all workspaces in your organization:

vault write auth/jwt/role/tfc-org-role \
role_type="jwt" \
bound_audiences="vault.workload.identity" \
bound_claims="sub=organization:my-org-name:*" \
user_claim="terraform_full_workspace" \
token_ttl=20m \
token_policies="tfc-policy"
Security Critical

Always validate at minimum:

  • bound_audiences: Prevents tokens intended for other services from being used
  • Organization name in bound_claims: Prevents other Terraform Cloud organizations from accessing your Vault

Example of an insecure configuration:

# ❌ INSECURE - accepts tokens from any organization
bound_claims="sub=organization:*:*"

Role Configuration Parameters

ParameterDescriptionExample
role_typeMust be "jwt" for Terraform Cloud"jwt"
bound_audiencesToken audience claim (prevents reuse)["vault.workload.identity"]
bound_claimsRequired claims in token (security boundary)sub=organization:acme:*
user_claimClaim to use as Vault usernameterraform_full_workspace
token_ttlToken lifetime (recommend 20m for automation)20m
token_policiesVault policies to attachtfc-policy

Step 5: Configure Terraform Cloud Workspace

Add these environment variables to your Terraform Cloud workspace:

Required Variables

Variable NameValueDescription
TFC_VAULT_PROVIDER_AUTHtrueEnable dynamic credentials
TFC_VAULT_ADDRhttps://vault.example.com:8200Vault server address
TFC_VAULT_RUN_ROLEtfc-roleVault role name (from Step 4)

Optional Variables

Variable NameValueUse Case
TFC_VAULT_NAMESPACEadmin/terraformVault Enterprise namespaces
TFC_VAULT_AUTH_PATHtfc-jwtCustom JWT mount path
TFC_VAULT_WORKLOAD_IDENTITY_AUDIENCEvault.workload.identityCustom audience (must match role)
TFC_VAULT_PLAN_ROLEtfc-plan-roleRead-only role for plan phase
TFC_VAULT_APPLY_ROLEtfc-apply-roleWrite role for apply phase
TFC_VAULT_ENCODED_CACERTLS0tLS1C...Base64-encoded CA cert for custom CAs
Custom CA Certificates

If your Vault instance uses a custom or self-signed certificate, encode it:

cat vault-ca.crt | base64

Set the output as TFC_VAULT_ENCODED_CACERT.

Step 6: Configure Terraform Vault Provider

Update your Terraform configuration to use dynamic credentials. Do not hardcode address, token, or namespace:

terraform {
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 4.0"
}
}
}

provider "vault" {
# address, token, and namespace are set automatically
# by Terraform Cloud using environment variables
}

# Access secrets in your configuration
data "vault_generic_secret" "database" {
path = "secret/data/terraform/database"
}

resource "aws_db_instance" "example" {
# Use secrets from Vault
username = data.vault_generic_secret.database.data["username"]
password = data.vault_generic_secret.database.data["password"]
}

Terraform Cloud automatically sets these environment variables during runs:

  • VAULT_ADDR - Vault server address
  • VAULT_TOKEN - Temporary token obtained via OIDC
  • VAULT_NAMESPACE - Vault namespace (if configured)

Step 7: Verify Configuration

Test Authentication

From a machine with the Vault CLI and a valid Terraform Cloud token:

# Get a JWT from Terraform Cloud (example - actual token retrieval varies)
export TFC_WORKLOAD_IDENTITY_TOKEN="<your-token>"

# Authenticate to Vault
vault write auth/jwt/login role=tfc-role jwt=$TFC_WORKLOAD_IDENTITY_TOKEN

# Verify you can access secrets
vault kv get secret/terraform/database

Run a Terraform Plan

In your Terraform Cloud workspace:

  1. Queue a plan
  2. Check the run logs for Vault authentication
  3. Verify secrets are accessible during the run

Expected log output:

Initializing Vault provider...
Vault provider configured successfully

Phase-Specific Roles (Advanced)

Implement least-privilege access by using different roles for plan and apply:

Plan Role (Read-Only)

vault policy write tfc-plan-policy - <<EOF
path "secret/data/terraform/*" {
capabilities = ["read"]
}
EOF

vault write auth/jwt/role/tfc-plan-role \
role_type="jwt" \
bound_audiences="vault.workload.identity" \
bound_claims="sub=organization:my-org:*:run_phase:plan" \
user_claim="terraform_full_workspace" \
token_ttl=20m \
token_policies="tfc-plan-policy"

Apply Role (Read-Write)

vault policy write tfc-apply-policy - <<EOF
path "secret/data/terraform/*" {
capabilities = ["read"]
}
path "database/creds/*" {
capabilities = ["read"]
}
path "aws/creds/*" {
capabilities = ["read"]
}
EOF

vault write auth/jwt/role/tfc-apply-role \
role_type="jwt" \
bound_audiences="vault.workload.identity" \
bound_claims="sub=organization:my-org:*:run_phase:apply" \
user_claim="terraform_full_workspace" \
token_ttl=20m \
token_policies="tfc-apply-policy"

Workspace Configuration

TFC_VAULT_PROVIDER_AUTH = true
TFC_VAULT_ADDR = "https://vault.example.com:8200"
TFC_VAULT_PLAN_ROLE = "tfc-plan-role"
TFC_VAULT_APPLY_ROLE = "tfc-apply-role"

Troubleshooting

"Permission Denied" Errors

Cause: Role not found or bound claims don't match

Solution: Verify role configuration matches your Terraform Cloud organization/workspace:

vault read auth/jwt/role/tfc-role

"Invalid Audience" Errors

Cause: Audience mismatch between role and workspace configuration

Solution: Ensure bound_audiences matches TFC_VAULT_WORKLOAD_IDENTITY_AUDIENCE (default: vault.workload.identity)

Token Expiration During Long Runs

Cause: token_ttl too short for run duration

Solution: Increase TTL and ensure token renewal is enabled:

vault write auth/jwt/role/tfc-role \
token_ttl=60m \
token_max_ttl=120m

Self-Signed Certificate Errors

Cause: Vault uses custom CA not trusted by Terraform Cloud

Solution: Set TFC_VAULT_ENCODED_CACERT with your CA certificate (base64-encoded)

Security Considerations

  • Rotate Secrets: Regularly rotate secrets stored in Vault
  • Audit Logging: Enable Vault audit logging to track secret access
  • Namespace Isolation: Use Vault namespaces to isolate teams/environments (Enterprise)
  • Principle of Least Privilege: Grant minimum necessary permissions
  • Monitor Usage: Set up alerting for unusual authentication patterns

Additional Resources