diff --git a/modules/AGENTS.md b/modules/AGENTS.md index 8f51125c..a2c24efe 100644 --- a/modules/AGENTS.md +++ b/modules/AGENTS.md @@ -210,7 +210,6 @@ name: Service Name Building Block supportedPlatforms: - aws|azure|gcp|btp description: Brief description for catalog explaining what this building block provides -category: "cost-management|security|networking|storage" --- ``` @@ -221,7 +220,6 @@ name: AWS S3 Bucket supportedPlatforms: - aws description: Provides an AWS S3 bucket for object storage with access controls, lifecycle policies, and encryption. -category: storage --- ``` diff --git a/modules/azure/aks/backplane/main.tf b/modules/azure/aks/backplane/main.tf index 32e57dab..4378012f 100644 --- a/modules/azure/aks/backplane/main.tf +++ b/modules/azure/aks/backplane/main.tf @@ -67,6 +67,9 @@ resource "azurerm_role_definition" "buildingblock_deploy" { permissions { actions = [ + # Register resource providers in Azure Resource Manager + "*/register/action", + "Microsoft.ContainerService/managedClusters/read", "Microsoft.ContainerService/managedClusters/write", "Microsoft.ContainerService/managedClusters/delete", diff --git a/modules/azure/aks/buildingblock/README.md b/modules/azure/aks/buildingblock/README.md index 4788a76e..d499ca47 100644 --- a/modules/azure/aks/buildingblock/README.md +++ b/modules/azure/aks/buildingblock/README.md @@ -4,7 +4,6 @@ supportedPlatforms: - azure description: | Provision a production-grade Azure Kubernetes Service (AKS) cluster with Azure AD, OIDC, Workload Identity, Log Analytics and custom VNet using Terraform. -category: compute --- # AKS Building Block diff --git a/modules/azure/azure-bastion/buildingblock/README.md b/modules/azure/azure-bastion/buildingblock/README.md index e390578f..c9a3a478 100644 --- a/modules/azure/azure-bastion/buildingblock/README.md +++ b/modules/azure/azure-bastion/buildingblock/README.md @@ -3,7 +3,6 @@ name: Azure Bastion Host supportedPlatforms: - azure description: Provides secure RDP and SSH connectivity to virtual machines in Azure virtual networks without exposing them to the public internet, with comprehensive monitoring and alerting. -category: networking --- # Azure Bastion Building Block with Comprehensive Observability diff --git a/modules/azure/container-registry/backplane/main.tf b/modules/azure/container-registry/backplane/main.tf index f3b6954b..316300f6 100644 --- a/modules/azure/container-registry/backplane/main.tf +++ b/modules/azure/container-registry/backplane/main.tf @@ -67,6 +67,9 @@ resource "azurerm_role_definition" "buildingblock_deploy" { permissions { actions = [ + # Register resource providers in Azure Resource Manager + "*/register/action", + # Container Registry "Microsoft.ContainerRegistry/registries/read", "Microsoft.ContainerRegistry/registries/write", diff --git a/modules/azure/container-registry/buildingblock/README.md b/modules/azure/container-registry/buildingblock/README.md index 897a29b8..d98b314f 100644 --- a/modules/azure/container-registry/buildingblock/README.md +++ b/modules/azure/container-registry/buildingblock/README.md @@ -4,7 +4,6 @@ supportedPlatforms: - azure description: | Provides a production-grade Azure Container Registry for storing and managing Docker container images and OCI artifacts with private networking support. -category: container-registry --- # Azure Container Registry Building Block diff --git a/modules/azure/key-vault/backplane/README.md b/modules/azure/key-vault/backplane/README.md index cc1c83e3..6f24ff10 100644 --- a/modules/azure/key-vault/backplane/README.md +++ b/modules/azure/key-vault/backplane/README.md @@ -12,9 +12,9 @@ across all subscriptions underneath a management group (typically the top-level | Name | Version | |------|---------| -| [terraform](#requirement\_terraform) | >= 1.0 | -| [azuread](#requirement\_azuread) | 3.1.0 | -| [azurerm](#requirement\_azurerm) | 3.116.0 | +| [terraform](#requirement\_terraform) | >= 1.3.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | +| [azurerm](#requirement\_azurerm) | ~> 4.36.0 | ## Modules @@ -24,27 +24,67 @@ No modules. | Name | Type | |------|------| -| [azuread_directory_role.directory_readers](https://registry.terraform.io/providers/hashicorp/azuread/3.1.0/docs/resources/directory_role) | resource | -| [azuread_directory_role_assignment.directory_readers](https://registry.terraform.io/providers/hashicorp/azuread/3.1.0/docs/resources/directory_role_assignment) | resource | -| [azurerm_role_assignment.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/3.116.0/docs/resources/role_assignment) | resource | -| [azurerm_role_definition.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/3.116.0/docs/resources/role_definition) | resource | +| [azuread_application.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_application.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | +| [azuread_application_federated_identity_credential.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | +| [azuread_application_federated_identity_credential.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | +| [azuread_application_password.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_password) | resource | +| [azuread_application_password.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_password) | resource | +| [azuread_directory_role.directory_readers](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/directory_role) | resource | +| [azuread_directory_role_assignment.directory_readers_created](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/directory_role_assignment) | resource | +| [azuread_directory_role_assignment.directory_readers_existing](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/directory_role_assignment) | resource | +| [azuread_service_principal.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azuread_service_principal.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | +| [azurerm_role_assignment.created_principal](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.created_principal_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.created_principal_hub_to_landingzone](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.created_principal_landingzone_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals_hub_to_landingzone](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_assignment.existing_principals_landingzone_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource | +| [azurerm_role_definition.buildingblock_deploy](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_role_definition.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_role_definition.buildingblock_hub_to_landingzone](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_role_definition.buildingblock_landingzone_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource | +| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/subscription) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [create\_hub\_service\_principal\_name](#input\_create\_hub\_service\_principal\_name) | name of a separate service principal to create for hub VNet peering (least privilege) | `string` | `null` | no | +| [create\_service\_principal\_name](#input\_create\_service\_principal\_name) | name of a service principal to create and grant permissions to deploy the building block | `string` | `null` | no | +| [existing\_hub\_principal\_ids](#input\_existing\_hub\_principal\_ids) | set of existing principal ids that will be granted permissions to peer with the hub VNet | `set(string)` | `[]` | no | +| [existing\_principal\_ids](#input\_existing\_principal\_ids) | set of existing principal ids that will be granted permissions to deploy the building block | `set(string)` | `[]` | no | +| [hub\_scope](#input\_hub\_scope) | Scope for hub VNet peering permissions (management group or subscription). Typically a hub subscription, but can be a management group containing hub resources. | `string` | n/a | yes | +| [hub\_workload\_identity\_federation](#input\_hub\_workload\_identity\_federation) | Configuration for workload identity federation for hub service principal. If not provided, an application password will be created instead. |
object({
issuer = string
subject = string
})
| `null` | no | | [name](#input\_name) | name of the building block, used for naming resources | `string` | `"key-vault"` | no | -| [principal\_ids](#input\_principal\_ids) | set of principal ids that will be granted permissions to deploy the building block | `set(string)` | n/a | yes | -| [scope](#input\_scope) | Scope where the building block should be deployable, typically the parent of all Landing Zones. | `string` | n/a | yes | +| [scope](#input\_scope) | Scope where the building block should be deployable (management group or subscription), typically the parent of all Landing Zones. | `string` | n/a | yes | +| [workload\_identity\_federation](#input\_workload\_identity\_federation) | Configuration for workload identity federation. If not provided, an application password will be created instead. |
object({
issuer = string
subject = string
})
| `null` | no | ## Outputs | Name | Description | |------|-------------| +| [application\_password](#output\_application\_password) | Information about the created application password (excludes the actual password value for security). | +| [created\_application](#output\_created\_application) | Information about the created Azure AD application. | +| [created\_hub\_application](#output\_created\_hub\_application) | Information about the created hub Azure AD application. | +| [created\_hub\_service\_principal](#output\_created\_hub\_service\_principal) | Information about the created hub service principal. | +| [created\_service\_principal](#output\_created\_service\_principal) | Information about the created service principal. | | [documentation\_md](#output\_documentation\_md) | Markdown documentation with information about the Key Vault Building Block building block backplane | -| [role\_assignment\_ids](#output\_role\_assignment\_ids) | The IDs of the role assignments for the service principals. | -| [role\_assignment\_principal\_ids](#output\_role\_assignment\_principal\_ids) | The principal IDs of the service principals that have been assigned the role. | -| [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the building block to subscriptions. | -| [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the building block to subscriptions. | +| [hub\_application\_password](#output\_hub\_application\_password) | Information about the created hub application password (excludes the actual password value for security). | +| [hub\_role\_assignment\_ids](#output\_hub\_role\_assignment\_ids) | The IDs of the hub role assignments for all service principals. | +| [hub\_role\_assignment\_principal\_ids](#output\_hub\_role\_assignment\_principal\_ids) | The principal IDs of all service principals that have been assigned the hub role. | +| [hub\_role\_definition\_id](#output\_hub\_role\_definition\_id) | The ID of the role definition that enables deployment of the building block to the hub. | +| [hub\_role\_definition\_name](#output\_hub\_role\_definition\_name) | The name of the role definition that enables deployment of the building block to the hub. | +| [hub\_scope](#output\_hub\_scope) | The scope (management group or subscription) where VNet peering role is applied. | +| [hub\_workload\_identity\_federation](#output\_hub\_workload\_identity\_federation) | Information about the created hub workload identity federation credential. | +| [provider\_tf](#output\_provider\_tf) | Ready-to-use provider.tf configuration for buildingblock deployment | +| [role\_assignment\_ids](#output\_role\_assignment\_ids) | The IDs of the role assignments for all service principals. | +| [role\_assignment\_principal\_ids](#output\_role\_assignment\_principal\_ids) | The principal IDs of all service principals that have been assigned the role. | +| [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the building block. | +| [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the building block. | | [scope](#output\_scope) | The scope where the role definition and role assignments are applied. | +| [workload\_identity\_federation](#output\_workload\_identity\_federation) | Information about the created workload identity federation credential. | diff --git a/modules/azure/key-vault/backplane/main.tf b/modules/azure/key-vault/backplane/main.tf index f29f43b4..d3a86695 100644 --- a/modules/azure/key-vault/backplane/main.tf +++ b/modules/azure/key-vault/backplane/main.tf @@ -1,34 +1,258 @@ +data "azurerm_subscription" "current" {} + +resource "azuread_application" "buildingblock_deploy" { + count = var.create_service_principal_name != null ? 1 : 0 + + display_name = "${var.name}-${var.create_service_principal_name}" +} + +resource "azuread_service_principal" "buildingblock_deploy" { + count = var.create_service_principal_name != null ? 1 : 0 + + client_id = azuread_application.buildingblock_deploy[0].client_id + app_role_assignment_required = false +} + +resource "azuread_application_federated_identity_credential" "buildingblock_deploy" { + count = var.create_service_principal_name != null && var.workload_identity_federation != null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy[0].id + display_name = var.create_service_principal_name + audiences = ["api://AzureADTokenExchange"] + issuer = var.workload_identity_federation.issuer + subject = var.workload_identity_federation.subject +} + +resource "azuread_application_password" "buildingblock_deploy" { + count = var.create_service_principal_name != null && var.workload_identity_federation == null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy[0].id + display_name = "${var.create_service_principal_name}-password" +} + +resource "azuread_application" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + display_name = "${var.name}-${var.create_hub_service_principal_name}" +} + +resource "azuread_service_principal" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + client_id = azuread_application.buildingblock_deploy_hub[0].client_id + app_role_assignment_required = false +} + +resource "azuread_application_federated_identity_credential" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation != null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy_hub[0].id + display_name = var.create_hub_service_principal_name + audiences = ["api://AzureADTokenExchange"] + issuer = var.hub_workload_identity_federation.issuer + subject = var.hub_workload_identity_federation.subject +} + +resource "azuread_application_password" "buildingblock_deploy_hub" { + count = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation == null ? 1 : 0 + + application_id = azuread_application.buildingblock_deploy_hub[0].id + display_name = "${var.create_hub_service_principal_name}-password" +} + resource "azurerm_role_definition" "buildingblock_deploy" { name = "${var.name}-deploy" - description = "Enables deployment of the ${var.name} building block to subscriptions" scope = var.scope + description = "Enables deployment of the ${var.name} building block to subscriptions" + permissions { actions = [ - "Microsoft.KeyVault/vaults/write", + # Register resource providers in Azure Resource Manager + "*/register/action", + + # Key Vault "Microsoft.KeyVault/vaults/read", + "Microsoft.KeyVault/vaults/write", "Microsoft.KeyVault/vaults/delete", "Microsoft.KeyVault/locations/deletedVaults/read", + "Microsoft.KeyVault/locations/deletedVaults/purge/action", + "Microsoft.KeyVault/vaults/PrivateEndpointConnectionsApproval/action", + + # Private Endpoints + "Microsoft.Network/privateEndpoints/read", + "Microsoft.Network/privateEndpoints/write", + "Microsoft.Network/privateEndpoints/delete", + "Microsoft.Network/privateEndpoints/privateDnsZoneGroups/read", + "Microsoft.Network/privateEndpoints/privateDnsZoneGroups/write", + "Microsoft.Network/privateEndpoints/privateDnsZoneGroups/delete", + + # Private DNS Zones + "Microsoft.Network/privateDnsZones/read", + "Microsoft.Network/privateDnsZones/write", + "Microsoft.Network/privateDnsZones/delete", + "Microsoft.Network/privateDnsZones/join/action", + "Microsoft.Network/privateDnsZones/virtualNetworkLinks/read", + "Microsoft.Network/privateDnsZones/virtualNetworkLinks/write", + "Microsoft.Network/privateDnsZones/virtualNetworkLinks/delete", + "Microsoft.Network/privateDnsZones/A/read", + "Microsoft.Network/privateDnsZones/A/write", + "Microsoft.Network/privateDnsZones/A/delete", + "Microsoft.Network/privateDnsZones/SOA/read", + + # Virtual Networks + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/write", + "Microsoft.Network/virtualNetworks/delete", + "Microsoft.Network/virtualNetworks/subnets/read", + "Microsoft.Network/virtualNetworks/subnets/write", + "Microsoft.Network/virtualNetworks/subnets/delete", + "Microsoft.Network/virtualNetworks/subnets/join/action", + "Microsoft.Network/virtualNetworks/join/action", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete", + "Microsoft.Network/virtualNetworks/peer/action", + + # Resource Groups + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Resources/subscriptions/resourceGroups/write", + "Microsoft.Resources/subscriptions/resourceGroups/delete", + + # Deployments + "Microsoft.Resources/deployments/read", + "Microsoft.Resources/deployments/write", + "Microsoft.Resources/deployments/delete", + + # Role Assignments + "Microsoft.Authorization/roleAssignments/read", "Microsoft.Authorization/roleAssignments/write", "Microsoft.Authorization/roleAssignments/delete", - "Microsoft.Authorization/roleAssignments/read" ] } } -resource "azurerm_role_assignment" "buildingblock_deploy" { - for_each = var.principal_ids +resource "azurerm_role_assignment" "existing_principals" { + for_each = var.existing_principal_ids role_definition_id = azurerm_role_definition.buildingblock_deploy.role_definition_resource_id principal_id = each.value scope = var.scope } +resource "azurerm_role_assignment" "created_principal" { + count = var.create_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_deploy.role_definition_resource_id + principal_id = azuread_service_principal.buildingblock_deploy[0].object_id + scope = var.scope +} + +resource "azurerm_role_definition" "buildingblock_deploy_hub" { + name = "${var.name}-deploy-hub" + description = "Enables deployment of the ${var.name} building block to the hub (for private endpoint peering)" + scope = var.hub_scope + + permissions { + actions = [ + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write", + "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete", + "Microsoft.Network/virtualNetworks/peer/action", + ] + } +} + +resource "azurerm_role_definition" "buildingblock_hub_to_landingzone" { + name = "${var.name}-hub-to-landingzone" + description = "Allows hub service principal to peer back to landing zone vnets" + scope = var.scope + + permissions { + actions = [ + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/peer/action", + ] + } +} + +resource "azurerm_role_definition" "buildingblock_landingzone_to_hub" { + name = "${var.name}-landingzone-to-hub" + description = "Allows landing zone service principal to peer to hub vnets" + scope = var.hub_scope + + permissions { + actions = [ + "Microsoft.Resources/subscriptions/resourceGroups/read", + "Microsoft.Network/virtualNetworks/read", + "Microsoft.Network/virtualNetworks/peer/action", + ] + } +} + +resource "azurerm_role_assignment" "existing_principals_hub" { + for_each = var.existing_hub_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_deploy_hub.role_definition_resource_id + description = azurerm_role_definition.buildingblock_deploy_hub.description + principal_id = each.value + scope = var.hub_scope +} + +resource "azurerm_role_assignment" "created_principal_hub" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_deploy_hub.role_definition_resource_id + description = azurerm_role_definition.buildingblock_deploy_hub.description + principal_id = azuread_service_principal.buildingblock_deploy_hub[0].object_id + scope = var.hub_scope +} + +resource "azurerm_role_assignment" "existing_principals_hub_to_landingzone" { + for_each = var.existing_hub_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_hub_to_landingzone.role_definition_resource_id + principal_id = each.value + scope = var.scope +} + +resource "azurerm_role_assignment" "created_principal_hub_to_landingzone" { + count = var.create_hub_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_hub_to_landingzone.role_definition_resource_id + principal_id = azuread_service_principal.buildingblock_deploy_hub[0].object_id + scope = var.scope +} + +resource "azurerm_role_assignment" "existing_principals_landingzone_to_hub" { + for_each = var.existing_principal_ids + + role_definition_id = azurerm_role_definition.buildingblock_landingzone_to_hub.role_definition_resource_id + principal_id = each.value + scope = var.hub_scope +} + +resource "azurerm_role_assignment" "created_principal_landingzone_to_hub" { + count = var.create_service_principal_name != null ? 1 : 0 + + role_definition_id = azurerm_role_definition.buildingblock_landingzone_to_hub.role_definition_resource_id + principal_id = azuread_service_principal.buildingblock_deploy[0].object_id + scope = var.hub_scope +} + resource "azuread_directory_role" "directory_readers" { display_name = "Directory Readers" } -resource "azuread_directory_role_assignment" "directory_readers" { - for_each = var.principal_ids +resource "azuread_directory_role_assignment" "directory_readers_existing" { + for_each = var.existing_principal_ids role_id = azuread_directory_role.directory_readers.template_id principal_object_id = each.value } + +resource "azuread_directory_role_assignment" "directory_readers_created" { + count = var.create_service_principal_name != null ? 1 : 0 + role_id = azuread_directory_role.directory_readers.template_id + principal_object_id = azuread_service_principal.buildingblock_deploy[0].object_id +} diff --git a/modules/azure/key-vault/backplane/outputs.tf b/modules/azure/key-vault/backplane/outputs.tf index 983bf40f..2da1078d 100644 --- a/modules/azure/key-vault/backplane/outputs.tf +++ b/modules/azure/key-vault/backplane/outputs.tf @@ -1,21 +1,66 @@ output "role_definition_id" { value = azurerm_role_definition.buildingblock_deploy.id - description = "The ID of the role definition that enables deployment of the building block to subscriptions." + description = "The ID of the role definition that enables deployment of the building block." } output "role_definition_name" { value = azurerm_role_definition.buildingblock_deploy.name - description = "The name of the role definition that enables deployment of the building block to subscriptions." + description = "The name of the role definition that enables deployment of the building block." } output "role_assignment_ids" { - value = [for id in azurerm_role_assignment.buildingblock_deploy : id.id] - description = "The IDs of the role assignments for the service principals." + value = concat( + [for id in azurerm_role_assignment.existing_principals : id.id], + var.create_service_principal_name != null ? [azurerm_role_assignment.created_principal[0].id] : [] + ) + description = "The IDs of the role assignments for all service principals." } output "role_assignment_principal_ids" { - value = [for id in azurerm_role_assignment.buildingblock_deploy : id.principal_id] - description = "The principal IDs of the service principals that have been assigned the role." + value = concat( + [for id in azurerm_role_assignment.existing_principals : id.principal_id], + var.create_service_principal_name != null ? [azurerm_role_assignment.created_principal[0].principal_id] : [] + ) + description = "The principal IDs of all service principals that have been assigned the role." +} + +output "created_service_principal" { + value = var.create_service_principal_name != null ? { + object_id = azuread_service_principal.buildingblock_deploy[0].object_id + client_id = azuread_service_principal.buildingblock_deploy[0].client_id + display_name = azuread_service_principal.buildingblock_deploy[0].display_name + name = var.create_service_principal_name + } : null + description = "Information about the created service principal." +} + +output "created_application" { + value = var.create_service_principal_name != null ? { + object_id = azuread_application.buildingblock_deploy[0].object_id + client_id = azuread_application.buildingblock_deploy[0].client_id + display_name = azuread_application.buildingblock_deploy[0].display_name + } : null + description = "Information about the created Azure AD application." +} + +output "workload_identity_federation" { + value = var.create_service_principal_name != null && var.workload_identity_federation != null ? { + credential_id = azuread_application_federated_identity_credential.buildingblock_deploy[0].credential_id + display_name = azuread_application_federated_identity_credential.buildingblock_deploy[0].display_name + issuer = azuread_application_federated_identity_credential.buildingblock_deploy[0].issuer + subject = azuread_application_federated_identity_credential.buildingblock_deploy[0].subject + audiences = azuread_application_federated_identity_credential.buildingblock_deploy[0].audiences + } : null + description = "Information about the created workload identity federation credential." +} + +output "application_password" { + value = var.create_service_principal_name != null && var.workload_identity_federation == null ? { + key_id = azuread_application_password.buildingblock_deploy[0].key_id + display_name = azuread_application_password.buildingblock_deploy[0].display_name + } : null + description = "Information about the created application password (excludes the actual password value for security)." + sensitive = true } output "scope" { @@ -23,3 +68,129 @@ output "scope" { description = "The scope where the role definition and role assignments are applied." } +output "hub_scope" { + value = var.hub_scope + description = "The scope (management group or subscription) where VNet peering role is applied." +} + +output "hub_role_definition_id" { + value = azurerm_role_definition.buildingblock_deploy_hub.id + description = "The ID of the role definition that enables deployment of the building block to the hub." +} + +output "hub_role_definition_name" { + value = azurerm_role_definition.buildingblock_deploy_hub.name + description = "The name of the role definition that enables deployment of the building block to the hub." +} + +output "hub_role_assignment_ids" { + value = concat( + [for id in azurerm_role_assignment.existing_principals_hub : id.id], + var.create_hub_service_principal_name != null ? [azurerm_role_assignment.created_principal_hub[0].id] : [] + ) + description = "The IDs of the hub role assignments for all service principals." +} + +output "hub_role_assignment_principal_ids" { + value = concat( + [for id in azurerm_role_assignment.existing_principals_hub : id.principal_id], + var.create_hub_service_principal_name != null ? [azurerm_role_assignment.created_principal_hub[0].principal_id] : [] + ) + description = "The principal IDs of all service principals that have been assigned the hub role." +} + +output "created_hub_service_principal" { + value = var.create_hub_service_principal_name != null ? { + object_id = azuread_service_principal.buildingblock_deploy_hub[0].object_id + client_id = azuread_service_principal.buildingblock_deploy_hub[0].client_id + display_name = azuread_service_principal.buildingblock_deploy_hub[0].display_name + name = var.create_hub_service_principal_name + } : null + description = "Information about the created hub service principal." +} + +output "created_hub_application" { + value = var.create_hub_service_principal_name != null ? { + object_id = azuread_application.buildingblock_deploy_hub[0].object_id + client_id = azuread_application.buildingblock_deploy_hub[0].client_id + display_name = azuread_application.buildingblock_deploy_hub[0].display_name + } : null + description = "Information about the created hub Azure AD application." +} + +output "hub_workload_identity_federation" { + value = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation != null ? { + credential_id = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].credential_id + display_name = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].display_name + issuer = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].issuer + subject = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].subject + audiences = azuread_application_federated_identity_credential.buildingblock_deploy_hub[0].audiences + } : null + description = "Information about the created hub workload identity federation credential." +} + +output "hub_application_password" { + value = var.create_hub_service_principal_name != null && var.hub_workload_identity_federation == null ? { + key_id = azuread_application_password.buildingblock_deploy_hub[0].key_id + display_name = azuread_application_password.buildingblock_deploy_hub[0].display_name + } : null + description = "Information about the created hub application password (excludes the actual password value for security)." + sensitive = true +} + +output "provider_tf" { + value = var.create_service_principal_name != null && var.create_hub_service_principal_name != null ? ( + var.workload_identity_federation == null && var.hub_workload_identity_federation == null ? <<-EOT + provider "azurerm" { + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy[0].client_id}" + client_secret = "${azuread_application_password.buildingblock_deploy[0].value}" + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + + provider "azurerm" { + alias = "hub" + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy_hub[0].client_id}" + client_secret = "${azuread_application_password.buildingblock_deploy_hub[0].value}" + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + EOT + : <<-EOT + terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.36.0" + } + } + } + + provider "azurerm" { + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy[0].client_id}" + use_oidc = true + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + + provider "azurerm" { + alias = "hub" + features {} + + client_id = "${azuread_service_principal.buildingblock_deploy_hub[0].client_id}" + use_oidc = true + subscription_id = "" + tenant_id = "${data.azurerm_subscription.current.tenant_id}" + } + EOT + ) : null + description = "Ready-to-use provider.tf configuration for buildingblock deployment" + sensitive = true +} + diff --git a/modules/azure/key-vault/backplane/provider.tf b/modules/azure/key-vault/backplane/provider.tf new file mode 100644 index 00000000..ab91b248 --- /dev/null +++ b/modules/azure/key-vault/backplane/provider.tf @@ -0,0 +1,3 @@ +provider "azurerm" { + features {} +} diff --git a/modules/azure/key-vault/backplane/variables.tf b/modules/azure/key-vault/backplane/variables.tf index d54c8481..81b81e60 100644 --- a/modules/azure/key-vault/backplane/variables.tf +++ b/modules/azure/key-vault/backplane/variables.tf @@ -12,11 +12,63 @@ variable "name" { variable "scope" { type = string nullable = false - description = "Scope where the building block should be deployable, typically the parent of all Landing Zones." + description = "Scope where the building block should be deployable (management group or subscription), typically the parent of all Landing Zones." } -variable "principal_ids" { - type = set(string) +variable "hub_scope" { + type = string nullable = false - description = "set of principal ids that will be granted permissions to deploy the building block" + description = "Scope for hub VNet peering permissions (management group or subscription). Typically a hub subscription, but can be a management group containing hub resources." +} + +variable "existing_principal_ids" { + type = set(string) + default = [] + description = "set of existing principal ids that will be granted permissions to deploy the building block" +} + +variable "existing_hub_principal_ids" { + type = set(string) + default = [] + description = "set of existing principal ids that will be granted permissions to peer with the hub VNet" +} + +variable "create_service_principal_name" { + type = string + default = null + description = "name of a service principal to create and grant permissions to deploy the building block" + + validation { + condition = var.create_service_principal_name == null ? true : can(regex("^[-a-zA-Z0-9_]+$", var.create_service_principal_name)) + error_message = "Service principal name can only contain alphanumeric characters, hyphens, and underscores" + } +} + +variable "create_hub_service_principal_name" { + type = string + default = null + description = "name of a separate service principal to create for hub VNet peering (least privilege)" + + validation { + condition = var.create_hub_service_principal_name == null ? true : can(regex("^[-a-zA-Z0-9_]+$", var.create_hub_service_principal_name)) + error_message = "Service principal name can only contain alphanumeric characters, hyphens, and underscores" + } +} + +variable "workload_identity_federation" { + type = object({ + issuer = string + subject = string + }) + default = null + description = "Configuration for workload identity federation. If not provided, an application password will be created instead." +} + +variable "hub_workload_identity_federation" { + type = object({ + issuer = string + subject = string + }) + default = null + description = "Configuration for workload identity federation for hub service principal. If not provided, an application password will be created instead." } diff --git a/modules/azure/key-vault/backplane/versions.tf b/modules/azure/key-vault/backplane/versions.tf index ecfa871a..7e487f6d 100644 --- a/modules/azure/key-vault/backplane/versions.tf +++ b/modules/azure/key-vault/backplane/versions.tf @@ -1,15 +1,14 @@ terraform { - required_version = ">= 1.0" + required_version = ">= 1.3.0" required_providers { azurerm = { source = "hashicorp/azurerm" - version = "3.116.0" + version = "~> 4.36.0" } azuread = { source = "hashicorp/azuread" - version = "3.1.0" + version = "~> 3.6.0" } } } - diff --git a/modules/azure/key-vault/buildingblock/APP_TEAM_README.md b/modules/azure/key-vault/buildingblock/APP_TEAM_README.md index 58807204..d5ddeeea 100644 --- a/modules/azure/key-vault/buildingblock/APP_TEAM_README.md +++ b/modules/azure/key-vault/buildingblock/APP_TEAM_README.md @@ -1,29 +1,322 @@ # Azure Key Vault ## Description -This building block provides an Azure Key Vault for secure storage and management of secrets, keys, and certificates. It helps application teams protect sensitive data, manage access control, and integrate securely with their applications. +This building block provides a production-grade Azure Key Vault for secure storage and management of secrets, keys, and certificates. It delivers a fully managed and secure key vault with support for both public and private deployments, optional hub connectivity, and seamless integration with Azure services. ## Usage Motivation -This building block is for application teams that need a secure and scalable solution to store and manage secrets, encryption keys, and certificates. Azure Key Vault ensures compliance with security best practices and helps prevent accidental exposure of sensitive credentials. +This building block is for application teams that need a secure and scalable solution to store and manage secrets, encryption keys, and certificates. Azure Key Vault ensures compliance with security best practices and helps prevent accidental exposure of sensitive credentials, while eliminating the complexity of managing key storage infrastructure. -## Usage Examples -- A development team stores API keys and database credentials in Azure Key Vault instead of hardcoding them in application code. -- A DevOps team manages TLS certificates in Key Vault and integrates them with Azure Application Gateway for secure HTTPS communication. +## πŸš€ Usage Examples +- A development team stores API keys and database credentials in Azure Key Vault instead of hardcoding them in application code +- A DevOps team manages TLS certificates in Key Vault and integrates them with Azure Application Gateway for secure HTTPS communication +- A security team implements centralized secret management for microservices with automatic rotation policies +- An application uses managed identities to securely retrieve database connection strings from Key Vault -## Shared Responsibility +## πŸ”„ Shared Responsibility -| Responsibility | Platform Team | Application Team | -|------------------------|--------------|----------------| +| Responsibility | Platform Team | Application Team | +|----------------|--------------|------------------| | Provisioning and configuring Key Vault | βœ… | ❌ | -| Enforcing security policies (e.g., access control, logging) | βœ… | ❌ | -| Managing access policies and permissions | ❌ | βœ… | +| Managing network configuration and private endpoints | βœ… | ❌ | +| Setting up hub network peering (for private Key Vault) | βœ… | ❌ | +| Enforcing security policies (e.g., RBAC, logging) | βœ… | ❌ | | Storing and retrieving secrets, keys, and certificates | ❌ | βœ… | | Rotating and managing secrets lifecycle | ❌ | βœ… | +| Implementing secret versioning strategies | ❌ | βœ… | | Integrating Key Vault with applications and services | ❌ | βœ… | -## Recommendations for Secure and Efficient Key Vault Usage -- **Use Azure RBAC or Key Vault access policies**: Grant least-privilege access to teams and services. -- **Enable logging and monitoring**: Use Azure Monitor and diagnostic logs to track access and modifications. -- **Automate secret rotation**: Regularly update secrets to enhance security and avoid expiration risks. -- **Restrict network access**: Use private endpoints or service endpoints to limit exposure. -- **Use managed identities**: Integrate with Azure services securely without exposing credentials. +## πŸ’‘ Best Practices for Secure and Efficient Key Vault Usage + +### Security +- **Use Azure RBAC**: Always use Azure RBAC instead of access policies for better governance and auditing +- **Use managed identities**: Integrate with Azure services securely without exposing credentials +- **Enable soft delete and purge protection**: Prevent accidental deletion of secrets and keys +- **Use private endpoints**: Deploy private Key Vault for production workloads to prevent internet exposure +- **Implement network rules**: Restrict access using firewall rules when public access is required +- **Monitor access patterns**: Track who accesses which secrets and when + +### Secret Management +- **Use semantic versioning**: Version secrets for better tracking and rollback capabilities +- **Implement rotation policies**: Configure automatic rotation for secrets that support it +- **Set expiration dates**: Configure expiration dates for secrets to enforce rotation +- **Use separate Key Vaults**: Separate development, staging, and production secrets +- **Avoid storing secrets in code**: Never commit secrets to source control +- **Use Key Vault references**: Use Key Vault references in App Service and Functions + +### Performance & Reliability +- **Cache secrets appropriately**: Cache retrieved secrets in your application to reduce calls +- **Implement retry logic**: Handle transient failures with exponential backoff +- **Monitor throttling**: Track API call limits and optimize secret retrieval patterns +- **Use regional endpoints**: Deploy Key Vault close to your application for lower latency + +### Cost Optimization +- **Use Standard tier**: Premium tier is only needed for HSM-protected keys +- **Monitor unused secrets**: Regularly audit and remove unused secrets and keys +- **Optimize API calls**: Batch secret retrieval where possible to reduce transaction costs +- **Use managed identities**: Avoid service principal credential management overhead + +### CI/CD Integration +- **Use service principals or managed identities**: Authenticate CI/CD pipelines using Azure AD +- **Reference secrets at runtime**: Never store secrets in build artifacts +- **Implement approval gates**: Require security approval before accessing production secrets +- **Use separate Key Vaults**: Use different Key Vaults for CI/CD and production + +## Deployment Scenarios + +### Scenario Matrix + +This building block supports 4 deployment scenarios based on your networking and security requirements: + +| # | Scenario | Private Endpoint | VNet Type | Hub Peering | Use Case | +|---|----------|-----------------|-----------|-------------|----------| +| **1** | **New VNet + Hub Peering** | βœ… | New (created) | βœ… Created | Isolated workload needing hub/on-prem access | +| **2** | **Existing Shared VNet** | βœ… | Existing (shared) | ❌ Skipped | Multi-tenant with shared connectivity | +| **3** | **Private Isolated** | βœ… | New or Existing | ❌ None | Secure workload, same-VNet access only | +| **4** | **Completely Public** | ❌ | Not applicable | ❌ None | Dev/test, public internet access | + +**Configuration Quick Reference:** + +| Scenario | `private_endpoint_enabled` | `vnet_name` | `hub_vnet_name` | +|----------|---------------------------|-------------|-----------------| +| **1 - New VNet + Hub** | `true` | `null` | Set (creates peering) | +| **2 - Existing Shared VNet** | `true` | Set (existing) | Omit/null (no peering) | +| **3 - Private Isolated** | `true` | `null` or Set | `null` | +| **4 - Public** | `false` | Any | Any | + +--- + +## Getting Started + +### Authenticating with Key Vault + +#### Using Azure CLI (Recommended) +```bash +# Login to Azure +az login + +# Set Key Vault secrets +az keyvault secret set --vault-name mycompanykv --name "DatabasePassword" --value "SecurePassword123!" + +# Retrieve secrets +az keyvault secret show --vault-name mycompanykv --name "DatabasePassword" --query value -o tsv +``` + +#### Using Azure PowerShell +```powershell +# Connect to Azure +Connect-AzAccount + +# Set a secret +Set-AzKeyVaultSecret -VaultName "mycompanykv" -Name "DatabasePassword" -SecretValue (ConvertTo-SecureString "SecurePassword123!" -AsPlainText -Force) + +# Retrieve a secret +Get-AzKeyVaultSecret -VaultName "mycompanykv" -Name "DatabasePassword" -AsPlainText +``` + +#### Using Managed Identity (Recommended for Applications) +```csharp +// .NET example using managed identity +using Azure.Identity; +using Azure.Security.KeyVault.Secrets; + +var client = new SecretClient( + new Uri("https://mycompanykv.vault.azure.net/"), + new DefaultAzureCredential() +); + +KeyVaultSecret secret = await client.GetSecretAsync("DatabasePassword"); +string password = secret.Value; +``` + +### Working with Private Key Vault + +For private Key Vault, ensure you have network connectivity: + +1. **From Azure Resources**: Must be in peered VNet or same VNet as private endpoint +2. **From On-Premises**: Connect via hub VNet using ExpressRoute or VPN Gateway +3. **From Developer Workstation**: Use VPN or Azure Bastion to access private network + +```bash +# Connect via VPN or Bastion, then access Key Vault +az keyvault secret show --vault-name mycompanykv --name "DatabasePassword" + +# Verify private endpoint resolution +nslookup mycompanykv.vault.azure.net +# Should resolve to private IP (10.x.x.x) +``` + +### Managing Secrets + +```bash +# Create a secret +az keyvault secret set --vault-name mycompanykv --name "ApiKey" --value "secret-value-123" + +# Create a secret with expiration +az keyvault secret set --vault-name mycompanykv --name "TempToken" --value "token-abc" --expires "2025-12-31T23:59:59Z" + +# List all secrets +az keyvault secret list --vault-name mycompanykv + +# List secret versions +az keyvault secret list-versions --vault-name mycompanykv --name "ApiKey" + +# Get a specific version +az keyvault secret show --vault-name mycompanykv --name "ApiKey" --version + +# Delete a secret (soft delete) +az keyvault secret delete --vault-name mycompanykv --name "ApiKey" + +# Recover a deleted secret +az keyvault secret recover --vault-name mycompanykv --name "ApiKey" + +# Purge a deleted secret (permanent) +az keyvault secret purge --vault-name mycompanykv --name "ApiKey" +``` + +### Managing Keys + +```bash +# Create an RSA key +az keyvault key create --vault-name mycompanykv --name "MyEncryptionKey" --kty RSA --size 2048 + +# List keys +az keyvault key list --vault-name mycompanykv + +# Get key details +az keyvault key show --vault-name mycompanykv --name "MyEncryptionKey" + +# Delete a key +az keyvault key delete --vault-name mycompanykv --name "MyEncryptionKey" +``` + +### Managing Certificates + +```bash +# Create a self-signed certificate +az keyvault certificate create --vault-name mycompanykv --name "MyCert" --policy "$(az keyvault certificate get-default-policy)" + +# Import a certificate +az keyvault certificate import --vault-name mycompanykv --name "ImportedCert" --file certificate.pfx --password "certpassword" + +# List certificates +az keyvault certificate list --vault-name mycompanykv + +# Download a certificate +az keyvault certificate download --vault-name mycompanykv --name "MyCert" --file mycert.pem +``` + +### Monitoring and Troubleshooting + +```bash +# Check Key Vault access +az keyvault check-access --vault-name mycompanykv + +# Enable diagnostic logging +az monitor diagnostic-settings create \ + --resource \ + --name kv-diagnostics \ + --workspace \ + --logs '[{"category": "AuditEvent", "enabled": true}]' + +# View Key Vault metrics +az monitor metrics list --resource --metric Availability +``` + +## Security Checklist + +- [ ] Use Azure RBAC instead of access policies +- [ ] Enable soft delete and purge protection +- [ ] Enable private endpoint for production workloads +- [ ] Configure network access rules (firewall or private endpoint) +- [ ] Implement diagnostic logging to Log Analytics +- [ ] Use managed identities for Azure service integration +- [ ] Implement RBAC with least-privilege access +- [ ] Set expiration dates on secrets +- [ ] Enable Azure Defender for Key Vault +- [ ] Regularly audit and rotate secrets +- [ ] Monitor access patterns and unusual activity +- [ ] Separate dev, staging, and production Key Vaults + +## Common Integration Patterns + +### Azure App Service +```bash +# Reference Key Vault secret in App Service configuration +az webapp config appsettings set \ + --resource-group myResourceGroup \ + --name myWebApp \ + --settings DatabasePassword="@Microsoft.KeyVault(VaultName=mycompanykv;SecretName=DatabasePassword)" +``` + +### Azure Functions +```json +{ + "IsEncrypted": false, + "Values": { + "AzureWebJobsStorage": "@Microsoft.KeyVault(VaultName=mycompanykv;SecretName=StorageConnectionString)", + "DatabasePassword": "@Microsoft.KeyVault(VaultName=mycompanykv;SecretName=DatabasePassword)" + } +} +``` + +### Kubernetes (AKS with CSI Driver) +```yaml +apiVersion: secrets-store.csi.x-k8s.io/v1 +kind: SecretProviderClass +metadata: + name: azure-keyvault-secrets +spec: + provider: azure + parameters: + keyvaultName: "mycompanykv" + tenantId: "" + objects: | + array: + - | + objectName: DatabasePassword + objectType: secret +--- +apiVersion: v1 +kind: Pod +metadata: + name: myapp +spec: + containers: + - name: myapp + image: myapp:latest + volumeMounts: + - name: secrets-store + mountPath: "/mnt/secrets" + readOnly: true + volumes: + - name: secrets-store + csi: + driver: secrets-store.csi.k8s.io + readOnly: true + volumeAttributes: + secretProviderClass: "azure-keyvault-secrets" +``` + +### CI/CD Pipeline (Azure DevOps) +```yaml +- task: AzureKeyVault@2 + inputs: + azureSubscription: 'MyServiceConnection' + KeyVaultName: 'mycompanykv' + SecretsFilter: '*' + RunAsPreJob: true +``` + +### GitHub Actions +```yaml +- name: Get secrets from Key Vault + uses: Azure/get-keyvault-secrets@v1 + with: + keyvault: "mycompanykv" + secrets: 'DatabasePassword, ApiKey' + id: kvSecrets + +- name: Use secret + run: | + echo "Secret retrieved: ${{ steps.kvSecrets.outputs.DatabasePassword }}" +``` diff --git a/modules/azure/key-vault/buildingblock/README.md b/modules/azure/key-vault/buildingblock/README.md index 21154d80..a09903be 100644 --- a/modules/azure/key-vault/buildingblock/README.md +++ b/modules/azure/key-vault/buildingblock/README.md @@ -3,17 +3,37 @@ name: Azure Key Vault supportedPlatforms: - azure description: | - Provides an Azure Key Vault to securely store and manage secrets, keys, and certificates with access control. + Provides an Azure Key Vault for secure storage and management of secrets, keys, and certificates with RBAC authorization, optional private endpoint support, and hub connectivity. --- # Azure Key Vault -This Terraform module provisions an Azure Key Vault along with necessary role assignments. +This Terraform module provisions an Azure Key Vault with support for both public and private networking configurations, including private endpoints with DNS integration and hub VNet peering. +## Features + +- **RBAC Authorization**: Uses Azure RBAC for secure access control +- **Private Endpoint Support**: Optional private endpoint with VNet integration +- **Hub Connectivity**: Bidirectional VNet peering for hub-spoke topologies +- **Private DNS Integration**: Automatic or custom Private DNS zone configuration +- **Security Defaults**: Soft delete and purge protection enabled by default +- **Flexible Networking**: Support for new or existing VNets and subnets +- **Tagging Support**: Custom tags for resource organization ## Requirements -- Terraform `>= 1.0` -- AzureRM Provider `>= 4.18.0` +- Terraform `>= 1.3.0` +- AzureRM Provider `~> 4.18.0` + +## Deployment Scenarios + +This module supports 4 deployment scenarios: + +1. **New VNet + Hub Peering**: Creates new VNet with private endpoint and bidirectional peering to hub +2. **Existing Shared VNet**: Deploys private endpoint into existing VNet (no peering) +3. **Private Isolated**: Creates private Key Vault without hub connectivity +4. **Completely Public**: Public Key Vault accessible from internet + +See [APP_TEAM_README.md](./APP_TEAM_README.md) for detailed usage examples and configuration. ## Providers @@ -22,7 +42,11 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "4.18.0" + version = "~> 4.18.0" + } + random = { + source = "hashicorp/random" + version = "~> 3.6.3" } } } @@ -30,7 +54,34 @@ terraform { provider "azurerm" { features {} } + +provider "azurerm" { + alias = "hub" + features {} +} +``` + +## Network Architecture + +### Private Endpoint with Hub Peering ``` +β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” +β”‚ Hub VNet │◄───────── Key Vault VNet β”‚ +β”‚ β”‚ Peeringβ”‚ β”‚ +β”‚ - On-premises │─────────► - Private EP β”‚ +β”‚ - VPN Gateway β”‚ β”‚ - DNS Zone β”‚ +β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ + β”‚ + β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β” + β”‚ Key Vault β”‚ + β”‚ Private IP β”‚ + β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ +``` + +### DNS Resolution +- **Private DNS Zone**: `privatelink.vaultcore.azure.net` +- **Private Endpoint Subresource**: `vault` +- **Automatic DNS**: System-managed or custom DNS zone ## Requirements @@ -50,25 +101,121 @@ No modules. | Name | Type | |------|------| | [azurerm_key_vault.key_vault](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/key_vault) | resource | +| [azurerm_private_dns_zone.key_vault_dns](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/private_dns_zone) | resource | +| [azurerm_private_dns_zone_virtual_network_link.key_vault_dns_link](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/private_dns_zone_virtual_network_link) | resource | +| [azurerm_private_endpoint.key_vault_pe](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/private_endpoint) | resource | | [azurerm_resource_group.key_vault](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/resource_group) | resource | +| [azurerm_subnet.pe_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/subnet) | resource | +| [azurerm_virtual_network.vnet](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/virtual_network) | resource | +| [azurerm_virtual_network_peering.hub_to_key_vault](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/virtual_network_peering) | resource | +| [azurerm_virtual_network_peering.key_vault_to_hub](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/resources/virtual_network_peering) | resource | | [random_string.resource_code](https://registry.terraform.io/providers/hashicorp/random/3.6.3/docs/resources/string) | resource | | [azurerm_client_config.current](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/client_config) | data source | +| [azurerm_resource_group.hub_rg](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/resource_group) | data source | +| [azurerm_subnet.existing_subnet](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/subnet) | data source | | [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/subscription) | data source | +| [azurerm_virtual_network.existing_vnet](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/virtual_network) | data source | +| [azurerm_virtual_network.hub_vnet](https://registry.terraform.io/providers/hashicorp/azurerm/4.18.0/docs/data-sources/virtual_network) | data source | ## Inputs | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [allow\_gateway\_transit\_from\_hub](#input\_allow\_gateway\_transit\_from\_hub) | Allow gateway transit from hub to spoke. Set to true if hub has a gateway and you want spoke to use it. | `bool` | `false` | no | +| [existing\_vnet\_resource\_group\_name](#input\_existing\_vnet\_resource\_group\_name) | Resource group name of the existing VNet. Only used when vnet\_name is provided. Defaults to the Key Vault resource group if not specified. | `string` | `null` | no | +| [hub\_resource\_group\_name](#input\_hub\_resource\_group\_name) | Resource group name of the hub virtual network. Required when private\_endpoint\_enabled is true and connecting to a hub. | `string` | `null` | no | +| [hub\_subscription\_id](#input\_hub\_subscription\_id) | Subscription ID of the hub network. Required when private\_endpoint\_enabled is true and connecting to a hub. | `string` | `null` | no | +| [hub\_vnet\_name](#input\_hub\_vnet\_name) | Name of the hub virtual network to peer with. Required when private\_endpoint\_enabled is true and connecting to a hub. | `string` | `null` | no | | [key\_vault\_name](#input\_key\_vault\_name) | The name of the key vault. | `string` | n/a | yes | | [key\_vault\_resource\_group\_name](#input\_key\_vault\_resource\_group\_name) | The name of the resource group containing the key vault. | `string` | n/a | yes | | [location](#input\_location) | The location/region where the key vault is created. | `string` | n/a | yes | +| [private\_dns\_zone\_id](#input\_private\_dns\_zone\_id) | Private DNS Zone ID for private endpoint. Use 'System' for Azure-managed zone, or provide custom zone ID. Only used when private\_endpoint\_enabled is true. | `string` | `"System"` | no | +| [private\_endpoint\_enabled](#input\_private\_endpoint\_enabled) | Enable private endpoint for Key Vault | `bool` | `false` | no | | [public\_network\_access\_enabled](#input\_public\_network\_access\_enabled) | n/a | `bool` | `false` | no | +| [subnet\_address\_prefix](#input\_subnet\_address\_prefix) | Address prefix for the private endpoint subnet (only used if subnet\_name is not provided) | `string` | `"10.250.1.0/24"` | no | +| [subnet\_name](#input\_subnet\_name) | Name of the subnet for private endpoint. If not provided, a new subnet will be created. | `string` | `null` | no | +| [tags](#input\_tags) | Tags to apply to all resources | `map(string)` | `{}` | no | +| [use\_remote\_gateways](#input\_use\_remote\_gateways) | Use remote gateways from hub VNet. Set to true only if hub has a VPN/ExpressRoute gateway configured. | `bool` | `false` | no | +| [vnet\_address\_space](#input\_vnet\_address\_space) | Address space for the VNet (only used if vnet\_name is not provided) | `string` | `"10.250.0.0/16"` | no | +| [vnet\_name](#input\_vnet\_name) | Name of the virtual network for private endpoint. If not provided, a new VNet will be created. | `string` | `null` | no | ## Outputs | Name | Description | |------|-------------| -| [key\_vault\_id](#output\_key\_vault\_id) | n/a | -| [key\_vault\_name](#output\_key\_vault\_name) | n/a | -| [key\_vault\_resource\_group](#output\_key\_vault\_resource\_group) | n/a | +| [key\_vault\_id](#output\_key\_vault\_id) | The ID of the Azure Key Vault | +| [key\_vault\_name](#output\_key\_vault\_name) | The name of the Azure Key Vault | +| [key\_vault\_resource\_group](#output\_key\_vault\_resource\_group) | Name of the resource group containing the Key Vault | +| [key\_vault\_uri](#output\_key\_vault\_uri) | The URI of the Azure Key Vault | +| [private\_dns\_zone\_id](#output\_private\_dns\_zone\_id) | ID of the private DNS zone (when System-managed) | +| [private\_endpoint\_ip](#output\_private\_endpoint\_ip) | Private IP address of the Key Vault private endpoint | +| [subnet\_id](#output\_subnet\_id) | ID of the subnet used for private endpoint | +| [vnet\_id](#output\_vnet\_id) | ID of the virtual network used for private endpoint | + +## Example Usage + +### Public Key Vault +```hcl +module "key_vault_public" { + source = "./buildingblock" + + key_vault_name = "myapp-kv" + key_vault_resource_group_name = "myapp-rg" + location = "West Europe" + public_network_access_enabled = true + private_endpoint_enabled = false +} +``` + +### Private Key Vault with Hub Peering +```hcl +module "key_vault_private" { + source = "./buildingblock" + + key_vault_name = "myapp-kv" + key_vault_resource_group_name = "myapp-rg" + location = "West Europe" + public_network_access_enabled = false + + private_endpoint_enabled = true + private_dns_zone_id = "System" + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.1.0/24" + + hub_subscription_id = "00000000-0000-0000-0000-000000000000" + hub_resource_group_name = "hub-network-rg" + hub_vnet_name = "hub-vnet" + + tags = { + Environment = "Production" + ManagedBy = "Terraform" + } +} +``` + +## Security Best Practices + +1. **Enable Private Endpoints**: Always use private endpoints for production workloads +2. **RBAC Authorization**: Use Azure RBAC for access control (enabled by default) +3. **Soft Delete and Purge Protection**: Enabled by default to prevent accidental deletion +4. **Network Isolation**: Disable public access for production environments +5. **Monitoring**: Enable diagnostic logging to Log Analytics +6. **Least Privilege**: Grant minimal required RBAC roles to users and applications + +## Troubleshooting + +### Private DNS Not Resolving +Ensure the Private DNS zone is linked to the VNet and DNS settings are configured correctly. + +### Cannot Access from On-Premises +Verify VNet peering is established and DNS forwarding is configured for the private DNS zone. + +### Permission Denied +Check Azure RBAC role assignments. Users need `Key Vault Secrets User` or similar roles. + +## Related Resources + +- [Azure Key Vault Documentation](https://docs.microsoft.com/en-us/azure/key-vault/) +- [Private Endpoints Overview](https://docs.microsoft.com/en-us/azure/private-link/private-endpoint-overview) +- [Azure RBAC for Key Vault](https://docs.microsoft.com/en-us/azure/key-vault/general/rbac-guide) diff --git a/modules/azure/key-vault/buildingblock/key-vault.tftest.hcl b/modules/azure/key-vault/buildingblock/key-vault.tftest.hcl index 440626d9..e6497760 100644 --- a/modules/azure/key-vault/buildingblock/key-vault.tftest.hcl +++ b/modules/azure/key-vault/buildingblock/key-vault.tftest.hcl @@ -1,39 +1,320 @@ -# terraform test is cool because it does the apply and destroy lifecycle -# what it doesn't test though is the backend storage. if we want to test that, we need to that via terragrunt - -run "verify" { - variables { - key_vault_resource_group_name = "kv-rg" - key_vault_name = "kv-integrationtest" - location = "westeurope" - users = [ - { - meshIdentifier = "identifier0" - username = "likvid-daniela@meshcloud.io" - firstName = "likvid" - lastName = "daniela" - email = "likvid-daniela@meshcloud.io" - euid = "likvid-daniela@meshcloud.io" - roles = ["reader"] - }, - { - meshIdentifier = "identifier1" - username = "likvid-tom@meshcloud.io" - firstName = "likvid" - lastName = "tom" - email = "likvid-tom@meshcloud.io" - euid = "likvid-tom@meshcloud.io" - roles = ["editor"] - }, - { - meshIdentifier = "identifier2" - username = "likvid-anna@meshcloud.io" - firstName = "likvid" - lastName = "anna" - email = "likvid-anna@meshcloud.io" - euid = "likvid-anna@meshcloud.io" - roles = ["admin"] - } - ] +run "scenario_1_new_vnet_with_hub_peering" { + variables { + key_vault_name = "testkv01" + key_vault_resource_group_name = "kv-test-rg01" + location = "Germany West Central" + public_network_access_enabled = false + + private_endpoint_enabled = true + private_dns_zone_id = "System" + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.1.0/24" + + hub_subscription_id = "5066eff7-4173-4fea-8c67-268456b4a4f7" + hub_resource_group_name = "likvid-hub-vnet-rg" + hub_vnet_name = "hub-vnet" + } + + assert { + condition = azurerm_key_vault.key_vault.public_network_access_enabled == false + error_message = "Public network access should be disabled" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 1 + error_message = "VNet should be created when vnet_name is null" + } + + assert { + condition = contains(one(azurerm_virtual_network.vnet[*].address_space), "10.250.0.0/16") + error_message = "VNet should have correct address space" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 1 + error_message = "Private endpoint should be created" + } + + assert { + condition = length(azurerm_virtual_network_peering.key_vault_to_hub) == 1 + error_message = "Peering to hub should be created when creating new VNet" + } + + assert { + condition = length(azurerm_virtual_network_peering.hub_to_key_vault) == 1 + error_message = "Peering from hub should be created when creating new VNet" + } + + assert { + condition = length(azurerm_private_dns_zone.key_vault_dns) == 1 + error_message = "Private DNS zone should be created with System option" + } + + assert { + condition = azurerm_key_vault.key_vault.enable_rbac_authorization == true + error_message = "RBAC authorization should be enabled" + } +} + +run "scenario_2_existing_shared_vnet" { + variables { + key_vault_name = "testkv02" + key_vault_resource_group_name = "kv-test-rg02" + location = "Germany West Central" + public_network_access_enabled = false + + private_endpoint_enabled = true + private_dns_zone_id = "System" + vnet_name = "lz102-on-prem-nwk-vnet" + existing_vnet_resource_group_name = "connectivity" + subnet_name = "default" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 0 + error_message = "VNet should NOT be created when vnet_name is provided" + } + + assert { + condition = length(azurerm_virtual_network_peering.key_vault_to_hub) == 0 + error_message = "Peering to hub should NOT be created when using existing VNet" + } + + assert { + condition = length(azurerm_virtual_network_peering.hub_to_key_vault) == 0 + error_message = "Peering from hub should NOT be created when using existing VNet" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 1 + error_message = "Private endpoint should be created in existing VNet" + } + + assert { + condition = var.existing_vnet_resource_group_name == "connectivity" + error_message = "Should use VNet from different resource group" + } +} + +run "scenario_3_private_isolated_no_hub" { + variables { + key_vault_name = "testkv03" + key_vault_resource_group_name = "kv-test-rg03" + location = "Germany West Central" + public_network_access_enabled = false + + private_endpoint_enabled = true + private_dns_zone_id = "System" + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.1.0/24" + + hub_vnet_name = null + hub_resource_group_name = null + hub_subscription_id = null + } + + assert { + condition = azurerm_key_vault.key_vault.public_network_access_enabled == false + error_message = "Public network access should be disabled" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 1 + error_message = "VNet should be created" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 1 + error_message = "Private endpoint should be created" + } + + assert { + condition = length(azurerm_virtual_network_peering.key_vault_to_hub) == 0 + error_message = "Peering to hub should NOT be created when hub_vnet_name is null" + } + + assert { + condition = length(azurerm_virtual_network_peering.hub_to_key_vault) == 0 + error_message = "Peering from hub should NOT be created when hub_vnet_name is null" + } +} + +run "scenario_4_completely_public" { + variables { + key_vault_name = "testkv04" + key_vault_resource_group_name = "kv-test-rg04" + location = "Germany West Central" + public_network_access_enabled = true + + private_endpoint_enabled = false + } + + assert { + condition = azurerm_key_vault.key_vault.public_network_access_enabled == true + error_message = "Public network access should be enabled" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 0 + error_message = "Private endpoint should NOT be created" + } + + assert { + condition = length(azurerm_virtual_network.vnet) == 0 + error_message = "VNet should NOT be created" + } + + assert { + condition = output.key_vault_uri != "" + error_message = "Key Vault URI should be accessible" + } +} + +run "private_endpoint_subnet_network_policies" { + variables { + key_vault_name = "testkv05" + key_vault_resource_group_name = "kv-test-rg05" + location = "Germany West Central" + private_endpoint_enabled = true + public_network_access_enabled = false + private_dns_zone_id = "System" + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.1.0/24" + } + + assert { + condition = length(azurerm_subnet.pe_subnet) == 1 + error_message = "Subnet should be created" + } + + assert { + condition = one(azurerm_subnet.pe_subnet[*].private_endpoint_network_policies) == "NetworkSecurityGroupEnabled" + error_message = "Subnet should have NSG network policies enabled for private endpoints" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 1 + error_message = "Private endpoint should be created" + } +} + +run "tags_applied" { + variables { + key_vault_name = "testkv06" + key_vault_resource_group_name = "kv-test-rg06" + location = "Germany West Central" + tags = { + Environment = "test" + CostCenter = "engineering" + } + } + + assert { + condition = azurerm_key_vault.key_vault.tags["Environment"] == "test" + error_message = "Environment tag should be set to test" + } + + assert { + condition = azurerm_key_vault.key_vault.tags["CostCenter"] == "engineering" + error_message = "CostCenter tag should be set to engineering" + } +} + +run "rbac_authorization_enabled" { + variables { + key_vault_name = "testkv07" + key_vault_resource_group_name = "kv-test-rg07" + location = "Germany West Central" + } + + assert { + condition = azurerm_key_vault.key_vault.enable_rbac_authorization == true + error_message = "RBAC authorization should be enabled" + } + + assert { + condition = azurerm_key_vault.key_vault.purge_protection_enabled == true + error_message = "Purge protection should be enabled" + } + + assert { + condition = azurerm_key_vault.key_vault.soft_delete_retention_days == 7 + error_message = "Soft delete retention should be 7 days" + } +} + +run "existing_vnet_with_custom_dns_zone" { + command = plan + variables { + key_vault_name = "testkv08" + key_vault_resource_group_name = "kv-test-rg08" + location = "Germany West Central" + public_network_access_enabled = false + + private_endpoint_enabled = true + private_dns_zone_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/dns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" + vnet_name = "lz102-on-prem-nwk-vnet" + existing_vnet_resource_group_name = "connectivity" + subnet_name = "default" + } + + assert { + condition = var.private_dns_zone_id != "System" + error_message = "Should use custom DNS zone ID" + } + + assert { + condition = length(azurerm_private_dns_zone.key_vault_dns) == 0 + error_message = "Private DNS zone should NOT be created when custom ID is provided" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 1 + error_message = "Private endpoint should be created with custom DNS zone" + } +} + +run "private_endpoint_subresource_name" { + variables { + key_vault_name = "testkv09" + key_vault_resource_group_name = "kv-test-rg09" + location = "Germany West Central" + private_endpoint_enabled = true + public_network_access_enabled = false + private_dns_zone_id = "System" + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.1.0/24" + } + + assert { + condition = length(azurerm_private_endpoint.key_vault_pe) == 1 + error_message = "Private endpoint should be created" + } + + assert { + condition = contains(azurerm_private_endpoint.key_vault_pe[0].private_service_connection[0].subresource_names, "vault") + error_message = "Private endpoint should use 'vault' subresource name" + } +} + +run "dns_zone_name_validation" { + variables { + key_vault_name = "testkv10" + key_vault_resource_group_name = "kv-test-rg10" + location = "Germany West Central" + private_endpoint_enabled = true + private_dns_zone_id = "System" + vnet_address_space = "10.250.0.0/16" + subnet_address_prefix = "10.250.1.0/24" + } + + assert { + condition = length(azurerm_private_dns_zone.key_vault_dns) == 1 + error_message = "Private DNS zone should be created" + } + + assert { + condition = one(azurerm_private_dns_zone.key_vault_dns[*].name) == "privatelink.vaultcore.azure.net" + error_message = "Private DNS zone should use correct naming for Key Vault" } } diff --git a/modules/azure/key-vault/buildingblock/main.tf b/modules/azure/key-vault/buildingblock/main.tf index 6caa17ad..226b1013 100644 --- a/modules/azure/key-vault/buildingblock/main.tf +++ b/modules/azure/key-vault/buildingblock/main.tf @@ -13,6 +13,51 @@ resource "azurerm_resource_group" "key_vault" { location = var.location } +resource "azurerm_virtual_network" "vnet" { + count = var.private_endpoint_enabled && var.vnet_name == null ? 1 : 0 + name = "${var.key_vault_name}-vnet" + address_space = [var.vnet_address_space] + location = var.location + resource_group_name = azurerm_resource_group.key_vault.name + tags = var.tags + + depends_on = [azurerm_resource_group.key_vault] +} + +data "azurerm_virtual_network" "existing_vnet" { + count = var.private_endpoint_enabled && var.vnet_name != null ? 1 : 0 + name = var.vnet_name + resource_group_name = var.existing_vnet_resource_group_name != null ? var.existing_vnet_resource_group_name : var.key_vault_resource_group_name +} + +locals { + vnet_id = var.private_endpoint_enabled ? (var.vnet_name != null ? data.azurerm_virtual_network.existing_vnet[0].id : azurerm_virtual_network.vnet[0].id) : null + vnet_name = var.private_endpoint_enabled ? (var.vnet_name != null ? var.vnet_name : azurerm_virtual_network.vnet[0].name) : null +} + +resource "azurerm_subnet" "pe_subnet" { + count = var.private_endpoint_enabled && var.subnet_name == null ? 1 : 0 + name = "${var.key_vault_name}-pe-subnet" + resource_group_name = azurerm_resource_group.key_vault.name + virtual_network_name = local.vnet_name + address_prefixes = [var.subnet_address_prefix] + + private_endpoint_network_policies = "NetworkSecurityGroupEnabled" + + depends_on = [azurerm_virtual_network.vnet] +} + +data "azurerm_subnet" "existing_subnet" { + count = var.private_endpoint_enabled && var.subnet_name != null ? 1 : 0 + name = var.subnet_name + virtual_network_name = var.vnet_name + resource_group_name = var.existing_vnet_resource_group_name != null ? var.existing_vnet_resource_group_name : var.key_vault_resource_group_name +} + +locals { + subnet_id = var.private_endpoint_enabled ? (var.subnet_name != null ? data.azurerm_subnet.existing_subnet[0].id : azurerm_subnet.pe_subnet[0].id) : null +} + resource "azurerm_key_vault" "key_vault" { name = "${var.key_vault_name}-${random_string.resource_code.result}" location = var.location @@ -23,4 +68,95 @@ resource "azurerm_key_vault" "key_vault" { purge_protection_enabled = true enable_rbac_authorization = true public_network_access_enabled = var.public_network_access_enabled + tags = var.tags +} + +resource "azurerm_private_endpoint" "key_vault_pe" { + count = var.private_endpoint_enabled ? 1 : 0 + name = "${var.key_vault_name}-pe" + location = var.location + resource_group_name = azurerm_resource_group.key_vault.name + subnet_id = local.subnet_id + + private_service_connection { + name = "${var.key_vault_name}-psc" + private_connection_resource_id = azurerm_key_vault.key_vault.id + is_manual_connection = false + subresource_names = ["vault"] + } + + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id != null ? [1] : [] + content { + name = "default" + private_dns_zone_ids = var.private_dns_zone_id == "System" ? [azurerm_private_dns_zone.key_vault_dns[0].id] : [var.private_dns_zone_id] + } + } + + tags = var.tags + + depends_on = [azurerm_key_vault.key_vault] +} + +resource "azurerm_private_dns_zone" "key_vault_dns" { + count = var.private_endpoint_enabled && var.private_dns_zone_id == "System" ? 1 : 0 + name = "privatelink.vaultcore.azure.net" + resource_group_name = azurerm_resource_group.key_vault.name + tags = var.tags + + depends_on = [azurerm_resource_group.key_vault] +} + +resource "azurerm_private_dns_zone_virtual_network_link" "key_vault_dns_link" { + count = var.private_endpoint_enabled && var.private_dns_zone_id == "System" ? 1 : 0 + name = "${var.key_vault_name}-dns-link" + resource_group_name = azurerm_resource_group.key_vault.name + private_dns_zone_name = azurerm_private_dns_zone.key_vault_dns[0].name + virtual_network_id = local.vnet_id + tags = var.tags + + depends_on = [ + azurerm_private_dns_zone.key_vault_dns, + azurerm_private_endpoint.key_vault_pe + ] +} + +data "azurerm_resource_group" "hub_rg" { + count = var.private_endpoint_enabled && var.hub_resource_group_name != null ? 1 : 0 + provider = azurerm.hub + name = var.hub_resource_group_name +} + +data "azurerm_virtual_network" "hub_vnet" { + count = var.private_endpoint_enabled && var.hub_vnet_name != null ? 1 : 0 + provider = azurerm.hub + name = var.hub_vnet_name + resource_group_name = data.azurerm_resource_group.hub_rg[0].name +} + +resource "azurerm_virtual_network_peering" "key_vault_to_hub" { + count = var.private_endpoint_enabled && var.vnet_name == null && var.hub_vnet_name != null ? 1 : 0 + name = "${var.key_vault_name}-to-hub" + resource_group_name = azurerm_resource_group.key_vault.name + virtual_network_name = local.vnet_name + remote_virtual_network_id = data.azurerm_virtual_network.hub_vnet[0].id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = false + use_remote_gateways = var.use_remote_gateways +} + +resource "azurerm_virtual_network_peering" "hub_to_key_vault" { + count = var.private_endpoint_enabled && var.vnet_name == null && var.hub_vnet_name != null ? 1 : 0 + provider = azurerm.hub + name = "hub-to-${var.key_vault_name}" + resource_group_name = data.azurerm_resource_group.hub_rg[0].name + virtual_network_name = data.azurerm_virtual_network.hub_vnet[0].name + remote_virtual_network_id = local.vnet_id + + allow_virtual_network_access = true + allow_forwarded_traffic = true + allow_gateway_transit = var.allow_gateway_transit_from_hub + use_remote_gateways = false } diff --git a/modules/azure/key-vault/buildingblock/outputs.tf b/modules/azure/key-vault/buildingblock/outputs.tf index c185a1ad..865d94fa 100644 --- a/modules/azure/key-vault/buildingblock/outputs.tf +++ b/modules/azure/key-vault/buildingblock/outputs.tf @@ -1,11 +1,39 @@ output "key_vault_id" { - value = azurerm_key_vault.key_vault.id + description = "The ID of the Azure Key Vault" + value = azurerm_key_vault.key_vault.id } output "key_vault_name" { - value = azurerm_key_vault.key_vault.name + description = "The name of the Azure Key Vault" + value = azurerm_key_vault.key_vault.name +} + +output "key_vault_uri" { + description = "The URI of the Azure Key Vault" + value = azurerm_key_vault.key_vault.vault_uri } output "key_vault_resource_group" { - value = azurerm_resource_group.key_vault.name + description = "Name of the resource group containing the Key Vault" + value = azurerm_resource_group.key_vault.name +} + +output "private_endpoint_ip" { + description = "Private IP address of the Key Vault private endpoint" + value = var.private_endpoint_enabled ? azurerm_private_endpoint.key_vault_pe[0].private_service_connection[0].private_ip_address : null +} + +output "vnet_id" { + description = "ID of the virtual network used for private endpoint" + value = local.vnet_id +} + +output "subnet_id" { + description = "ID of the subnet used for private endpoint" + value = local.subnet_id +} + +output "private_dns_zone_id" { + description = "ID of the private DNS zone (when System-managed)" + value = var.private_endpoint_enabled && var.private_dns_zone_id == "System" ? azurerm_private_dns_zone.key_vault_dns[0].id : null } diff --git a/modules/azure/key-vault/buildingblock/provider.tf b/modules/azure/key-vault/buildingblock/provider.tf index d339328a..bfc4ba0d 100644 --- a/modules/azure/key-vault/buildingblock/provider.tf +++ b/modules/azure/key-vault/buildingblock/provider.tf @@ -1,4 +1,8 @@ provider "azurerm" { features {} - # Configuration options +} + +provider "azurerm" { + alias = "hub" + features {} } diff --git a/modules/azure/key-vault/buildingblock/variables.tf b/modules/azure/key-vault/buildingblock/variables.tf index fbe8438e..a6c50bb9 100644 --- a/modules/azure/key-vault/buildingblock/variables.tf +++ b/modules/azure/key-vault/buildingblock/variables.tf @@ -19,3 +19,91 @@ variable "public_network_access_enabled" { type = bool default = false } + +variable "private_endpoint_enabled" { + type = bool + description = "Enable private endpoint for Key Vault" + default = false +} + +variable "private_dns_zone_id" { + type = string + description = "Private DNS Zone ID for private endpoint. Use 'System' for Azure-managed zone, or provide custom zone ID. Only used when private_endpoint_enabled is true." + default = "System" +} + +variable "vnet_name" { + type = string + description = "Name of the virtual network for private endpoint. If not provided, a new VNet will be created." + default = null +} + +variable "existing_vnet_resource_group_name" { + type = string + description = "Resource group name of the existing VNet. Only used when vnet_name is provided. Defaults to the Key Vault resource group if not specified." + default = null +} + +variable "vnet_address_space" { + type = string + description = "Address space for the VNet (only used if vnet_name is not provided)" + default = "10.250.0.0/16" + + validation { + condition = can(cidrhost(var.vnet_address_space, 0)) + error_message = "VNet address space must be a valid CIDR block." + } +} + +variable "subnet_name" { + type = string + description = "Name of the subnet for private endpoint. If not provided, a new subnet will be created." + default = null +} + +variable "subnet_address_prefix" { + type = string + description = "Address prefix for the private endpoint subnet (only used if subnet_name is not provided)" + default = "10.250.1.0/24" + + validation { + condition = can(cidrhost(var.subnet_address_prefix, 0)) + error_message = "Subnet address prefix must be a valid CIDR block." + } +} + +variable "hub_subscription_id" { + type = string + description = "Subscription ID of the hub network. Required when private_endpoint_enabled is true and connecting to a hub." + default = null +} + +variable "hub_resource_group_name" { + type = string + description = "Resource group name of the hub virtual network. Required when private_endpoint_enabled is true and connecting to a hub." + default = null +} + +variable "hub_vnet_name" { + type = string + description = "Name of the hub virtual network to peer with. Required when private_endpoint_enabled is true and connecting to a hub." + default = null +} + +variable "use_remote_gateways" { + type = bool + description = "Use remote gateways from hub VNet. Set to true only if hub has a VPN/ExpressRoute gateway configured." + default = false +} + +variable "allow_gateway_transit_from_hub" { + type = bool + description = "Allow gateway transit from hub to spoke. Set to true if hub has a gateway and you want spoke to use it." + default = false +} + +variable "tags" { + type = map(string) + description = "Tags to apply to all resources" + default = {} +} diff --git a/modules/azure/service-principal/buildingblock/README.md b/modules/azure/service-principal/buildingblock/README.md index 40454a05..36b6dffa 100644 --- a/modules/azure/service-principal/buildingblock/README.md +++ b/modules/azure/service-principal/buildingblock/README.md @@ -3,7 +3,6 @@ name: Azure Service Principal supportedPlatforms: - azure description: Creates an Entra ID application registration and service principal with role assignment for automated access to Azure resources -category: security --- # Azure Service Principal Building Block diff --git a/modules/azuredevops/pipeline/buildingblock/README.md b/modules/azuredevops/pipeline/buildingblock/README.md index 880a5e68..18c20a07 100644 --- a/modules/azuredevops/pipeline/buildingblock/README.md +++ b/modules/azuredevops/pipeline/buildingblock/README.md @@ -3,7 +3,6 @@ name: Azure DevOps Pipeline supportedPlatforms: - azuredevops description: Provides a CI/CD pipeline in Azure DevOps linked to a repository with YAML-based configuration -category: devops --- # Azure DevOps Pipeline Building Block diff --git a/modules/azuredevops/project/buildingblock/README.md b/modules/azuredevops/project/buildingblock/README.md index 4fa018bb..b77abf73 100644 --- a/modules/azuredevops/project/buildingblock/README.md +++ b/modules/azuredevops/project/buildingblock/README.md @@ -4,7 +4,6 @@ supportedPlatforms: - azuredevops description: | Creates and manages Azure DevOps projects with user entitlements, stakeholder licenses, and role-based group memberships. -category: devops --- # Azure DevOps Project Building Block diff --git a/modules/azuredevops/repository/buildingblock/README.md b/modules/azuredevops/repository/buildingblock/README.md index 419b9013..06aaf6e7 100644 --- a/modules/azuredevops/repository/buildingblock/README.md +++ b/modules/azuredevops/repository/buildingblock/README.md @@ -3,7 +3,6 @@ name: Azure DevOps Git Repository supportedPlatforms: - azuredevops description: Provides a Git repository in Azure DevOps with optional branch protection policies -category: devops --- # Azure DevOps Repository Building Block diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index 1a24b1e4..49d51237 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -3,7 +3,6 @@ name: Azure DevOps Service Connection (Subscription) supportedPlatforms: - azuredevops description: Provides an Azure subscription service connection in Azure DevOps for pipeline integration with Azure subscriptions -category: devops --- # Azure DevOps Service Connection (Subscription) Building Block diff --git a/modules/ionos/dcd/buildingblock/README.md b/modules/ionos/dcd/buildingblock/README.md index e7596cc5..9921bd18 100644 --- a/modules/ionos/dcd/buildingblock/README.md +++ b/modules/ionos/dcd/buildingblock/README.md @@ -3,7 +3,6 @@ name: IONOS DCD (Data Center Designer) supportedPlatforms: - ionos description: Creates and manages IONOS Data Center Designer environments with user onboarding, role-based access control, and datacenter provisioning. -category: infrastructure --- # IONOS DCD Building Block diff --git a/modules/ionos/user-management/buildingblock/README.md b/modules/ionos/user-management/buildingblock/README.md index f5efe725..9b687fe9 100644 --- a/modules/ionos/user-management/buildingblock/README.md +++ b/modules/ionos/user-management/buildingblock/README.md @@ -3,7 +3,6 @@ name: IONOS User Management supportedPlatforms: - ionos description: Creates and manages IONOS Cloud users with role-based access. This is a foundational module that should be deployed before DCD environments. -category: identity --- # IONOS User Management Building Block