HashiCorp Vault Integration
HashiCorp Vault is a dedicated secrets engine that issues, rotates, and revokes credentials on demand. Pairing it with Terraform lets your configuration read secrets — or mint brand-new, short-lived ones — at the moment of plan/apply, instead of stashing long-lived passwords in variables or version control. The big win is dynamic credentials: Vault can generate a unique database user or cloud key that automatically expires minutes later. The big caveat, which this page hammers, is that anything Vault hands Terraform still ends up in state, so a short lease is your main defence.
The Vault provider
The vault provider authenticates to a Vault cluster and exposes its secret engines through data sources and resources. Configure it with the cluster address and a token (typically sourced from the environment, never hardcoded).
terraform {
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 4.4"
}
}
}
provider "vault" {
address = "https://vault.internal.example.com:8200"
# token is read from VAULT_TOKEN; prefer auth methods in CI (see below)
}
The provider reads VAULT_ADDR and VAULT_TOKEN from the environment automatically, so in CI you usually omit both arguments and export them from the pipeline’s secret store. This provider works unchanged under OpenTofu.
Tip: Avoid pasting a root token into config or env. In CI, authenticate with a short-lived method — AppRole, JWT/OIDC, or AWS IAM — so the token Terraform holds is itself ephemeral and scoped to one job.
Reading static secrets with a data source
For secrets already stored in Vault’s key/value engine, the vault_kv_secret_v2 data source fetches them at runtime. The value never touches your .tf files.
data "vault_kv_secret_v2" "db" {
mount = "secret"
name = "prod/app/db"
}
resource "aws_db_instance" "main" {
identifier = "app-prod"
engine = "postgres"
username = "appuser"
password = data.vault_kv_secret_v2.db.data["password"]
instance_class = "db.t3.medium"
allocated_storage = 20
}
The data attribute is a map of the secret’s fields, so data["password"] pulls one key out of the stored JSON.
Dynamic short-lived credentials
This is where Vault outshines a plain secrets store. A secret backend such as the AWS or database engine generates brand-new credentials each run, bound to a lease that expires automatically. Use a resource (not a data source) so Terraform requests fresh credentials on apply.
# Vault's AWS secrets engine vends temporary IAM credentials
resource "vault_aws_access_credentials" "deploy" {
backend = "aws"
role = "terraform-deployer" # maps to an IAM policy in Vault
type = "creds"
}
provider "aws" {
region = "us-east-1"
access_key = vault_aws_access_credentials.deploy.access_key
secret_key = vault_aws_access_credentials.deploy.secret_key
token = vault_aws_access_credentials.deploy.security_token
}
Here Vault issues a temporary IAM key pair scoped to the terraform-deployer role, and the AWS provider uses it for that run only. After the lease TTL elapses, Vault revokes the IAM user — there is no standing credential to leak. The same pattern with vault_database_secret_backend_static_role / dynamic database roles produces ephemeral Postgres or MySQL logins.
Output:
$ terraform apply
vault_aws_access_credentials.deploy: Creating...
vault_aws_access_credentials.deploy: Creation complete after 2s [id=aws/creds/terraform-deployer/HmAc...]
aws_db_instance.main: Creating...
aws_db_instance.main: Still creating... [4m0s elapsed]
aws_db_instance.main: Creation complete after 4m21s [id=app-prod]
Apply complete! Resources: 2 added, 0 changed, 0 destroyed.
The state caveat
Every value Vault returns — static KV secrets, dynamic access keys, lease IDs — is written to Terraform state in plaintext. A short lease shrinks the window during which a leaked dynamic credential is usable, but the state file is still sensitive while the lease is live (and the static secrets never expire).
$ terraform show -json | jq '.values.root_module.resources[]
| select(.type=="vault_aws_access_credentials") | .values.secret_key'
"wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
Warning: Do not rely on Vault to keep secrets out of state — it cannot. Dynamic short leases plus an encrypted, access-controlled backend (S3 + SSE-KMS, locking) are what actually contain the exposure.
Patterns to minimize exposure
| Pattern | What it does | Trade-off |
|---|---|---|
| Dynamic secrets engine | New credential per run, auto-revoked | Requires Vault backend config |
Short lease TTL (default_lease_ttl) | Credential dies minutes after apply | Long applies may outlive the lease |
| Auth via AppRole/OIDC in CI | No standing Vault token | Slightly more setup |
ephemeral resources (TF 1.10+) | Values never persisted to state | Newer feature, narrower support |
Terraform 1.10 introduced ephemeral resources and values specifically for this problem — an ephemeral block is available during the run but is never written to state or plan files, which is the cleanest fix for the state-persistence caveat where the provider supports it.
Best Practices
- Authenticate to Vault with a short-lived method (AppRole, JWT/OIDC, AWS IAM) rather than a long-lived root token.
- Prefer dynamic secrets engines over static KV so each apply gets a fresh, auto-revoked credential.
- Keep lease TTLs as short as the apply duration allows to minimize the credential’s useful lifetime.
- Treat state as a secret regardless of Vault: encrypt the backend at rest and restrict read access.
- Use ephemeral resources/values (Terraform 1.10+) where available to keep secrets out of state entirely.
- Source
VAULT_ADDRandVAULT_TOKENfrom the environment or CI secret store, never from committed files. - Audit Vault access centrally — every secret read and lease is logged, giving you a single rotation and revocation point.