Managing sensitive information is one of the major challenges in adopting Infrastructure as Code (IaC) with Terraform. When we automate the deployment of Azure infrastructure, we inevitably handle critical data: passwords, API keys, certificates, connection strings, and other secrets that should never be exposed.
Terraform, as an Infrastructure as Code tool, offers a powerful declarative approach to managing Azure infrastructure. However, this approach introduces specific security risks related to the persistence and sharing of configurations. The Terraform state file (terraform.tfstate) contains the complete state of your infrastructure, including all sensitive values in plaintext. Similarly, storing Terraform code in version control systems like Git can expose secrets if best practices are not followed.
Main risks include:
- Exposure in the state file: terraform.tfstate stores all resource attribute values, including passwords and keys
- Leakage via version control: Hard-coded secrets in .tf files can be accidentally committed to Git
- Logs and outputs: Sensitive values may appear in Terraform logs or plan/apply outputs
- State sharing: Sharing the state file with the team exposes secrets to all members
- Compliance and audit: Difficulty tracking access and use of sensitive information
Objective of This Article:
This comprehensive guide is intended for DevOps engineers and cloud architects who need to implement secure secrets management practices with Terraform and Azure. We will explore:
- Existing security issues with the state file and Git
- Native Terraform features for sensitive values
- Last Terraform feature with ephemeral resources
- Integration with Azure Key Vault for secure secrets management
- Best practices and recommended patterns
Whether you're starting with Terraform or looking to improve your existing security practices, this article will provide you with the knowledge and practical examples needed to protect your sensitive information.
Existing Security Problems
The Terraform State File (terraform.tfstate)
The Terraform state file is at the heart of Terraform's operation, but it also represents the greatest security risk.
State File Structure and Content
The terraform.tfstate file is a JSON file that contains:
{
"version": 4,
"terraform_version": "1.9.0",
"serial": 42,
"lineage": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"outputs": {
"database_password": {
"value": "SuperSecretPassword123!",
"type": "string",
"sensitive": true
}
},
"resources": [
{
"mode": "managed",
"type": "azurerm_sql_server",
"name": "example",
"provider": "provider[\"registry.terraform.io/hashicorp/azurerm\"]",
"instances": [
{
"attributes": {
"id": "/subscriptions/.../servers/example-sql",
"name": "example-sql",
"administrator_login": "sqladmin",
"administrator_login_password": "P@ssw0rd123!ComplexPassword",
"version": "12.0"
}
}
]
}
]
}
Security Issues:
- Plaintext Storage: All resource attributes, including passwords, are stored in plain text in the state file
- No Native Encryption: By default, Terraform does not encrypt the state file locally
- Git History: If the state file is committed to Git, secrets remain in the history even after deletion
- Backups: State file backups (.tfstate.backup) also contain secrets
Impact on Remote Backends
Even with a remote backend like Azure Storage, risks persist:
terraform {
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstatexxxxxx"
container_name = "tfstate"
key = "prod.terraform.tfstate"
}
}
Risks with Remote Backends:
- Storage Access: Anyone with access to the storage account can read the state file and its secrets
- Azure Logs: Read/write operations may be logged with sensitive metadata
- Automatic Backups: Backup systems may create unencrypted copies
- Replication: Geographic replications duplicate secrets in multiple regions
Reference: Terraform State Management
Secrets in Source Code and Git
The Hard-Coding Problem
Hard-coding secrets directly in Terraform files is a dangerous but unfortunately common practice:
Problematic Example:
resource "azurerm_sql_server" "example" {
name = "example-sqlserver"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = "ThisIsATerriblePassword123!" # DANGER!
}
resource "azurerm_storage_account" "example" {
name = "examplestorage"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
account_tier = "Standard"
account_replication_type = "LRS"
# Hard-coded API key - DANGER!
tags = {
api_key = "ak_live_1234567890abcdefghijklmnop"
}
}
Consequences:
- Permanent Exposure: Once committed to Git, the secret remains in history
- Uncontrolled Access: All developers with repo access can see the secrets
- Rotation Difficulties: Changing a secret requires modifying code and redeploying
- Impossible Audit: Cannot trace who accessed the secrets
Risks with Git and Version Control Systems
# The secret is now in Git history
git add main.tf
git commit -m "Add SQL Server configuration"
git push origin main
# Even after deletion, the secret remains accessible
git log --all --full-history -- main.tf
git show <commit-hash>:main.tf
Exposure Vectors:
- Public Repositories: Secrets exposed to the internet if the repo is public
- Forks and Clones: Repository copies contain the secrets
- Pull Requests: Secrets may be visible in PR diffs
- CI/CD Logs: Pipelines may display secrets in logs
Secret Detection in Git
Use tools to scan Git history:
# Install gitleaks
brew install gitleaks
# Scan the repository
gitleaks detect --source . --verbose
# Scan complete history
gitleaks detect --source . --log-opts="--all"
Example Result:
Finding: administrator_login_password = "ThisIsATerriblePassword123!"
Secret: ThisIsATerriblePassword123!
RuleID: generic-api-key
Entropy: 3.891820
File: main.tf
Line: 15
Commit: a1b2c3d4e5f6
Author: developer@company.com
Date: 2024-10-29
Reference: Gitleaks Documentation
Issues with Outputs and Logs
Exposure in Terraform Outputs
Terraform outputs can also expose secrets:
output "database_connection_string" {
value = "Server=${azurerm_sql_server.example.fully_qualified_domain_name};Database=mydb;User Id=${azurerm_sql_server.example.administrator_login};******;"
}
Problem: This output displays the password in plaintext when running terraform output.
Plan and Apply Logs
Terraform commands can expose sensitive values:
# The plan displays values in plaintext
terraform plan
# Example of problematic output:
# + administrator_login_password = "ThisIsATerriblePassword123!"
Impact:
- CI/CD Logs: Pipelines store logs with secrets
- Terminal History: Commands with secrets remain in bash history
- System Logs: Logging systems may capture outputs
Managing Sensitive Values in Terraform
Terraform provides several native mechanisms to improve the management of sensitive information.
The sensitive Attribute
For Variables
Mark variables as sensitive to mask their value:
variable "database_password" {
description = "Password for the SQL Server administrator"
type = string
sensitive = true
}
variable "api_key" {
description = "API key for external service"
type = string
sensitive = true
}
Behavior:
# During plan, the value is masked
terraform plan
# Output:
# + administrator_login_password = (sensitive value)
Explanation: The sensitive = true attribute tells Terraform to mask the value in all outputs, logs, and plans. However, the value is still stored in plaintext in the state file.
For Outputs
Protect outputs containing sensitive information:
output "database_password" {
description = "The password for the database administrator"
value = azurerm_sql_server.example.administrator_login_password
sensitive = true
}
output "connection_string" {
description = "Full database connection string"
value = "Server=${azurerm_sql_server.example.fully_qualified_domain_name};..."
sensitive = true
}
Usage:
# Sensitive outputs don't display automatically
terraform output
# To see a specific sensitive value
terraform output -raw database_password
Explanation: Outputs marked as sensitive require explicit action to be displayed, reducing the risk of accidental exposure.
Reference: Sensitive Variables in Terraform
Environment Variables
Use environment variables to pass secrets:
# Define environment variables
export TF_VAR_database_password="SecurePassword123!"
export TF_VAR_api_key="sk_live_abcdefghijklmnop"
# Terraform automatically uses these variables
terraform plan
terraform apply
Terraform Configuration:
variable "database_password" {
type = string
sensitive = true
# No default value to force use of environment variable
}
resource "azurerm_sql_server" "example" {
name = "example-sqlserver"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = var.database_password
}
Advantages:
- Secrets not stored in files
- Facilitates CI/CD integration
- Compatible with secret managers
Limitations:
- Secrets remain in the state file
- Accessible via shell history
- No automatic rotation
Separate Variable Files
Use separate .tfvars files for secrets:
# variables.tf
variable "database_password" {
type = string
sensitive = true
}
variable "admin_username" {
type = string
}
# terraform.tfvars (NOT COMMITTED to Git)
admin_username = "sqladmin"
database_password = "SecurePassword123!"
# .gitignore
*.tfvars
!terraform.tfvars.example
terraform.tfstate*
.terraform/
Usage:
# Terraform automatically loads terraform.tfvars
terraform plan
# Or specify a specific file
terraform plan -var-file="production.tfvars"
Template for the Team:
# terraform.tfvars.example (committed to Git)
admin_username = "sqladmin"
database_password = "CHANGE_ME_TO_SECURE_PASSWORD"
api_key = "CHANGE_ME_TO_YOUR_API_KEY"
Explanation: This approach separates secrets from source code. Developers copy the example file and fill in the real values locally.
Integration with Azure Key Vault (Traditional Approach)
Reading Secrets from Key Vault
Use the azurerm_key_vault_secret data source:
# Reference to existing Key Vault
data "azurerm_key_vault" "example" {
name = "mykeyvault"
resource_group_name = "keyvault-rg"
}
# Read a secret
data "azurerm_key_vault_secret" "db_password" {
name = "database-admin-password"
key_vault_id = data.azurerm_key_vault.example.id
}
# Use the secret
resource "azurerm_sql_server" "example" {
name = "example-sqlserver"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
version = "12.0"
administrator_login = "sqladmin"
administrator_login_password = data.azurerm_key_vault_secret.db_password.value
}
Advantages:
- Centralization of secrets in Azure Key Vault
- Secret rotation without modifying Terraform code
- Audit and traceability via Azure Monitor
- Access control with Azure RBAC
Critical Limitations:
- The secret is still in the state file: The data source reads the secret and stores it in terraform.tfstate
- No runtime protection: The secret is exposed in memory during execution
- Logs and Outputs: Risk of exposure if misconfigured
Reference: Azure Key Vault with Terraform
Last Terraform feature Innovations: Ephemeral Resources
Introduction to Ephemeral Resources
Terraform 1.10+ introduces the revolutionary concept of ephemeral resources. These resources are specifically designed to handle sensitive data that should only exist at runtime and should never be stored in the state file.
Main Characteristics:
- No Persistence: Values are never written to terraform.tfstate
- Limited Lifetime: Exist only during Terraform execution
- Read-Only: Intended for consumption, not for creating permanent resources
- Security by Design: Designed to minimize secret exposure
Concept and Operation
Ephemeral Resource Syntax
ephemeral "azurerm_key_vault_secret" "db_password" {
name = "database-admin-password"
key_vault_id = data.azurerm_key_vault.example.id
}
Difference from Classic Data Sources:
| Aspect | Classic Data Source | Ephemeral Resource |
|---|---|---|
| State Storage | ✅ Yes | ❌ No |
| Persistence | Permanent | Temporary |
| Availability | Always | During execution only |
| Security | Medium | High |
| Use Case | Infrastructure data | Secrets and credentials |
Explanation: Ephemeral resources are read at runtime and their value is immediately used without ever being stored. This eliminates the major risk of exposure via the state file.
Lifecycle of Ephemeral Resources
┌─────────────────┐
│ terraform plan │
│ │
│ 1. Read secret │
│ from Key │
│ Vault │
│ │
│ 2. Use in │
│ memory │
│ │
│ 3. Validate │
│ plan │
│ │
│ 4. ❌ NO │
│ storage │
└─────────────────┘
↓
┌─────────────────┐
│ terraform apply │
│ │
│ 1. New read │
│ │
│ 2. Apply │
│ changes │
│ │
│ 3. ❌ NO │
│ storage │
└─────────────────┘
Limitations and Constraints
Ephemeral resources have intentional restrictions to ensure security:
# ❌ FORBIDDEN: Use ephemeral resource in output
output "password" {
value = ephemeral.azurerm_key_vault_secret.db_password.value
# ERROR: Ephemeral resources cannot be used in outputs
}
# ❌ FORBIDDEN: Store in local variable
locals {
saved_password = ephemeral.azurerm_key_vault_secret.db_password.value
# ERROR: Ephemeral values cannot be stored in locals
}
# ✅ ALLOWED: Direct use in a resource
resource "azurerm_sql_server" "example" {
administrator_login_password = ephemeral.azurerm_key_vault_secret.db_password.value
}
Explanation: These restrictions ensure that ephemeral values cannot be accidentally persisted or exposed.
Reference: Terraform Ephemeral Resources RFC
Practical Example with Azure Key Vault and Ephemeral Resources
Solution Architecture
Here's a complete architecture for securely managing secrets with Terraform and Azure:
┌─────────────────────────────────────────────────────┐
│ Azure Subscription │
│ │
│ ┌────────────────┐ ┌──────────────────┐ │
│ │ Key Vault │ │ SQL Server │ │
│ │ │ │ │ │
│ │ - Secrets │────────▶│ Uses ephemeral │ │
│ │ - Access │ │ secret for │ │
│ │ Policies │ │ admin password │ │
│ │ - RBAC │ │ │ │
│ └────────────────┘ └──────────────────┘ │
│ ▲ │
│ │ │
│ │ Ephemeral Read (not stored) │
│ │ │
│ ┌──────┴──────────────────────────────────────┐ │
│ │ Terraform Execution │ │
│ │ │ │
│ │ - Reads secrets at runtime │ │
│ │ - Never stores in tfstate │ │
│ │ - Applies configuration │ │
│ └─────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────┘
Infrastructure Setup
Step 1: Create Key Vault and Secrets
# Resource Group
resource "azurerm_resource_group" "example" {
name = "secure-terraform-rg"
location = "West Europe"
tags = {
Environment = "Production"
ManagedBy = "Terraform"
Purpose = "Secure Secrets Management"
}
}
# Current Azure AD Configuration
data "azurerm_client_config" "current" {}
# Key Vault
resource "azurerm_key_vault" "example" {
name = "secure-kv-${random_integer.suffix.result}"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "standard"
# Enhanced security
soft_delete_retention_days = 90
purge_protection_enabled = true
# Disable public access if needed
public_network_access_enabled = true
# Network ACLs to restrict access
network_acls {
default_action = "Deny"
bypass = "AzureServices"
ip_rules = ["203.0.113.0/24"] # Your public IP
}
tags = {
Environment = "Production"
ManagedBy = "Terraform"
}
}
# Random suffix for unique name
resource "random_integer" "suffix" {
min = 10000
max = 99999
}
# Access Policy for Terraform (Service Principal or Managed Identity)
resource "azurerm_key_vault_access_policy" "terraform" {
key_vault_id = azurerm_key_vault.example.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = data.azurerm_client_config.current.object_id
secret_permissions = [
"Get",
"List",
"Set",
"Delete",
"Recover",
"Backup",
"Restore"
]
}
# Create secrets (with recommended rotation)
resource "azurerm_key_vault_secret" "db_admin_password" {
name = "sql-admin-password"
value = random_password.db_admin.result
key_vault_id = azurerm_key_vault.example.id
content_type = "password"
tags = {
Purpose = "SQL Server Admin Password"
}
depends_on = [
azurerm_key_vault_access_policy.terraform
]
}
# Generate a secure password
resource "random_password" "db_admin" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
min_lower = 1
min_upper = 1
min_numeric = 1
min_special = 1
}
Explanation: This configuration creates a secure Key Vault with soft delete and purge protection enabled. Network ACLs restrict Key Vault access. The password is automatically generated with high complexity.
Step 2: Use Ephemeral Resources
# Terraform configuration with version 1.10+
terraform {
required_version = ">= 1.10"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "~> 4.39"
}
}
backend "azurerm" {
resource_group_name = "terraform-state-rg"
storage_account_name = "tfstatexxxxxx"
container_name = "tfstate"
key = "secure-prod.terraform.tfstate"
use_azuread_auth = true
}
}
provider "azurerm" {
features {
key_vault {
purge_soft_delete_on_destroy = false
recover_soft_deleted_key_vaults = true
}
}
}
# Key Vault reference
data "azurerm_key_vault" "example" {
name = "secure-kv-12345"
resource_group_name = "secure-terraform-rg"
}
# ✨ NEW: Ephemeral Resource for password
ephemeral "azurerm_key_vault_secret" "db_admin_password" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.example.id
}
# SQL Server using ephemeral secret
resource "azurerm_mssql_server" "example" {
name = "secure-sqlserver-${random_integer.suffix.result}"
resource_group_name = azurerm_resource_group.example.name
location = azurerm_resource_group.example.location
version = "12.0"
administrator_login = "sqladmin"
# ✨ Direct use of ephemeral secret
administrator_login_password = ephemeral.azurerm_key_vault_secret.db_admin_password.value
# Enhanced security
minimum_tls_version = "1.2"
public_network_access_enabled = false
azuread_administrator {
login_username = "AzureAD Admin"
object_id = data.azurerm_client_config.current.object_id
}
tags = {
Environment = "Production"
ManagedBy = "Terraform"
}
}
# SQL Database
resource "azurerm_mssql_database" "example" {
name = "exampledb"
server_id = azurerm_mssql_server.example.id
sku_name = "S0"
tags = {
Environment = "Production"
ManagedBy = "Terraform"
}
}
Explanation: The ephemeral resource ephemeral.azurerm_key_vault_secret.db_admin_password reads the secret from Key Vault at runtime. The value is used to configure the SQL Server but is never stored in the state file.
Step 3: Secure Network Configuration
# Private Endpoint for Key Vault
resource "azurerm_private_endpoint" "keyvault" {
name = "keyvault-pe"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "keyvault-privateserviceconnection"
private_connection_resource_id = azurerm_key_vault.example.id
is_manual_connection = false
subresource_names = ["vault"]
}
private_dns_zone_group {
name = "keyvault-dns-zone-group"
private_dns_zone_ids = [azurerm_private_dns_zone.keyvault.id]
}
}
# Private DNS Zone for Key Vault
resource "azurerm_private_dns_zone" "keyvault" {
name = "privatelink.vaultcore.azure.net"
resource_group_name = azurerm_resource_group.example.name
}
# Private Endpoint for SQL Server
resource "azurerm_private_endpoint" "sqlserver" {
name = "sqlserver-pe"
location = azurerm_resource_group.example.location
resource_group_name = azurerm_resource_group.example.name
subnet_id = azurerm_subnet.private_endpoints.id
private_service_connection {
name = "sqlserver-privateserviceconnection"
private_connection_resource_id = azurerm_mssql_server.example.id
is_manual_connection = false
subresource_names = ["sqlServer"]
}
private_dns_zone_group {
name = "sqlserver-dns-zone-group"
private_dns_zone_ids = [azurerm_private_dns_zone.sqlserver.id]
}
}
# Private DNS Zone for SQL Server
resource "azurerm_private_dns_zone" "sqlserver" {
name = "privatelink.database.windows.net"
resource_group_name = azurerm_resource_group.example.name
}
Explanation: Private Endpoints ensure that communication to Key Vault and SQL Server never traverses the public internet, adding a network security layer.
Reference: Azure Private Endpoints
Comparison: Before and After Ephemeral Resources
Traditional Approach (Data Source)
# ❌ Traditional approach - Security risks
data "azurerm_key_vault_secret" "db_password" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.example.id
}
resource "azurerm_mssql_server" "example" {
administrator_login_password = data.azurerm_key_vault_secret.db_password.value
# ...
}
Resulting State File:
{
"resources": [
{
"type": "azurerm_key_vault_secret",
"name": "db_password",
"instances": [
{
"attributes": {
"value": "SuperSecretPassword123!", // ❌ SECRET EXPOSED
"name": "sql-admin-password"
}
}
]
}
]
}
New Approach (Ephemeral Resource)
# ✅ New approach - Secure
ephemeral "azurerm_key_vault_secret" "db_password" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.example.id
}
resource "azurerm_mssql_server" "example" {
administrator_login_password = ephemeral.azurerm_key_vault_secret.db_password.value
# ...
}
Resulting State File:
{
"resources": [
{
"type": "azurerm_mssql_server",
"name": "example",
"instances": [
{
"attributes": {
"name": "example-sqlserver",
"administrator_login": "sqladmin",
// ✅ NO administrator_login_password in state
}
}
]
}
]
}
Explanation: With ephemeral resources, the password never appears in the state file, eliminating the most critical exposure risk.
Advanced Management: Automatic Secret Rotation
# Automatic password rotation with time_rotating
resource "time_rotating" "db_password_rotation" {
rotation_days = 90 # Rotation every 90 days
}
resource "random_password" "db_admin" {
length = 32
special = true
override_special = "!#$%&*()-_=+[]{}<>:?"
keepers = {
rotation_time = time_rotating.db_password_rotation.id
}
}
resource "azurerm_key_vault_secret" "db_admin_password" {
name = "sql-admin-password"
value = random_password.db_admin.result
key_vault_id = azurerm_key_vault.example.id
lifecycle {
create_before_destroy = true
}
}
# Ephemeral resource always uses latest version
ephemeral "azurerm_key_vault_secret" "db_admin_password" {
name = "sql-admin-password"
key_vault_id = data.azurerm_key_vault.example.id
}
Explanation: The time_rotating resource automatically triggers password regeneration every 90 days. The lifecycle.create_before_destroy ensures a new password is created before the old one is deleted, avoiding interruptions.
Reference: Time Provider
Best Practices and Recommendations
Securing the State File
Encryption at Rest
For Azure Storage Backend:
resource "azurerm_storage_account" "tfstate" {
name = "tfstate${random_integer.suffix.result}"
resource_group_name = azurerm_resource_group.state.name
location = azurerm_resource_group.state.location
account_tier = "Standard"
account_replication_type = "GRS"
# Encryption with customer-managed keys
customer_managed_key {
key_vault_key_id = azurerm_key_vault_key.tfstate.id
}
# Enhanced security
min_tls_version = "TLS1_2"
allow_nested_items_to_be_public = false
blob_properties {
versioning_enabled = true
delete_retention_policy {
days = 90
}
}
}
Explanation: Using Customer-Managed Keys (CMK) to encrypt state storage adds an extra layer of protection.
Strict Access Control
# RBAC for state file access
az role assignment create \
--role "Storage Blob Data Contributor" \
--assignee <terraform-sp-id> \
--scope "/subscriptions/<sub-id>/resourceGroups/terraform-state-rg/providers/Microsoft.Storage/storageAccounts/tfstate"
# Limit access to only necessary users
az role assignment create \
--role "Storage Blob Data Reader" \
--assignee <developer-group-id> \
--scope "/subscriptions/<sub-id>/resourceGroups/terraform-state-rg"
Explanation: Apply the principle of least privilege. Only Terraform service accounts should have write access, while developers can have read-only access if necessary.
Secret Rotation Strategy
| Secret Type | Rotation Frequency | Method |
|---|---|---|
| Admin Passwords | 90 days | Automatic with time_rotating
|
| External API Keys | 180 days | Manual with notification |
| Certificates | 365 days | Automatic via Key Vault |
| Access Tokens | 30 days | Automatic with OAuth refresh |
Implementing Rotation Alerts:
resource "azurerm_monitor_action_group" "secrets_rotation" {
name = "secrets-rotation-alerts"
resource_group_name = azurerm_resource_group.example.name
short_name = "secretrot"
email_receiver {
name = "security-team"
email_address = "security@company.com"
}
}
resource "azurerm_monitor_metric_alert" "key_vault_secret_expiry" {
name = "keyvault-secret-expiry"
resource_group_name = azurerm_resource_group.example.name
scopes = [azurerm_key_vault.example.id]
description = "Alert when secrets are about to expire"
severity = 2
criteria {
metric_namespace = "Microsoft.KeyVault/vaults"
metric_name = "ServiceApiLatency"
aggregation = "Average"
operator = "GreaterThan"
threshold = 1000
}
action {
action_group_id = azurerm_monitor_action_group.secrets_rotation.id
}
}
Security Checklist
Project Configuration
- [ ] No hard-coded secrets in .tf files
- [ ] All .tfvars files containing secrets are in .gitignore
- [ ] Sensitive variables marked with
sensitive = true - [ ] Sensitive outputs marked with
sensitive = true - [ ] No sensitive
terraform outputin CI/CD scripts
State File
- [ ] Remote backend configured (Azure Storage, Terraform Cloud, etc.)
- [ ] Encryption at rest enabled on backend
- [ ] Strict access control (RBAC) configured
- [ ] Versioning enabled for recovery
- [ ] Soft delete configured (90 days minimum)
Azure Key Vault
- [ ] Ephemeral resources used to read secrets
- [ ] Soft delete and purge protection enabled
- [ ] Network ACLs or Private Endpoints configured
- [ ] RBAC configured with least privilege principle
- [ ] Monitoring and alerts configured
- [ ] Automatic secret rotation implemented
CI/CD and Automation
- [ ] Environment variables used for secrets
- [ ] Integration with secret manager (GitHub Secrets, Azure DevOps Variables)
- [ ] Terraform logs filtered to mask sensitive values
- [ ] Service Principal or Managed Identity for authentication
- [ ] No access keys in pipelines
Compliance and Audit
- [ ] Key Vault access logs enabled
- [ ] Integration with Azure Monitor
- [ ] Alerts configured for suspicious access
- [ ] Documentation of rotation procedures
- [ ] Secret recovery tests
Anti-Patterns
❌ Anti-Patterns to Avoid
# ❌ BAD: Hard-coding secrets
resource "azurerm_sql_server" "bad" {
administrator_login_password = "Password123!"
}
# ❌ BAD: Secrets in default variables
variable "api_key" {
default = "sk_live_1234567890"
}
# ❌ BAD: Output secrets without sensitive
output "password" {
value = azurerm_sql_server.example.administrator_login_password
}
# ❌ BAD: Using data source instead of ephemeral
data "azurerm_key_vault_secret" "bad" {
name = "secret"
key_vault_id = data.azurerm_key_vault.example.id
}
# ❌ BAD: Secrets in tags
resource "azurerm_resource_group" "bad" {
tags = {
api_key = var.api_key
}
}
✅ Best Practices
# ✅ GOOD: Sensitive variables without default value
variable "api_key" {
type = string
sensitive = true
# No default - must be provided via environment variable
}
# ✅ GOOD: Ephemeral resources for secrets
ephemeral "azurerm_key_vault_secret" "good" {
name = "api-key"
key_vault_id = data.azurerm_key_vault.example.id
}
# ✅ GOOD: Protected sensitive outputs
output "connection_string" {
value = local.connection_string
sensitive = true
}
# ✅ GOOD: Automatic secret generation
resource "random_password" "good" {
length = 32
special = true
}
resource "azurerm_key_vault_secret" "good" {
name = "generated-password"
value = random_password.good.result
key_vault_id = azurerm_key_vault.example.id
}
# ✅ GOOD: Using Managed Identity
resource "azurerm_linux_virtual_machine" "good" {
identity {
type = "SystemAssigned"
}
}
Secure CI/CD Configuration
GitHub Actions
name: 'Secure Terraform'
on:
push:
branches: [ main ]
pull_request:
permissions:
id-token: write
contents: read
env:
ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
ARM_USE_OIDC: true
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Azure Login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.10.0
- name: Terraform Init
run: terraform init
# Filter sensitive logs
- name: Terraform Plan
run: |
terraform plan -no-color | grep -v "sensitive value"
continue-on-error: false
# Never log apply in production
- name: Terraform Apply
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve > /dev/null 2>&1
Explanation: This configuration uses OIDC for authentication without long-lived secrets. Logs are filtered and apply output is redirected to /dev/null to avoid exposing sensitive information.
Azure DevOps
trigger:
branches:
include:
- main
pool:
vmImage: 'ubuntu-latest'
variables:
- group: terraform-secrets # Variable group with secrets
stages:
- stage: Plan
jobs:
- job: TerraformPlan
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-Service-Connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
terraform init
terraform plan -no-color | grep -v "sensitive"
- stage: Apply
dependsOn: Plan
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- deployment: TerraformApply
environment: 'production'
strategy:
runOnce:
deploy:
steps:
- task: AzureCLI@2
inputs:
azureSubscription: 'Azure-Service-Connection'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
terraform init
terraform apply -auto-approve 2>&1 | grep -v "password"
Reference: GitHub Actions Security
Conclusion
Secure management of sensitive information is a fundamental pillar of any Infrastructure as Code strategy with Terraform and Azure. Throughout this article, we've explored the challenges, existing solutions, and major innovations that transform our approach to security.
Key Takeaways
1. Understanding the Risks:
Terraform state files and Git source code represent the two main vectors for secret exposure. Without appropriate precautions:
- Secrets are stored in plaintext in terraform.tfstate
- Git history preserves secrets indefinitely
- Logs and outputs can expose sensitive information
- Team sharing multiplies uncontrolled access points
2. Adopt Native Features:
Terraform provides basic but essential mechanisms:
- The
sensitiveattribute to mask values in outputs and logs - Environment variables to avoid hard-coding
- Separate uncommitted .tfvars files
- Integration with secure remote backends
3. Leverage Ephemeral Resources:
The feature revolution with ephemeral resources is a game-changer:
- Zero storage of secrets in the state file
- On-the-fly reading from Azure Key Vault
- Lifetime limited to Terraform execution
- Security by design with intentional restrictions
4. Implement a Comprehensive Strategy:
A holistic approach combines:
- Azure Key Vault as centralized source of truth
- Ephemeral resources for access without persistence
- Automatic secret rotation
- Complete monitoring and alerting
- Strict access controls with RBAC
- Private Endpoints for network isolation
Reference Architecture
┌─────────────────────────────────────────────────────────┐
│ Terraform Security Strategy │
│ │
│ Level 1: Source Code │
│ ✓ No hard-coded secrets │
│ ✓ .tfvars in .gitignore │
│ ✓ Sensitive variables marked │
│ │
│ Level 2: Execution │
│ ✓ Ephemeral resources for Key Vault │
│ ✓ Environment variables in CI/CD │
│ ✓ Sensitive log filtering │
│ │
│ Level 3: State │
│ ✓ Encrypted remote backend (Azure Storage) │
│ ✓ Strict RBAC │
│ ✓ Versioning and soft delete │
│ │
│ Level 4: Secrets Management │
│ ✓ Centralized Azure Key Vault │
│ ✓ Automatic rotation │
│ ✓ Private Endpoints │
│ ✓ Monitoring and audit │
│ │
└─────────────────────────────────────────────────────────┘
Implementation Roadmap
Phase 1: Audit and Cleanup (1-2 weeks)
- Scan Git history to detect exposed secrets
- Identify all hard-coded secrets in code
- Clean Git history with BFG Repo-Cleaner or git-filter-branch
- Revoke and regenerate all compromised secrets
Phase 2: Foundation (2-4 weeks)
- Deploy Azure Key Vault with secure configuration
- Migrate to remote backend for state file
- Implement RBAC and access controls
- Configure Private Endpoints
Phase 3: Migration to Ephemeral Resources (4-6 weeks)
- Update Terraform to version 1.10+
- Identify all secret data sources
- Convert to ephemeral resources
- Test in development then staging environment
Phase 4: Automation and Governance (Ongoing)
- Implement automatic secret rotation
- Configure monitoring and alerts
- Establish incident response procedures
- Train team on new practices
Future Perspectives
The Terraform and Azure ecosystem continues to evolve:
Expected Improvements:
- Native support for ephemeral resources in more providers
- Deeper integration between Terraform Cloud and Azure Key Vault
- Improved secret leak detection and prevention tools
- Automated compliance standards (SOC2, ISO 27001, HIPAA)
Emerging Trends:
- Growing adoption of secretless authentication (Workload Identity, OIDC)
- Homomorphic encryption for computation on sensitive data
- Zero Trust Architecture with continuous validation
- Integrated FinOps and secret governance
Final Recommendations
For New Projects:
- Start directly with Terraform 1.10+ and ephemeral resources
- Design your architecture with security from the start
- Automate secret rotation from day one
- Document your security practices
For Existing Projects:
- Audit your current infrastructure
- Prioritize migration of production environments
- Proceed in stages to minimize disruptions
- Train your team on new practices
For Teams:
- Establish clear secret management policies
- Automate security checks in CI/CD pipelines
- Share knowledge and experiences
- Stay updated with ecosystem evolution
Security is not a destination but a continuous journey. By adopting ephemeral resources and the best practices presented in this article, you build a more secure, compliant, and resilient Terraform infrastructure.
Remember: The best-protected secrets are those that never exist outside the moment they are used.
References
Official Terraform Documentation:
Azure Documentation:
- Azure Key Vault Overview
- Key Vault Best Practices
- Private Endpoints
- Azure Storage Security
- Customer-Managed Keys
Security and Compliance:
- Azure Security Baseline for Key Vault
- CIS Azure Foundations Benchmark
- OWASP Secrets Management Cheat Sheet
- Azure Defender for Key Vault
Tools and Utilities:
- Gitleaks - Secret Detection
- TFLint - Terraform Linter
- Checkov - IaC Security
- BFG Repo-Cleaner
- git-secrets - AWS Labs
CI/CD and Automation:
Terraform Providers:
Articles and Blogs:
Top comments (0)