Skip to content

Commit

Permalink
Allow storage account's system assigned identity use CMK (#155)
Browse files Browse the repository at this point in the history
  • Loading branch information
christian-calabrese authored Feb 5, 2025
1 parent 72076b9 commit e7a44b0
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 26 deletions.
5 changes: 5 additions & 0 deletions .changeset/gorgeous-cheetahs-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"azure_storage_account": patch
---

Allowing key vault based customer managed key to be automatically created and used by the storage account's system assigned managed identity
2 changes: 1 addition & 1 deletion infra/github-runner/dev/tfmodules.lock.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"container_app_job_selfhosted_runner.naming_convention": "cebed82c7742c150fbcb4c363670d8078bee27ebd7cb37a37c01cd3803670c64"
"container_app_job_selfhosted_runner.naming_convention": "5b1d21788783dcf33e17a9842f9f7c874c8c5f736c82e70979eb9c8785a74ce4"
}
9 changes: 7 additions & 2 deletions infra/modules/azure_storage_account/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,28 @@

| Name | Type |
|------|------|
| [azurerm_key_vault_access_policy.keys](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_access_policy) | resource |
| [azurerm_key_vault_key.key](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault_key) | resource |
| [azurerm_monitor_metric_alert.storage_account_health_check](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/monitor_metric_alert) | resource |
| [azurerm_private_endpoint.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) | resource |
| [azurerm_role_assignment.keys](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
| [azurerm_security_center_storage_defender.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/security_center_storage_defender) | resource |
| [azurerm_storage_account.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) | resource |
| [azurerm_storage_account_customer_managed_key.kv](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account_customer_managed_key) | resource |
| [azurerm_storage_account_network_rules.network_rules](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account_network_rules) | resource |
| [azurerm_key_vault.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source |
| [azurerm_private_dns_zone.storage_account](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/private_dns_zone) | data source |
| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_access_tier"></a> [access\_tier](#input\_access\_tier) | (Optional) Access tier of the Storage Account. Defaults to Hot. | `string` | `"Hot"` | no |
| <a name="input_action_group_id"></a> [action\_group\_id](#input\_action\_group\_id) | (Optional) Set the Action Group Id to invoke when the Storage Account alert triggers. Required when tier is l. | `string` | `null` | no |
| <a name="input_blob_features"></a> [blob\_features](#input\_blob\_features) | (Optional) Blob features configuration | <pre>object({<br/> restore_policy_days = optional(number, 0)<br/> delete_retention_days = optional(number, 0)<br/> last_access_time = optional(bool, false)<br/> versioning = optional(bool, false)<br/> change_feed = object({<br/> enabled = optional(bool, false)<br/> retention_in_days = optional(number, 0)<br/> })<br/> immutability_policy = object({<br/> enabled = optional(bool, false)<br/> allow_protected_append_writes = optional(bool, false)<br/> period_since_creation_in_days = optional(number, 730)<br/> })<br/> })</pre> | <pre>{<br/> "change_feed": {<br/> "enabled": false,<br/> "retention_in_days": 0<br/> },<br/> "delete_retention_days": 0,<br/> "immutability_policy": {<br/> "enabled": false<br/> },<br/> "last_access_time": false,<br/> "restore_policy_days": 0,<br/> "versioning": false<br/>}</pre> | no |
| <a name="input_blob_features"></a> [blob\_features](#input\_blob\_features) | (Optional) Blob features configuration | <pre>object({<br/> restore_policy_days = optional(number, 0)<br/> delete_retention_days = optional(number, 0)<br/> last_access_time = optional(bool, false)<br/> versioning = optional(bool, false)<br/> change_feed = optional(object({<br/> enabled = optional(bool, false)<br/> retention_in_days = optional(number, 0)<br/> }), { enabled = false })<br/> immutability_policy = optional(object({<br/> enabled = optional(bool, false)<br/> allow_protected_append_writes = optional(bool, false)<br/> period_since_creation_in_days = optional(number, 730)<br/> }), { enabled = false })<br/> })</pre> | <pre>{<br/> "change_feed": {<br/> "enabled": false,<br/> "retention_in_days": 0<br/> },<br/> "delete_retention_days": 0,<br/> "immutability_policy": {<br/> "enabled": false<br/> },<br/> "last_access_time": false,<br/> "restore_policy_days": 0,<br/> "versioning": false<br/>}</pre> | no |
| <a name="input_custom_domain"></a> [custom\_domain](#input\_custom\_domain) | (Optional) Custom domain configuration | <pre>object({<br/> name = optional(string, null)<br/> use_subdomain = optional(bool, false)<br/> })</pre> | <pre>{<br/> "name": null,<br/> "use_subdomain": false<br/>}</pre> | no |
| <a name="input_customer_managed_key"></a> [customer\_managed\_key](#input\_customer\_managed\_key) | (Optional) Customer managed key to use for encryption. Currently type can only be set to 'kv'. | <pre>object({<br/> enabled = optional(bool, false)<br/> type = optional(string, null)<br/> key_name = optional(string)<br/> user_assigned_identity_id = optional(string, null)<br/> key_vault_key_id = optional(string, null)<br/> })</pre> | <pre>{<br/> "enabled": false,<br/> "key_name": null<br/>}</pre> | no |
| <a name="input_customer_managed_key"></a> [customer\_managed\_key](#input\_customer\_managed\_key) | (Optional) Customer managed key to use for encryption. Currently type can only be set to 'kv'. If the key vault is in the same tenant, and key\_name is not set, the key and relevant permissions will be automatically created. | <pre>object({<br/> enabled = optional(bool, false)<br/> type = optional(string, null)<br/> key_name = optional(string, null)<br/> user_assigned_identity_id = optional(string, null)<br/> key_vault_id = optional(string, null)<br/> })</pre> | <pre>{<br/> "enabled": false<br/>}</pre> | no |
| <a name="input_environment"></a> [environment](#input\_environment) | Values which are used to generate resource names and location short names. They are all mandatory except for domain, which should not be used only in the case of a resource used by multiple domains. | <pre>object({<br/> prefix = string<br/> env_short = string<br/> location = string<br/> domain = optional(string)<br/> app_name = string<br/> instance_number = string<br/> })</pre> | n/a | yes |
| <a name="input_force_public_network_access_enabled"></a> [force\_public\_network\_access\_enabled](#input\_force\_public\_network\_access\_enabled) | (Optional) Whether the Storage Account permits public network access or not. Defaults to false. | `bool` | `false` | no |
| <a name="input_network_rules"></a> [network\_rules](#input\_network\_rules) | (Optional) Network rules for the Storage Account. If not provided, defaults will be used. | <pre>object({<br/> default_action = string # Specifies the default action of allow or deny when no other rules match. Valid options are Deny or Allow<br/> bypass = list(string) # Specifies whether traffic is bypassed for Logging/Metrics/AzureServices. Valid options are any combination of Logging, Metrics, AzureServices, or None<br/> ip_rules = list(string) # List of public IP or IP ranges in CIDR Format. Only IPV4 addresses are allowed<br/> virtual_network_subnet_ids = list(string) # A list of resource ids for subnets.<br/> })</pre> | <pre>{<br/> "bypass": [],<br/> "default_action": "Deny",<br/> "ip_rules": [],<br/> "virtual_network_subnet_ids": []<br/>}</pre> | no |
Expand Down
41 changes: 41 additions & 0 deletions infra/modules/azure_storage_account/cmk.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Create a CMK key vault key if a key is not provided
# tfsec:ignore:azure-keyvault-ensure-key-expiry
resource "azurerm_key_vault_key" "key" {
for_each = (local.cmk_flags.kv && var.customer_managed_key.key_name == null ? toset(["kv"]) : toset([]))
name = "${replace("${module.naming_convention.prefix}-st", "-", "")}-cmk-${module.naming_convention.suffix}"
key_vault_id = var.customer_managed_key.key_vault_id
key_type = "RSA"
key_size = 4096
key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"]

depends_on = [
azurerm_key_vault_access_policy.keys
]
}

# Add key vault access policy if it's the supported IAM mean and the key is in the same tenant
resource "azurerm_key_vault_access_policy" "keys" {
for_each = (local.cmk_flags.kv && try(local.cmk_info.kv.same_subscription, false) && try(data.azurerm_key_vault.this["kv"].enable_rbac_authorization, false) == false ? toset(["kv"]) : toset([]))
key_vault_id = var.customer_managed_key.key_vault_id
tenant_id = data.azurerm_subscription.current.tenant_id
object_id = local.cmk_info.kv.principal_id

key_permissions = ["Get", "Create", "List", "Restore", "Recover", "UnwrapKey", "WrapKey", "Purge", "Encrypt", "Decrypt", "Sign", "Verify"]
secret_permissions = ["Get"]
}

# Add role assignment if it's the supported IAM mean and the key is in the same tenant
resource "azurerm_role_assignment" "keys" {
for_each = (local.cmk_flags.kv && try(local.cmk_info.kv.same_subscription, false) && try(data.azurerm_key_vault.this["kv"].enable_rbac_authorization, false) == true ? toset(["kv"]) : toset([]))
scope = var.customer_managed_key.key_vault_id
role_definition_name = "Key Vault Crypto Service Encryption User"
principal_id = local.cmk_info.kv.principal_id
}

resource "azurerm_storage_account_customer_managed_key" "kv" {
for_each = (local.cmk_flags.kv ? toset(["kv"]) : toset([]))
storage_account_id = azurerm_storage_account.this.id
key_vault_id = var.customer_managed_key.key_vault_id
key_name = var.customer_managed_key.key_name == null ? azurerm_key_vault_key.key["kv"].name : var.customer_managed_key.key_name
user_assigned_identity_id = var.customer_managed_key.user_assigned_identity_id
}
12 changes: 11 additions & 1 deletion infra/modules/azure_storage_account/data.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,14 @@ data "azurerm_private_dns_zone" "storage_account" {
for_each = { for subservice, status in var.subservices_enabled : subservice => status if status == true }
name = local.peps[each.key].dns_zone
resource_group_name = var.private_dns_zone_resource_group_name
}
}

data "azurerm_subscription" "current" {
}

data "azurerm_key_vault" "this" {
for_each = (local.cmk_flags.kv ? toset(["kv"]) : toset([]))

name = split("/", var.customer_managed_key.key_vault_id)[8]
resource_group_name = split("/", var.customer_managed_key.key_vault_id)[4]
}
7 changes: 3 additions & 4 deletions infra/modules/azure_storage_account/examples/complete/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,9 @@ module "azure_storage_account" {
private_dns_zone_resource_group_name = "${local.environment.prefix}-${local.environment.env_short}-rg-common"

customer_managed_key = {
enabled = true
type = "kv"
user_assigned_identity_id = azurerm_user_assigned_identity.example.id
key_vault_key_id = "your-key-vault-key-id"
enabled = true
type = "kv"
key_vault_id = "your-kv-id"
}

blob_features = {
Expand Down
14 changes: 14 additions & 0 deletions infra/modules/azure_storage_account/locals.tf
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,18 @@ locals {
dns_zone = "privatelink.table.core.windows.net"
}
}

cmk_flags = {
kv = (var.customer_managed_key.enabled && var.customer_managed_key.type == "kv")
}

cmk_info = {
kv = local.cmk_flags.kv ? {
key_vault_name = try(split("/", var.customer_managed_key.key_vault_id)[8], "")
resource_group_name = try(split("/", var.customer_managed_key.key_vault_id)[4], "")
subscription = try(split("/", var.customer_managed_key.key_vault_id)[2], "")
same_subscription = try((split("/", var.customer_managed_key.key_vault_id)[2] == data.azurerm_subscription.current.subscription_id), false)
principal_id = try(coalesce(var.customer_managed_key.user_assigned_identity_id, azurerm_storage_account.this.identity[0].principal_id), "")
} : {}
}
}
8 changes: 0 additions & 8 deletions infra/modules/azure_storage_account/storage_account.tf
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,3 @@ resource "azurerm_security_center_storage_defender" "this" {
count = local.tier_features.advanced_threat_protection ? 1 : 0
storage_account_id = azurerm_storage_account.this.id
}

resource "azurerm_storage_account_customer_managed_key" "kv" {
for_each = (var.customer_managed_key.enabled && var.customer_managed_key.type == "kv" ? { type = var.customer_managed_key.type } : {})
storage_account_id = azurerm_storage_account.this.id
key_vault_id = var.customer_managed_key.key_vault_key_id
key_name = var.customer_managed_key.key_name
user_assigned_identity_id = var.customer_managed_key.user_assigned_identity_id
}
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,9 @@ run "storage_account_is_correct_plan" {
tier = "l"

customer_managed_key = {
enabled = false
enabled = true
type = "kv"
key_vault_id = "/subscriptions/d7de83e0-0571-40ad-b63a-64c942385eae/resourceGroups/dx-d-itn-common-rg-01/providers/Microsoft.KeyVault/vaults/dx-d-itn-common-kv-01"
}

subnet_pep_id = run.setup_tests.pep_id
Expand Down Expand Up @@ -135,4 +137,9 @@ run "storage_account_is_correct_plan" {
condition = azurerm_storage_account.this.access_tier == "Hot"
error_message = "The Storage Account must have the access tier set to Hot"
}

assert {
condition = azurerm_storage_account_customer_managed_key.kv["kv"].user_assigned_identity_id == null
error_message = "The storage account's managed identity should be used instead of user assigned identity"
}
}
16 changes: 8 additions & 8 deletions infra/modules/azure_storage_account/variables.tf
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ variable "customer_managed_key" {
type = object({
enabled = optional(bool, false)
type = optional(string, null)
key_name = optional(string)
key_name = optional(string, null)
user_assigned_identity_id = optional(string, null)
key_vault_key_id = optional(string, null)
key_vault_id = optional(string, null)
})
description = "(Optional) Customer managed key to use for encryption. Currently type can only be set to 'kv'."
default = { enabled = false, key_name = null }
description = "(Optional) Customer managed key to use for encryption. Currently type can only be set to 'kv'. If the key vault is in the same tenant, and key_name is not set, the key and relevant permissions will be automatically created."
default = { enabled = false }
}

variable "force_public_network_access_enabled" {
Expand Down Expand Up @@ -78,15 +78,15 @@ variable "blob_features" {
delete_retention_days = optional(number, 0)
last_access_time = optional(bool, false)
versioning = optional(bool, false)
change_feed = object({
change_feed = optional(object({
enabled = optional(bool, false)
retention_in_days = optional(number, 0)
})
immutability_policy = object({
}), { enabled = false })
immutability_policy = optional(object({
enabled = optional(bool, false)
allow_protected_append_writes = optional(bool, false)
period_since_creation_in_days = optional(number, 730)
})
}), { enabled = false })
})
description = "(Optional) Blob features configuration"
default = {
Expand Down
2 changes: 1 addition & 1 deletion infra/resources/dev/tfmodules.lock.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"core.naming_convention": "cebed82c7742c150fbcb4c363670d8078bee27ebd7cb37a37c01cd3803670c64"
"core.naming_convention": "5b1d21788783dcf33e17a9842f9f7c874c8c5f736c82e70979eb9c8785a74ce4"
}

0 comments on commit e7a44b0

Please sign in to comment.