Introduction
When provisioning cloud infrastructure using Terraform, it is common to require sensitive values such as:
- Administrative credentials (usernames, passwords)
- API keys and tokens
- SSH keys
- Database connection strings
- Domain join credentials
In most environments, these secrets are commonly:
- Stored inside Terraform variable files
- Passed via CI/CD pipeline variables
- Hardcoded within Terraform configuration
This introduces multiple security risks:
- Exposure of secrets in Terraform state files
- Secrets stored in source control repositories
- Unauthorized reuse across environments
- Compliance violations in enterprise deployments
To mitigate these risks, organizations can integrate HashiCorp Vault with Terraform to securely store and retrieve secrets dynamically during infrastructure provisioning.
This blog demonstrates the Vault-Terraform integration using a real-world example:
- Storing Azure VM admin credentials securely inside Vault
- Retrieving them dynamically by Terraform at runtime
- Provisioning an Azure Virtual Machine without exposing secrets in code
Problem Statement
Terraform configurations frequently require sensitive inputs. Consider provisioning an Azure VM:
Hardcoding secrets like above:
- Exposes them in Terraform state files
- Risks Git commit leaks
- Makes rotation difficult
- Breaks Zero Trust security principles
The same problem applies to any Terraform resource that needs secrets — database passwords, storage account keys, TLS certificates, and more.
To eliminate this risk, secrets should be:
- Stored in a centralized secret management system
- Retrieved only at runtime
- Restricted using RBAC access policies
HashiCorp Vault solves this by acting as the single source of truth for all secrets consumed by Terraform.
Solution Architecture
The Vault-Terraform integration workflow:
- Secrets are stored in HashiCorp Vault (KV v2 secrets engine)
- Terraform authenticates to Vault using a trusted identity (Azure AD, tokens, AppRole, etc.)
- Terraform retrieves secrets dynamically using the vault_kv_secret_v2 data source
- Infrastructure resources are provisioned using runtime-fetched secrets
- No secrets are stored in Terraform code, variable files, or pipelines
- Terraform state is protected using an encrypted remote backend
Step 1: Store Secrets in HashiCorp Vault
First, ensure the KV v2 secrets engine is enabled:
Store your secrets inside Vault. For this example, we store Azure VM admin credentials:
This stores the credentials at:
Note: The vault kv put command writes to KV v2. Under the hood, the API path becomes secret/data/azure/vm-admin. This distinction matters when writing Vault policies (Step 3) and choosing the correct Terraform data source (Step 5).
Vault encrypts all secrets at rest.
Step 2: Configure Authentication in Vault
Terraform needs to authenticate to Vault before it can read secrets. Vault supports multiple auth methods. For Azure-based workloads, the Azure auth method is recommended.
Enable Azure AD authentication method:
Register the Azure tenant and provide a Service Principal that Vault uses to validate Azure JWT tokens:
Why client_id and client_secret? Vault needs its own Service Principal to call the Azure AD metadata endpoint and verify JWT tokens presented by Terraform. Without these, Azure auth will fail unless Vault itself runs on an Azure VM with a Managed Identity.
Other supported auth methods include:
| Auth Method | Use Case |
|---|---|
| Token | Development / testing |
| Azure | Azure-hosted workloads |
| AppRole | CI/CD pipelines (Azure DevOps, GitHub Actions) |
| JWT/OIDC | Federated identity providers |
Step 3: Configure Vault Policy for Terraform
Create a policy that grants Terraform read-only access to the required secrets. Since this is KV v2, the policy path must include /data/ after the mount point:
Common Mistake: Using path "secret/azure/vm-admin" (without /data/) will silently deny access on KV v2. Always use secret/data/... in policies for KV v2 engines.
Apply the policy and attach it to the Terraform role:
Terraform now has restricted, read-only access to only the secrets it needs — following the principle of least privilege.
Step 4: Configure Vault Provider in Terraform
Add the Vault provider alongside your infrastructure provider:
Authenticate Terraform to Vault using environment variables:
Token auth (dev/testing):
Azure auth (production):
Step 5: Retrieve Secrets in Terraform
Use the vault_kv_secret_v2 data source to fetch secrets at runtime:
Why not vault_generic_secret? While vault_generic_secret can work with KV v2, it requires the raw API path (secret/data/azure/vm-admin) which is error-prone. The vault_kv_secret_v2 data source handles the /data/ prefix automatically and is the recommended approach for KV v2.
Individual values are accessed as:
Terraform retrieves these values dynamically during plan and apply — they never exist in .tf files.
Step 6: Use Vault Secrets in Resource Provisioning
Here is a practical example using the Vault-fetched secrets to provision an Azure Windows VM:
No credentials are stored inside Terraform configuration. Secrets are injected securely during execution.
Azure VM Note: Azure requires VM admin passwords to be 12–123 characters and satisfy 3 of 4 complexity rules (uppercase, lowercase, digit, special character). Ensure credentials stored in Vault meet these requirements.
The same pattern applies to any Terraform resource that requires secrets — database admin passwords, storage account access keys, application settings, and more.
Protecting Terraform State
Terraform state will contain secrets in plaintext after terraform apply. This is unavoidable — it is how Terraform tracks resource attributes.
To protect state:
| Control | How |
|---|---|
| Remote backend | Use Azure Storage with Server-Side Encryption |
| Access restriction | Lock the storage account with RBAC and network rules |
| State locking | Use blob lease locking (built into azurerm backend) |
| Audit | Enable Azure Storage diagnostic logging |
Never use a local terraform.tfstate file when Vault secrets are involved.
Recommended Vault Path Structure
Segregate secrets by environment to prevent cross-environment exposure:
Pass the appropriate secret name per Terraform workspace:
# Dev
terraform workspace select dev
terraform apply -var vault_secret_name="azure/dev/vm-admin"
# Prod
terraform workspace select prod
terraform apply -var vault_secret_name="azure/prod/vm-admin"
Benefits of Vault Integration with Terraform
| Capability | Benefit |
|---|---|
| Runtime secret retrieval | No hardcoded secrets in code or pipelines |
| RBAC controlled access | Only authorized workloads can read secrets |
| Secret encryption | Secure storage at rest and in transit |
| Environment isolation | Safer multi-stage deployments |
| Secret rotation | Update secrets in Vault without changing Terraform code |
| KV v2 versioning | Secret history, rollback, and soft-delete |
| Audit logging | Full trail of who accessed which secrets |
Conclusion
Terraform infrastructure provisioning should never require hardcoding sensitive values in configuration files, variable files, or CI/CD pipelines.
By integrating HashiCorp Vault with Terraform:
- Secrets are retrieved securely at runtime via vault_kv_secret_v2
- Authentication is handled through trusted identity methods (Azure AD, AppRole, tokens)
- Access is governed via Vault policies using the correct KV v2 /data/ paths
- Terraform state is encrypted using a remote backend
- Deployments remain compliant and auditable
This approach aligns with enterprise-grade Zero Trust practices and applies to any infrastructure provisioning scenario — whether deploying virtual machines, databases, Kubernetes clusters, or application services.
Terraform vault policy code :
# Vault policy: allows Terraform to READ VM admin credentials only.
# Apply with: vault policy write terraform-vm-policy vault/terraform-vm-policy.hcl
# Dev environment
path "secret/data/azure/dev/vm-admin" {
capabilities = ["read"]
}
# Test environment
path "secret/data/azure/test/vm-admin" {
capabilities = ["read"]
}
# Prod environment
path "secret/data/azure/prod/vm-admin" {
capabilities = ["read"]
}
# Generic path (if not using per-env segregation)
path "secret/data/azure/vm-admin" {
capabilities = ["read"]
}
VM Code :
# ---------------------------------------------------------------
# Windows Virtual Machine
# Admin credentials are fetched from Vault (KV v2) — never hardcoded.
# ---------------------------------------------------------------
resource "azurerm_windows_virtual_machine" "vm" {
name = var.vm_name
resource_group_name = azurerm_resource_group.rg.name
location = azurerm_resource_group.rg.location
size = var.vm_size
computer_name = var.vm_name
# Credentials injected from Vault at runtime
admin_username = data.vault_kv_secret_v2.vm_secret.data["username"]
admin_password = data.vault_kv_secret_v2.vm_secret.data["password"]
network_interface_ids = [
azurerm_network_interface.nic.id
]
os_disk {
caching = var.os_disk_caching
storage_account_type = var.os_disk_storage_type
}
source_image_reference {
publisher = var.image_publisher
offer = var.image_offer
sku = var.image_sku
version = var.image_version
}
tags = var.tags
}