| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
A single Key Vault access policy that grants one principal least-privilege permissions over a vault's keys, secrets, certificates, and managed-storage objects — targeting hashicorp/azurerm ~> 4.0.
This module manages a single azurerm_key_vault_access_policy on a Key Vault that uses the access-policy authorization model (not RBAC). It grants exactly one principal — a user, service principal, group, or managed identity — a scoped set of permissions over the vault's data planes:
Every permission list defaults to empty, so the empty call grants nothing. An all-empty policy is accepted rather than refused: the provider publishes no at-least-one-permission requirement for this resource, so enforcing one here would reject input the provider accepts — and, because a failed validation {} blocks terraform destroy as well as apply, would strand anyone who already owns such a policy. The condition is reported instead, through the grants_no_access output.
⚠️ Its sibling terraform-azurerm-application-insights-api-key does the opposite, and correctly: there the provider does require at least one permission, in its Create function body, so the module mirrors that rule as a validation to move the refusal offline. Two grant resources, opposite answers — do not carry the assumption between them.
💡 Why it matters: Access policies are the classic Key Vault authorization model. Getting them wrong either locks out a workload or over-grants it. This module encodes the principal, tenant, and vault as immutable inputs and validates every permission string against its closed enum before Terraform ever calls Azure, so a typo fails at plan rather than surfacing as a runtime authorization gap.
If this module saves you time, please consider supporting its continued development:
flowchart LR
kv["terraform-azurerm-key-vault"]
uai["terraform-azurerm-user-assigned-identity"]
me["terraform-azurerm-key-vault-access-policy"]
v["azurerm_key_vault_access_policy"]
kv -->|"key_vault_id"| me
uai -->|"object_id (principal)"| me
me -->|"creates"| v
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
classDef target fill:#004578,stroke:#002d4d,color:#ffffff;
classDef ext fill:#f2f2f2,stroke:#c8c8c8,color:#111111;
class me me;
class v target;
class kv,uai ext;
The Key Vault module supplies the key_vault_id; the user-assigned identity module (or any caller-supplied principal) supplies the object_id. This module owns only the access policy record.
flowchart LR
in_id["key_vault_id / tenant_id / object_id / application_id"]
in_perm["key / secret / certificate / storage permissions"]
res["azurerm_key_vault_access_policy.this"]
out_id["id"]
in_id -->|"input"| res
in_perm -->|"input"| res
res -->|"output"| out_id
classDef me fill:#0078D4,stroke:#004578,color:#ffffff;
class res me;
Resource inventory
| Resource | Name | Cardinality | Role |
|---|---|---|---|
| azurerm_key_vault_access_policy | this | single (keystone) | Grants one principal scoped permissions on the vault. |
| timeouts (nested block) | — | optional | Per-operation create/read/update/delete timeouts. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| Provider | hashicorp/azurerm ~> 4.0 |
| Provider block | None in this module — the caller configures provider "azurerm", including the mandatory features {} block, plus authentication, subscription, and tenant. |
Schema notes that bite
Least-privilege at the Key Vault scope, on the caller's identity:
The built-in Key Vault Contributor role (management plane) covers editing access policies.
terraform-azurerm-key-vault-access-policy/
├── providers.tf # required_version + azurerm ~> 4.0 pin; no provider block
├── variables.tf # typed inputs: identity, four permission lists, timeouts
├── main.tf # keystone azurerm_key_vault_access_policy.this + timeouts block
├── outputs.tf # id (Terraform-unique)
├── README.md # this document
├── SCOPE.md # cross-module contract
├── LICENSE # MIT
└── .gitignore # canonical library ignore set
provider "azurerm" {
features {}
}
module "app_secret_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.example.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_user_assigned_identity.app.principal_id
secret_permissions = ["Get", "List"]
}ℹ️ The caller owns the provider block, authentication, and the mandatory features {} block. This module never declares a provider.
Consumes
| Input | Type | Typical source |
|---|---|---|
| key_vault_id | string | terraform-azurerm-key-vault (id) |
| object_id | string | terraform-azurerm-user-assigned-identity (principal_id) or a caller-supplied principal |
| tenant_id | string | caller / azurerm_client_config data source |
Emits
| Output | Description | Consumed by |
|---|---|---|
| id | Resource ID of the access policy (Terraform-unique; does not map to a distinct Azure object). | References / downstream depends_on sequencing. |
The most common shape — a workload that reads connection strings and API keys but never writes them.
module "secret_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_user_assigned_identity.app.principal_id
secret_permissions = ["Get", "List"]
}2 · Secret read/write for a provisioning identity🔒 Grant only Get and List. A reader that cannot Set or Delete cannot tamper with secret material.
A CI or provisioning identity that seeds secrets needs write and lifecycle permissions.
module "secret_writer" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.provisioner_object_id
secret_permissions = ["Get", "List", "Set", "Delete", "Recover"]
}3 · Key crypto for CMK wrap/unwrap⚠️ Recover matters when the vault has soft-delete enabled — without it, a name that was soft-deleted cannot be reclaimed by this principal.
Customer-managed key (CMK) scenarios — a Storage account, disk, or SQL server that wraps its data encryption key with a Key Vault key needs exactly WrapKey, UnwrapKey, and Get.
module "cmk_consumer" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.cmk.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_storage_account.data.identity[0].principal_id
key_permissions = ["Get", "WrapKey", "UnwrapKey"]
}4 · Key administration and rotation💡 This is the canonical CMK grant. The consuming service uses the key to wrap its own DEK; it never needs to export or delete the key itself.
A key operator that creates and rotates keys needs the full lifecycle plus rotation-policy management.
module "key_admin" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.cmk.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.key_operator_object_id
key_permissions = [
"Get", "List", "Create", "Import", "Update", "Delete", "Recover",
"Rotate", "GetRotationPolicy", "SetRotationPolicy",
]
}5 · Certificate managementℹ️ Rotate, GetRotationPolicy, and SetRotationPolicy are what let this principal drive automated key rotation — omit them for a principal that only consumes keys.
A certificate operator managing TLS certificates and their issuers.
module "cert_manager" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.tls.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.cert_operator_object_id
certificate_permissions = [
"Get", "List", "Create", "Import", "Update", "Delete",
"ManageContacts", "ManageIssuers", "GetIssuers", "ListIssuers", "SetIssuers",
]
}6 · Managed-storage permissions🔒 Issuer permissions (ManageIssuers, SetIssuers, …) are only needed by principals that integrate with a CA. A principal that merely reads certificates needs just Get and List.
Key Vault managed storage account keys — a principal that rotates storage account keys and issues SAS tokens through the vault.
module "storage_key_manager" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.ops.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.storage_ops_object_id
storage_permissions = ["Get", "List", "GetSAS", "SetSAS", "RegenerateKey"]
}7 · Compound identity with application_id⚠️ RegenerateKey rotates the underlying storage account key on the vault's schedule. Grant it only to the identity responsible for storage-key lifecycle.
Some legacy on-behalf-of scenarios require a compound identity — a service principal acting through a specific application. Supplying application_id alongside object_id scopes the policy to that pairing.
module "compound_identity" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.principal_object_id
application_id = var.application_object_id
secret_permissions = ["Get"]
}8 · Mixed keys and secrets for a single workload⚠️ application_id is force-new and rarely needed. Leave it null (the default) for standard user, group, or service-principal grants.
A workload that both reads a secret and unwraps a CMK in one grant.
module "mixed_grant" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = azurerm_user_assigned_identity.app.principal_id
key_permissions = ["Get", "UnwrapKey"]
secret_permissions = ["Get", "List"]
}9 · Break-glass administrator💡 A single access policy can span multiple data planes. Keep each list minimal — grant keys OR secrets OR both, but only the verbs the workload actually calls.
An emergency human operator granted broad access across all four data planes. Reserve this for a tightly controlled break-glass group.
module "break_glass" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.break_glass_group_object_id
key_permissions = ["Get", "List", "Create", "Delete", "Recover", "Backup", "Restore"]
secret_permissions = ["Get", "List", "Set", "Delete", "Recover", "Backup", "Restore"]
certificate_permissions = ["Get", "List", "Create", "Delete", "Recover", "Backup", "Restore"]
}10 · Backup and restore only🔒 Broad grants belong to audited groups, not standing individual identities. Pair this with diagnostic logging on the vault so every operation is recorded.
A backup service that needs to back up and restore vault objects but must not read their plaintext values.
module "backup_service" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.backup_service_object_id
key_permissions = ["Backup", "Restore"]
secret_permissions = ["Backup", "Restore"]
}11 · Custom timeouts for a slow-provisioning tenant🔒 Backup returns an encrypted blob, not plaintext. A backup identity that lacks Get can move material between vaults without ever seeing the underlying secret.
Override the default operation timeouts where a busy tenant is slow to apply access-policy changes.
module "with_timeouts" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = var.principal_object_id
secret_permissions = ["Get", "List"]
timeouts = {
create = "30m"
delete = "30m"
}
}12 · for_each over multiple principalsℹ️ Leave timeouts null (the default) to use the provider defaults. Only set it when you have observed genuine timeouts.
Grant the same secret-read permission to a keyed map of principals, so adding or removing one never re-indexes the rest.
variable "readers" {
type = map(string) # key => principal object ID
default = {
api = "00000000-0000-0000-0000-000000000001"
worker = "00000000-0000-0000-0000-000000000002"
scanner = "00000000-0000-0000-0000-000000000003"
}
}
module "secret_readers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
for_each = var.readers
key_vault_id = azurerm_key_vault.app.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = each.value
secret_permissions = ["Get", "List"]
}13 · 🏗️ End-to-end composition💡 Key the map by a stable role name, not by list index. Each object_id must be unique across the vault's policies, which map keys naturally enforce here.
Wire a resource group, an access-policy-model Key Vault, and a user-assigned identity into this module — the full path from infrastructure to a scoped grant.
provider "azurerm" {
features {}
}
data "azurerm_client_config" "current" {}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-vault-demo"
location = "eastus2"
}
module "identity" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-user-assigned-identity.git?ref=v1.0.0"
name = "id-app-demo"
resource_group_name = module.rg.name
location = "eastus2"
}
module "vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
name = "kv-demo-eus2"
resource_group_name = module.rg.name
location = "eastus2"
tenant_id = data.azurerm_client_config.current.tenant_id
# This module manages access policies, so the vault must use the access-policy model.
rbac_authorization_enabled = false
}
module "app_secret_reader" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault-access-policy.git?ref=v1.0.0"
key_vault_id = module.vault.id
tenant_id = data.azurerm_client_config.current.tenant_id
object_id = module.identity.principal_id
secret_permissions = ["Get", "List"]
}🔒 The vault is created with enable_rbac_authorization = false so it honors access policies. Never also declare inline access_policy blocks on that vault — this module owns them.
Identity (required, all force-new except application_id)
Permissions (each list(string), default [], element-validated against its enum)
Universal tail
variable "key_vault_id" {
type = string # force-new
}
variable "tenant_id" {
type = string # force-new
}
variable "object_id" {
type = string # force-new; unique across the vault's access policies
}
variable "application_id" {
type = string # force-new; compound identity
default = null
}
variable "key_permissions" {
type = list(string)
default = []
# each element ∈ Backup, Create, Decrypt, Delete, Encrypt, Get, Import, List,
# Purge, Recover, Restore, Sign, UnwrapKey, Update, Verify, WrapKey, Release,
# Rotate, GetRotationPolicy, SetRotationPolicy
}
variable "secret_permissions" {
type = list(string)
default = []
# each element ∈ Backup, Delete, Get, List, Purge, Recover, Restore, Set
}
variable "certificate_permissions" {
type = list(string)
default = []
# each element ∈ Backup, Create, Delete, DeleteIssuers, Get, GetIssuers, Import,
# List, ListIssuers, ManageContacts, ManageIssuers, Purge, Recover, Restore,
# SetIssuers, Update
}
variable "storage_permissions" {
type = list(string)
default = []
# each element ∈ Backup, Delete, DeleteSAS, Get, GetSAS, List, Purge, Recover,
# RegenerateKey, Restore, Set, SetSAS, Update
# plus a cross-list check: total permissions across all four lists must be > 0
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Kind |
|---|---|---|
| id | Passthrough | |
| key_vault_id | Resource ID of the vault this policy is attached to - a REAL Azure Resource ID, unlike this resource's own id | Passthrough |
| tenant_id | The Entra ID tenant the policy authenticates against | Passthrough |
| object_id | Object ID of the principal granted access | Passthrough |
| application_id | Application (client) object ID for the compound-identity case, or null | Passthrough |
| key_permissions | Key permissions granted, as stored | Passthrough |
| secret_permissions | Secret permissions granted, as stored | Passthrough |
| certificate_permissions | Certificate permissions granted, as stored | Passthrough |
| storage_permissions | Managed-storage permissions granted, as stored | Passthrough |
| permission_count | Total number of permissions granted across all four families | Passthrough |
| grants_no_access | True when all four permission lists are empty | Passthrough |
| grants_purge_permission | True when Purge appears in ANY of the four permission families | Passthrough |
| grants_privileged_key_permissions | True when the policy grants Purge or Release on keys - the two the provider classes as privileged | Passthrough |
| grants_secret_read | True when the policy grants Get on secrets - the permission that reads secret VALUES rather than merely enumerating names | Passthrough |
| grants_storage_sas_issuance | True when the policy grants GetSAS or SetSAS on managed storage | Passthrough |
| id_is_not_an_azure_resource_id | Always true, and the fact most likely to be misused about this module | Constant |
| cannot_be_combined_with_the_vaults_inline_access_policy_block | Always true | Constant |
| is_inert_if_the_vault_uses_rbac_authorization | Always true, and unverifiable from here | Constant |
| applies_are_serialised_per_vault | Always true | Constant |
| every_argument_is_force_new | Always true | Constant |
| Principle | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Grant nothing by default | All four permission lists default to [] | Populate the relevant list(s) |
| Reject a no-op policy | At-least-one-permission cross-list check at parse time | — (cannot be disabled) |
| Reject invalid verbs | Each list element validated against its closed enum at parse time | — (cannot be disabled) |
| No secret material | Module carries only permission metadata; never accepts or emits a secret | — |
| No tags | Resource does not support tags; universal tail is timeouts only | — |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 — never a branch. This library is plan-only: a human applies from CI. Nothing here performs a cloud apply.
The offline proof gate exercises everything that does not require Azure credentials:
What only terraform plan (with credentials) exercises: whether the target vault actually uses the access-policy model, whether the object_id already has a policy on the vault, and whether the caller's identity holds the required Key Vault write permission.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-vault-demo/providers/Microsoft.KeyVault/vaults/kv-demo-eus2/objectId/11111111-1111-1111-1111-111111111111"ℹ️ The id embeds the vault and the principal's object ID. It is Terraform's handle for the policy record, not an independently addressable Azure resource.
| Symptom | Cause | Fix |
|---|---|---|
| Vault ... is configured for RBAC authorization; access policies are not supported | The target vault uses the RBAC model. | Recreate the vault with enable_rbac_authorization = false, or use a role-assignment module instead of this one. |
| An access policy already exists for object ID ... | Two policies target the same object_id on one vault. | Model the principal once; consolidate its permissions into a single module call. |
| Every plan shows a diff on the access policy | Inline access_policy blocks on the vault fight this module for ownership. | Remove the inline blocks and let this module own all access policies on the vault. |
| Each secret_permissions value must be one of: ... at plan time | A permission string is misspelled or not in the enum. | Correct the verb to a valid value listed in the input schema. |
| The principal still cannot reach the vault after a clean apply | Any of three causes, none of which raises an error: the vault uses RBAC authorization, so policies are not consulted; an application ID was passed where the principal's object ID belongs (both are GUIDs, so every check passes); or all four permission lists are empty. | Check is_inert_if_the_vault_uses_rbac_authorization, confirm the object ID is the principal's own, and check grants_no_access. |
| Each key_permissions value must be one of: ... at plan time | A permission is misspelled, or lower-cased — the provider's check is case-sensitive. | Use the exact capitalisation from the input schema; get is not Get. |
| Using id as an RBAC scope or a lock target fails | This resource's id is a Terraform-only identifier and maps to no object in Azure. | Use the key_vault_id output instead; see id_is_not_an_azure_resource_id. |
💙 "Infrastructure as Code should be standardized, consistent, and secure."
| Back | FazBrowse Home | New Git URL |