Bind a customer-managed key (CMK) to an existing Azure storage account so encryption at rest uses a key you control in Key Vault or a Managed HSM instead of the Microsoft-managed key. Targets
hashicorp/azurerm ~> 4.0.
- π Switches a storage account's server-side encryption from the Microsoft-managed key to a customer-managed key you hold in Azure Key Vault or a Managed HSM.
- π Steers toward a versionless key so the account tracks the key's current version and adopts rotations without a Terraform change.
- π§· Accepts exactly one key source β
key_vault_key_id(preferred),key_vault_id,key_vault_uri, ormanaged_hsm_key_idβ and rejects an ambiguous or empty call at plan time. - πͺͺ Reaches the key with the storage account's system-assigned identity by default, or a user-assigned identity, including the cross-tenant federated-identity path.
- π§© Is a standalone resource on purpose, so the storage account and the key vault can be created without a dependency cycle.
π‘ Why it matters: Regulated data platforms are routinely required to own the key that encrypts data at rest β to rotate it, audit its use, and revoke it independently of the platform. This module is that control for Azure Storage, applied as a reviewable, least-surprise piece of configuration.
If this module saves you time, please consider supporting it:
- β Star the repository to help others find it.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
uai["terraform-azurerm-user-assigned-identity"]
kv["terraform-azurerm-key-vault + key"]
sa["terraform-azurerm-storage-account"]
cmk["terraform-azurerm-storage-account-customer-managed-key"]
rg -->|"contains"| kv
rg -->|"contains"| sa
rg -->|"contains"| uai
kv -->|"key_vault_key_id"| cmk
sa -->|"storage_account_id"| cmk
uai -->|"user_assigned_identity_id"| cmk
cmk -->|"re-keys encryption of"| sa
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef target fill:#004578,stroke:#002d4d,color:#fff;
classDef sib fill:#eef3f8,stroke:#b9c8d8,color:#1b2733;
class cmk me;
class sa target;
class rg,uai,kv sib;
This module consumes its siblings by id: the storage account, the Key Vault key, and (optionally) a
user-assigned identity. It owns none of them β it only wires the key to the account.
flowchart LR
sa_in["storage_account_id"]
key_in["key_vault_key_id / key_vault_id / key_vault_uri / managed_hsm_key_id"]
idn["user_assigned_identity_id (+ federated_identity_client_id)"]
this["azurerm_storage_account_customer_managed_key.this"]
out_id["id"]
out_kv["key_vault_key_id"]
sa_in -->|"protects"| this
key_in -->|"exactly one"| this
idn -->|"reaches key via"| this
this -->|"emits"| out_id
this -->|"emits"| out_kv
classDef me fill:#0078D4,stroke:#004578,color:#fff;
classDef keystone fill:#004578,stroke:#002d4d,color:#fff;
classDef io fill:#eef3f8,stroke:#b9c8d8,color:#1b2733;
class this keystone;
class sa_in,key_in,idn,out_id,out_kv io;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_storage_account_customer_managed_key.this |
1 | The keystone: binds the chosen key to the storage account |
timeouts (dynamic block) |
0β1 | Optional per-operation timeouts |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | hashicorp/azurerm ~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, authentication, and subscription |
Schema notes that bite (verified against the live provider schema):
storage_account_idis force-new β changing it destroys the association (reverting the old account to its Microsoft-managed key) and rebuilds it against the new account.- Exactly one of
key_vault_key_id,key_vault_id,key_vault_uri,managed_hsm_key_idmay be set; none or more than one is rejected. federated_identity_client_idrequiresuser_assigned_identity_idβ it is the cross-tenant path and cannot stand alone.key_vault_id,key_vault_uri,managed_hsm_key_id,key_name, andkey_versionare the older split fields, superseded bykey_vault_key_idin the~> 4.0line and removed in the provider's next major line. Preferkey_vault_key_id.- This resource type does not support
tags; the module exposes notagsvariable.
- The caller's identity needs
Microsoft.Storage/storageAccounts/writeon the target account β most simplyStorage Account Contributorscoped to the storage account or its resource group β to set the account's encryption configuration. - Granting the storage account's identity crypto access to the key is a prerequisite performed out of band (see below), not an action this module takes.
- The
Microsoft.StorageandMicrosoft.KeyVaultresource providers registered on the subscription. - The target storage account already exists and has a managed identity β its system-assigned
identity, or a user-assigned identity passed as
user_assigned_identity_id. - The Key Vault (or Managed HSM) and the key already exist. The vault must have soft delete and purge protection enabled β Azure refuses to use a vault without purge protection as a storage CMK source.
- The identity the account uses is granted crypto access to the key out of band: on an
RBAC-authorization vault, the
Key Vault Crypto Service Encryption Userrole on the key or vault; on an access-policy vault, theGet,Wrap Key, andUnwrap Keykey permissions. - For the cross-tenant path, a multi-tenant application whose client ID is passed as
federated_identity_client_id, with the user-assigned identity federated to it. - The caller configures the
provider "azurerm" { features {} }block, authentication, and subscription.
terraform-azurerm-storage-account-customer-managed-key/
βββ providers.tf # required_version + azurerm ~> 4.0 pin; no provider block
βββ variables.tf # storage_account_id, the four key sources + companions, identity, timeouts
βββ main.tf # the single azurerm_storage_account_customer_managed_key.this
βββ outputs.tf # id (first), storage_account_id, key_vault_key_id
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT, Copyright (c) 2026 Casey Wood
βββ .gitignore # canonical library ignore set
# The caller configures the provider, authentication, and the mandatory features {} block.
provider "azurerm" {
features {}
}
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
# Versionless key ID -> the account tracks the current version (auto-rotation).
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage"
}π Pin
?ref=v1.0.0(or a later released tag) β never a branch. Plan-only; a human applies from CI.
Consumes
| Input | Type | Typical source |
|---|---|---|
storage_account_id |
string |
terraform-azurerm-storage-account (id) |
key_vault_key_id / key_vault_id / key_vault_uri / managed_hsm_key_id |
string |
terraform-azurerm-key-vault (a key ID / vault ID / vault URI) |
user_assigned_identity_id |
string |
terraform-azurerm-user-assigned-identity (id) |
Emits
| Output | Description |
|---|---|
id |
Resource ID of the CMK association (equals the storage account ID). Emitted first. |
storage_account_id |
Resource ID of the protected storage account. |
key_vault_key_id |
Resolved Key Vault key ID in effect; null for a Managed HSM key. |
The examples read the current tenant from the provider rather than hard-coding it.
data "azurerm_client_config" "current" {}1 Β· Minimal call β versionless key (auto-rotation)
The recommended baseline: one storage account, one versionless Key Vault key ID.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage"
}π‘ A versionless key ID (no trailing version segment) lets the account follow the key's current version, so a rotation in Key Vault is adopted without a plan.
2 Β· Pinned key version
Freeze the account on a specific version by appending it to the key ID.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage/abcdef0123456789abcdef0123456789"
}
β οΈ A pinned version does not auto-rotate. You must update this value on each rotation, or the account keeps using the old version.
3 Β· Split form β key_vault_id + key_name (auto-rotation)
The older split form: reference the vault and key by separate fields, leaving key_version unset for
auto-rotation.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sec/providers/Microsoft.KeyVault/vaults/kv-example"
key_name = "cmk-storage"
}βΉοΈ
key_vault_key_idis the forward-compatible choice; the split fields are supported for existing configurations and are removed in the provider's next major line.
4 Β· Split form β pinned version
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-sec/providers/Microsoft.KeyVault/vaults/kv-example"
key_name = "cmk-storage"
key_version = "abcdef0123456789abcdef0123456789"
}π
key_name/key_versiononly apply to the split forms (key_vault_id/key_vault_uri). The module validates this so a stray pairing withkey_vault_key_idis caught early.
5 Β· Managed HSM key
Use a key held in a Managed HSM pool (single-tenant, FIPS 140-2 Level 3) where the compliance regime calls for a dedicated HSM.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
managed_hsm_key_id = "https://hsm-example.managedhsm.azure.net/keys/cmk-storage"
}π‘ A versionless Managed HSM key ID also auto-rotates. The
key_vault_key_idoutput is null when the source is a Managed HSM key.
6 Β· User-assigned identity access
Reach the key with a specific user-assigned identity instead of the account's system-assigned identity.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage"
user_assigned_identity_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-id/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai-storage-cmk"
}π The chosen identity must already hold
Key Vault Crypto Service Encryption Useron the key (RBAC vault) orGet/Wrap Key/Unwrap Key(access-policy vault). Grant it out of band.
7 Β· Cross-tenant federated identity
The storage account is in one tenant; the key vault is in another. Pair a user-assigned identity with the multi-tenant application's client ID and the vault's URI.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_uri = "https://kv-customer.vault.azure.net/"
key_name = "cmk-storage"
user_assigned_identity_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-id/providers/Microsoft.ManagedIdentity/userAssignedIdentities/uai-xtenant"
federated_identity_client_id = "11111111-2222-3333-4444-555555555555"
}
β οΈ federated_identity_client_idcannot stand alone β it requiresuser_assigned_identity_id. The module validates this.
8 Β· Custom operation timeouts
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage"
timeouts = {
create = "30m"
update = "30m"
delete = "30m"
}
}βΉοΈ Timeouts are Go duration strings. Omit any field to accept the provider default.
9 Β· One key across many accounts (for_each at scale)
Apply the same organizational key to a set of storage accounts.
locals {
cmk_accounts = {
logs = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stlogs01"
exports = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexports01"
archive = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/starchive01"
}
}
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
for_each = local.cmk_accounts
storage_account_id = each.value
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage"
}π‘ A stable map key (
logs,exports,archive) keeps the plan steady when accounts are added or removed.
10 Β· Per-account keys (for_each over a map of objects)
Give each account its own key and identity.
locals {
cmk = {
logs = {
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stlogs01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-logs"
}
exports = {
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexports01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-exports"
}
}
}
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
for_each = local.cmk
storage_account_id = each.value.storage_account_id
key_vault_key_id = each.value.key_vault_key_id
}11 Β· Why this is a separate resource (breaking the cycle)
The storage account needs an identity; the vault grant needs that identity; the CMK needs the grant. Model the account, its identity, the grant, and this binding as separate steps β never the key inline on the account.
# 1) Storage account with a system-assigned identity (its own module).
# 2) Grant that identity crypto access on the key (RBAC role assignment, its own module).
# 3) Bind the key here, after the grant exists.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = module.storage_account.id
key_vault_key_id = module.key_vault.key_ids["storage-cmk"]
depends_on = [module.crypto_role] # ensure the grant lands before the binding
}βΉοΈ Because encryption enablement depends on the key access already existing, ordering the grant before this binding (implicitly via references, or explicitly via
depends_on) avoids a first-apply failure.
12 Β· Security / least-privilege variant
System-assigned identity, versionless key, RBAC-authorization vault β the tightest common posture.
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = module.storage_account.id
# Versionless -> auto-rotation; no user-assigned identity -> the account's own
# system-assigned identity, scoped by a single Crypto Service Encryption User grant.
key_vault_key_id = module.key_vault.key_ids["storage-cmk"]
}π Keep the grant to the account's identity as narrow as possible β the
Key Vault Crypto Service Encryption Userrole on the single key, not the whole vault, where your governance allows it.
13 Β· ποΈ End-to-end composition
Resource group, Key Vault with a key, user-assigned identity, storage account, and this CMK binding, wired by outputs. Sibling module inputs/outputs are illustrative of this suite.
provider "azurerm" {
features {}
}
module "resource_group" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-data"
location = "eastus2"
}
module "user_assigned_identity" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
name = "uai-storage-cmk"
resource_group_name = module.resource_group.name
location = module.resource_group.location
}
module "key_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
tenant_id = data.azurerm_client_config.current.tenant_id
name = "kv-data-eus2"
resource_group_name = module.resource_group.name
location = module.resource_group.location
# Hardened by default in this suite: RBAC authZ, purge protection on.
keys = {
storage-cmk = {
key_type = "RSA"
key_size = 3072
key_opts = ["wrapKey", "unwrapKey"]
}
}
}
# Grant the identity crypto access to the key (its own aggregation module).
module "crypto_role" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-role-assignments.git?ref=v1.0.0"
scope = module.key_vault.id
role_assignments = {
storage-cmk = {
role_definition_name = "Key Vault Crypto Service Encryption User"
principal_id = module.user_assigned_identity.principal_id
}
}
}
module "storage_account" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account.git?ref=v1.0.0"
name = "stdataeus201"
resource_group_name = module.resource_group.name
location = module.resource_group.location
identity = {
type = "UserAssigned"
identity_ids = [module.user_assigned_identity.id]
}
}
module "storage_cmk" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-storage-account-customer-managed-key.git?ref=v1.0.0"
storage_account_id = module.storage_account.id
key_vault_key_id = module.key_vault.key_ids["storage-cmk"]
user_assigned_identity_id = module.user_assigned_identity.id
depends_on = [module.crypto_role] # the grant must exist before the account can use the key
}π The key vault carries purge protection and RBAC authorization from the sibling module's secure defaults β both are prerequisites for a storage CMK. The
depends_onorders the crypto grant ahead of the binding so the first apply succeeds.
Identity (required)
| Name | Type | Description |
|---|---|---|
storage_account_id |
string |
Resource ID of the storage account to protect. Force-new. |
Key source (set exactly one of the four; key_name / key_version accompany the split forms)
| Name | Type | Default | Description |
|---|---|---|---|
key_vault_key_id |
string |
null |
Preferred single-field key ID. Versionless = auto-rotation. |
key_vault_id |
string |
null |
Split form β vault ID; pair with key_name. |
key_vault_uri |
string |
null |
Split form for cross-tenant β vault URI; pair with key_name. |
managed_hsm_key_id |
string |
null |
A key in a Managed HSM pool. |
key_name |
string |
null |
Key name for the split forms. |
key_version |
string |
null |
Pin a version; leave null for auto-rotation. |
Access identity (optional)
| Name | Type | Default | Description |
|---|---|---|---|
user_assigned_identity_id |
string |
null |
Identity used to reach the key; defaults to the account's system-assigned identity. |
federated_identity_client_id |
string |
null |
Cross-tenant app client ID; requires user_assigned_identity_id. |
Universal tail (this resource type does not support tags)
| Name | Type | Default | Description |
|---|---|---|---|
timeouts |
object |
null |
Optional create / read / update / delete duration strings. |
Full variable schemas
variable "storage_account_id" {
type = string # required; force-new
}
# Key source β exactly one of the following four (enforced by validation):
variable "key_vault_key_id" { type = string, default = null } # preferred
variable "key_vault_id" { type = string, default = null }
variable "key_vault_uri" { type = string, default = null }
variable "managed_hsm_key_id" { type = string, default = null }
# Companions to the split (key_vault_id / key_vault_uri) forms:
variable "key_name" { type = string, default = null }
variable "key_version" { type = string, default = null } # null = auto-rotation
# Access identity:
variable "user_assigned_identity_id" { type = string, default = null }
variable "federated_identity_client_id" { type = string, default = null } # requires user_assigned_identity_id
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}Validations enforced by the module:
- Exactly one of
key_vault_key_id/key_vault_id/key_vault_uri/managed_hsm_key_id. key_name/key_versiononly withkey_vault_idorkey_vault_uri.federated_identity_client_idonly withuser_assigned_identity_id.
| Output | Description | Kind |
|---|---|---|
id |
Resource ID of this binding - which IS THE STORAGE ACCOUNT'S OWN ID, not a distinct child resource's | Passthrough |
storage_account_id |
Resource ID of the storage account whose encryption this binding controls | Passthrough |
key_vault_key_id |
The Key Vault key in force, as Azure resolved it | Passthrough |
user_assigned_identity_id |
The user-assigned identity the storage account uses to reach the key, or null when the account's system-assigned identity is used instead | Passthrough |
federated_identity_client_id |
Client ID of the multi-tenant application used for CROSS-TENANT customer-managed keys, or null in the ordinary single-tenant case | Passthrough |
key_source_argument_used |
Which of the four mutually-exclusive key-source forms this call used: key_vault_key_id, key_vault_id, key_vault_uri or managed_hsm_key_id | Passthrough |
deprecated_key_source_in_use |
True when the call uses one of the key-source forms the provider has deprecated - key_vault_uri, key_vault_id or managed_hsm_key_id, along with their key_name and key_version companions | Passthrough |
key_rotation_is_automatic |
True when the key is referenced WITHOUT a version, so the account follows the key's current version and a rotation in Key Vault is adopted with no Terraform change | Passthrough |
uses_cross_tenant_key |
True when a federated identity client ID is supplied, meaning the key lives in a DIFFERENT Entra tenant from the storage account | Derived |
uses_managed_hsm |
True when the key is held in a Managed HSM rather than a standard Key Vault | Derived |
identity_used_is_system_assigned |
True when no user-assigned identity was supplied, so the storage account's SYSTEM-assigned identity is used to reach the key | Derived |
destroy_reverts_to_microsoft_managed_keys |
Constant true, and the fact most worth knowing here | Constant |
is_a_singleton_per_storage_account |
Constant true | Constant |
vault_needs_purge_protection |
Constant true, and a prerequisite rather than a recommendation | Constant |
key_access_can_break_after_apply |
Constant true | Constant |
account_must_support_customer_managed_keys |
Constant true | Constant |
No secret is emitted. The resolved key ID is a resource identifier, not key material.
- A binding, not a container. The keystone
thishas no name of its own; itsidis the storage account's ID. The module therefore emitsidfirst and then useful, non-secret computed values β it has nonameoutput because the resource has no name field. - Force-new on
storage_account_id. Re-pointing this at a different account is a destroy-and-create: the old account reverts to the Microsoft-managed key. Treat the account/binding pair as one unit. - Own the CMK in exactly one place. Do not also configure a
customer_managed_keyblock inline on the storage account resource for the same account β managing it both ways produces perpetual diffs. This module is the single owner. - Exactly one key source, enforced at parse time. The
validationblock surfaces a missing or ambiguous key source as a clear error before any API call, mirroring the provider's own constraint. - Deprecated split fields still work but signal the future.
key_vault_id,key_vault_uri,managed_hsm_key_id,key_name, andkey_versionare retained in the~> 4.0line and removed in the next major line. The module keeps them for existing configurations and steers new callers tokey_vault_key_id. Outputs read only the non-deprecated attribute, so akey_vault_key_idcaller sees no deprecation notices. - Auto-rotation is a property of the reference, not a toggle. A versionless key ID (or an unset
key_version) is what makes the account follow rotations; a version segment pins it. features {}is the caller's. In isolation the module has no provider block; a root module must supplyprovider "azurerm" { features {} }.
| Concern | Secure default (minimal call) | Opt-out (caller must type it) |
|---|---|---|
| Key rotation | Versionless key β auto-rotation (key_version unset) |
Pin a version (key_version or a versioned key ID) |
| Key reference field | Prefer key_vault_key_id (non-deprecated, next-major-ready) |
Use a split / deprecated field |
| Key source count | Exactly one β enforced by validation |
β (cannot be relaxed) |
| Access identity | Account's system-assigned identity | Supply user_assigned_identity_id |
| Cross-tenant | Off (same tenant) | Set user_assigned_identity_id + federated_identity_client_id |
| Secret handling | No secret accepted or emitted | β |
# From the module folder β offline, no cloud calls.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module
?ref=v1.0.0(or a later released tag) β never a branch. - This library is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials.
The offline proof gate covers everything that does not require Azure:
terraform init -backend=falseβ resolves the pinned provider without a backend.terraform validateβ proves the configuration is type-correct against the pinned provider schema, including the module'svalidationblocks.terraform fmt -checkβ enforces canonical formatting.
What only a human terraform plan (from CI, with credentials) exercises: whether the storage account
has a usable identity, whether that identity holds crypto access on the key, and whether the vault has
purge protection β the ARM API validates these at plan/apply time, not offline.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
storage_account_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data/providers/Microsoft.Storage/storageAccounts/stexample01"
key_vault_key_id = "https://kv-example.vault.azure.net/keys/cmk-storage/9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d"| Symptom | Cause | Fix |
|---|---|---|
one of managed_hsm_key_id, key_vault_id, key_vault_uri, key_vault_key_id must be specified |
No key source set | Set exactly one of the four key fields. |
| Module error: "Set exactly one key source" | Two or more key fields set | Leave exactly one non-null. |
federated_identity_client_id: requires user_assigned_identity_id |
Cross-tenant client ID set alone | Also set user_assigned_identity_id. |
403 / access-denied to Key Vault on apply |
The account's identity lacks crypto access, or the vault lacks purge protection | Grant Key Vault Crypto Service Encryption User (or Get/Wrap/Unwrap) and enable soft delete + purge protection. |
Deprecation warning about key_vault_id / key_vault_uri / managed_hsm_key_id / key_name / key_version |
Using a split / deprecated field | Migrate to key_vault_key_id. |
| Rotation not picked up by the account | Pinned key_version (or a versioned key ID) |
Use a versionless key reference. |
| Perpetual diff / re-key on every plan | The CMK is managed both here and inline on the storage account | Own it in one place β remove the inline block. |
| Replacement planned unexpectedly | storage_account_id changed |
Expected β it is force-new; treat account and binding as one unit. |
- Provider resource:
azurerm_storage_account_customer_managed_key - Provider resource:
azurerm_storage_account - Azure concept: Customer-managed keys for Azure Storage encryption
- Azure concept: Configure cross-tenant customer-managed keys
- Sibling modules:
terraform-azurerm-storage-account,terraform-azurerm-key-vault,terraform-azurerm-user-assigned-identity,terraform-azurerm-role-assignments. - This module's
SCOPE.md.
π "Infrastructure as Code should be standardized, consistent, and secure."