DEV Community

Cover image for Managing Sensitive Information in Terraform and Azure
Mikael Krief
Mikael Krief

Posted on

Managing Sensitive Information in Terraform and Azure

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"
          }
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Security Issues:

  1. Plaintext Storage: All resource attributes, including passwords, are stored in plain text in the state file
  2. No Native Encryption: By default, Terraform does not encrypt the state file locally
  3. Git History: If the state file is committed to Git, secrets remain in the history even after deletion
  4. 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

Consequences:

  1. Permanent Exposure: Once committed to Git, the secret remains in history
  2. Uncontrolled Access: All developers with repo access can see the secrets
  3. Rotation Difficulties: Changing a secret requires modifying code and redeploying
  4. 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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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};******;"
}
Enter fullscreen mode Exit fullscreen mode

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!"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Behavior:

# During plan, the value is masked
terraform plan

# Output:
# + administrator_login_password = (sensitive value)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Usage:

# Sensitive outputs don't display automatically
terraform output

# To see a specific sensitive value
terraform output -raw database_password
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode
# terraform.tfvars (NOT COMMITTED to Git)
admin_username      = "sqladmin"
database_password   = "SecurePassword123!"
Enter fullscreen mode Exit fullscreen mode
# .gitignore
*.tfvars
!terraform.tfvars.example
terraform.tfstate*
.terraform/
Enter fullscreen mode Exit fullscreen mode

Usage:

# Terraform automatically loads terraform.tfvars
terraform plan

# Or specify a specific file
terraform plan -var-file="production.tfvars"
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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      │
└─────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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                    │   │
│  └─────────────────────────────────────────────┘   │
│                                                      │
└─────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
  # ...
}
Enter fullscreen mode Exit fullscreen mode

Resulting State File:

{
  "resources": [
    {
      "type": "azurerm_key_vault_secret",
      "name": "db_password",
      "instances": [
        {
          "attributes": {
            "value": "SuperSecretPassword123!",  //  SECRET EXPOSED
            "name": "sql-admin-password"
          }
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
  # ...
}
Enter fullscreen mode Exit fullscreen mode

Resulting State File:

{
  "resources": [
    {
      "type": "azurerm_mssql_server",
      "name": "example",
      "instances": [
        {
          "attributes": {
            "name": "example-sqlserver",
            "administrator_login": "sqladmin",
            //  NO administrator_login_password in state
          }
        }
      ]
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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 output in 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
  }
}
Enter fullscreen mode Exit fullscreen mode

✅ 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"
  }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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 sensitive attribute 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                                │
│                                                          │
└─────────────────────────────────────────────────────────┘
Enter fullscreen mode Exit fullscreen mode

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:

Security and Compliance:

Tools and Utilities:

CI/CD and Automation:

Terraform Providers:

Articles and Blogs:

Top comments (0)