Back to Blog
DevOps
4 min read

Building Secure-by-Default Terraform Modules for Azure

TerraformAzureSecurityIaCModules

After every security assessment, there's a scramble to fix things. Storage accounts without TLS 1.2, Key Vaults without purge protection, SQL servers with public access enabled.

The problem isn't that teams don't care about security - it's that the defaults are insecure, and there's no standard approach.

The solution? Terraform modules with security baked in.

The Module Approach

Instead of every team writing their own storage account resource with their own (inconsistent) security settings, create a shared module with sensible defaults:

# Storage Account Module Defaults
min_tls_version                 = "TLS1_2"
allow_nested_items_to_be_public = false
enable_https_traffic_only       = true
blob_soft_delete_days           = 30
container_soft_delete_days      = 30
network_rules_default_action    = "Deny"

Teams use the module, security is automatic. They can override if needed, but the path of least resistance is secure.

Priority Tiers for Security Settings

Not all security recommendations are equal. Here's how we prioritise:

Tier 1: Do Immediately (High Impact, Low Effort)

These should be non-negotiable defaults:

  • TLS 1.2 enforcement (Storage, SQL, App Services)
  • HTTPS only (App Services, Storage)
  • Key Vault purge protection
  • Disable anonymous blob access

Tier 2: Plan & Implement (Medium Effort, High Impact)

These need testing but should be standard:

  • Network rules "Deny" default (test connectivity first!)
  • SQL public network access disabled
  • Soft delete enabled (storage, key vault)

Tier 3: Consider Carefully (Higher Effort)

Enable where genuinely needed:

  • Blob versioning - only for critical production storage
  • Infrastructure encryption - only if compliance requires

Tier 4: Skip Unless Required (High Cost, Low Benefit)

Don't enable by default:

  • Customer-managed keys (Storage & SQL) - unless regulatory requirement
  • Client certificates (App Services) - VNet-locked apps don't need this

Module Structure

terraform-modules/
├── storage-account/
│   ├── main.tf           # Security defaults baked in
│   ├── variables.tf      # Overrides available but optional
│   ├── outputs.tf
│   └── README.md         # Documents security decisions
├── key-vault/
│   ├── main.tf           # Purge protection, RBAC, soft delete
│   └── ...
├── sql-database/
│   ├── main.tf           # TLS, auditing, private access
│   └── ...
└── app-service/
    ├── main.tf           # HTTPS only, TLS 1.2, managed identity
    └── ...

Why Modules Beat Policy

Azure Policy is great for guardrails, but it has limitations:

State conflicts - Policy remediation can conflict with Terraform state, causing drift and plan failures.

Reactive, not proactive - Policy catches violations after deployment. Modules prevent them before.

Limited flexibility - Policy is binary (allow/deny). Modules can have intelligent defaults with overrides.

Developer experience - Modules are familiar to Terraform users. Policy is a separate skill set.

Use Policy for audit and compliance reporting. Use Modules for enforcement.

Auditing Existing Resources

Before rolling out modules, audit what you have:

$subscriptions = Get-AzSubscription | Where-Object { $_.State -eq "Enabled" }
$results = @()

foreach ($sub in $subscriptions) {
    Set-AzContext -SubscriptionId $sub.Id | Out-Null
    $storageAccounts = Get-AzStorageAccount

    foreach ($sa in $storageAccounts) {
        $result = [PSCustomObject]@{
            Subscription = $sub.Name
            StorageAccount = $sa.StorageAccountName
            MinTlsVersion = $sa.MinimumTlsVersion
            AllowBlobPublicAccess = $sa.AllowBlobPublicAccess
            HttpsOnly = $sa.EnableHttpsTrafficOnly
        }
        $results += $result
    }
}
$results | Export-Csv "storage-audit.csv" -NoTypeInformation

This gives you a baseline. Fix the worst offenders, then prevent new ones with modules.

The Rollout

  1. Build the modules with security defaults
  2. Document the decisions - why each default is set
  3. Pilot with one team - get feedback, refine
  4. Mandate for new resources - all new infra uses modules
  5. Migrate existing resources - gradually, with testing

The goal isn't perfection overnight. It's making secure the easy choice.


Need help standardising your Terraform modules or auditing your Azure security posture? Get in touch - we help teams build secure infrastructure.

Need help with your Azure environment?

Get in touch for a free consultation.

Get in Touch