Problem Context
Your Terraform configuration deploys a database with a connection string. The password is in a terraform.tfvars file that got accidentally committed to Git six months ago. Your CI pipeline logs show the database URL in plaintext because someone used output "db_url" {...} without marking it sensitive. And your Bicep template has a parameters.json file with a storage account key sitting next to it.
Secrets management in IaC is about ensuring that credentials, API keys, connection strings, and certificates never appear in code, state files, logs, or version control โ and that your infrastructure can securely retrieve them at runtime.
- A secret was found in Git history โ you rotated it, but you're not sure where else it spread
- Your CI pipeline echoes secrets to build logs because of
set -xdebugging - Terraform state contains plaintext passwords because you used
sensitive = false - Your deployment requires someone to manually inject a secret before every run
Secrets belong in a vault, not in code โ and your IaC pipeline should prove it.
Azure Key Vault: The Recommended Pattern
Store secrets in Azure Key Vault. Reference them from IaC at deploy time. Applications retrieve them at runtime using managed identities โ no secrets in environment variables, no secrets in config files.
Create Key Vault with Bicep
param keyVaultName string
param location string = resourceGroup().location
param tenantId string = subscription().tenantId
resource kv 'Microsoft.KeyVault/vaults@2023-07-01' = {
name: keyVaultName
location: location
properties: {
sku: { family: 'A', name: 'standard' }
tenantId: tenantId
enableRbacAuthorization: true // Use RBAC, not access policies
enableSoftDelete: true
softDeleteRetentionInDays: 90
enablePurgeProtection: true // Production: prevent accidental purge
networkAcls: {
defaultAction: 'Deny' // Deny all by default
virtualNetworkRules: []
ipRules: []
}
}
}Reference Key Vault secrets in Bicep parameters
// main.prod.bicepparam
using './main.bicep'
param sqlAdminPassword = getSecret(
'subscription-id',
'rg-secrets',
'kv-prod-001',
'sql-admin-password'
)
// Azure fetches the secret at deploy time โ never stored in bicepparam filesTerraform: Sensitive Variables and Key Vault Integration
Marking variables and outputs as sensitive
# variables.tf
variable "db_password" {
description = "Database administrator password"
type = string
sensitive = true # Redacted from plan output and logs
}
# outputs.tf
output "connection_string" {
value = "Server=...;Password=${var.db_password}"
sensitive = true # Never outputs to console or state in plaintext
}Reading secrets from Key Vault in Terraform
data "azurerm_key_vault" "main" {
name = "kv-prod-001"
resource_group_name = "rg-secrets"
}
data "azurerm_key_vault_secret" "db_password" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.main.id
}
resource "azurerm_mssql_server" "main" {
name = "sql-prod-001"
resource_group_name = azurerm_resource_group.main.name
location = azurerm_resource_group.main.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = data.azurerm_key_vault_secret.db_password.value
}Injecting secrets in CI without storing them
# GitHub Actions โ inject from repository secrets
- name: Terraform Apply
env:
TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
ARM_CLIENT_SECRET: ${{ secrets.ARM_CLIENT_SECRET }}
run: terraform apply -auto-approve tfplanPulumi Secrets
Pulumi encrypts secrets in the state file using a per-stack encryption key (managed by Pulumi Cloud, or a KMS key for self-hosted state):
# Set a secret config value โ encrypted in Pulumi.prod.yaml
pulumi config set --secret dbPassword "super-secret-value"
# Retrieve in code (TypeScript)
const config = new pulumi.Config();
const dbPassword = config.requireSecret('dbPassword');
// Type: pulumi.Output<string> โ treated as secret throughout the graph
# Mark an output as secret
export const connectionString = pulumi.secret(
pulumi.interpolate`Server=...;Password=${dbPassword}`
);What NOT to Do
# โ Never store secrets in tfvars committed to Git
db_password = "MyPassword123" # terraform.tfvars
# โ Never use plaintext in parameter files
{
"parameters": {
"sqlAdminPassword": { "value": "MyPassword123" }
}
}
# โ Never output secrets without marking them sensitive (Terraform)
output "db_password" {
value = var.db_password
# Missing: sensitive = true
}
# โ Never hardcode in Pulumi config without --secret flag
pulumi config set dbPassword "MyPassword123" # Stored plaintext in Pulumi.yamlRuntime Secret Access: Managed Identity Pattern
Applications should never receive secrets as environment variables or config files. Instead, grant them managed identity access to Key Vault and let them retrieve secrets at runtime:
// Bicep: grant app service identity access to Key Vault
resource kvSecretReaderRole 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
name: guid(kv.id, appService.id, 'KeyVaultSecretsUser')
scope: kv
properties: {
roleDefinitionId: subscriptionResourceId(
'Microsoft.Authorization/roleDefinitions',
'4633458b-17de-408a-b874-0445c86b69e6' // Key Vault Secrets User
)
principalId: appService.identity.principalId
principalType: 'ServicePrincipal'
}
}Production Checklist
- Run
git secretsortrufflehogin CI to scan for accidental secret commits - Mark all sensitive Terraform variables and outputs with
sensitive = true - Never commit
.tfvarsfiles containing secrets โ use CI environment variables or Key Vault data sources - Enable Key Vault soft-delete and purge protection in all environments
- Use managed identity + RBAC for application secret access โ no stored credentials
- Rotate secrets on a schedule; use Key Vault versioning to support zero-downtime rotation
- Audit Key Vault access logs โ alert on unexpected reads

