Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 53 additions & 22 deletions modules/azure/spoke-network/backplane/README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,47 @@
# Azure Connect
# Azure Spoke Network — Backplane

This documentation is intended as a reference documentation for platform engineers using this module.
This documentation is intended as reference documentation for platform engineers deploying this backplane.

## Permissions
## What this backplane provisions

This is a complex building block backplane that requires permission across the central network hub as well as into the
target subscription for creating a spoke network. This backplane thus needs to work with multiple `azurerm` terraform providers.
The spoke network building block is special: it peers a newly created spoke vnet into a central
network hub, and a vnet peering must be created on **both** sides of the connection. The automation
identity therefore needs permissions in two places:

We establish a clear shared responsibility boundary in the target subscription by
deploying a `connectivity` resource group to target subscription. This resource group is exclusively owned by the connectivity building block backplane SPN.
- the **landing zone scope** (`var.scope`), where the spoke resource group, vnet and the spoke side
of the peering are created, and
- the **hub scope** (`var.hub_scope`), where the hub vnet lives and the hub side of the peering is created.

An Azure Policy confines the access of the SPN to that resource group.
This backplane creates a single **User-Assigned Managed Identity (UAMI)** as the automation principal
and grants it two custom role definitions:

| Role definition | Scope | Purpose |
| ---------------------- | --------------- | ---------------------------------------------------------------------- |
| `<name>-deploy` | `var.scope` | Manage the spoke resource group + vnet, hand ownership to the tenant, create the spoke side of the peering. |
| `<name>-deploy-hub` | `var.hub_scope` | Read the hub vnet/resource group and create the hub side of the peering. |

Because a single identity holds `Microsoft.Network/virtualNetworks/peer/action` at **both** scopes,
the cross-scope `LinkedAuthorizationFailed` error that a split spoke/hub identity would hit when
creating the linked peering is avoided.

The UAMI authenticates via **workload identity federation** (no secrets to rotate), federated against
the meshStack replicator subject(s) passed in `var.workload_identity_federation`.

## Operational notes

- `var.scope` is typically the management group that parents all landing zones; `var.hub_scope` is
typically the hub subscription (or a management group containing it).
- The identity is granted `Microsoft.Authorization/roleAssignments/*` at the spoke scope because the
building block hands ownership of the spoke resource group to the tenant. Consider confining the
identity to the connectivity resource group with an Azure Policy if you need a tighter boundary.

<!-- BEGIN_TF_DOCS -->
## Requirements

| Name | Version |
|------|---------|
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.0 |
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | 4.11.0 |
| <a name="requirement_terraform"></a> [terraform](#requirement\_terraform) | >= 1.3.0 |
| <a name="requirement_azurerm"></a> [azurerm](#requirement\_azurerm) | >= 4.36.0 |

## Modules

Expand All @@ -28,25 +51,33 @@ No modules.

| Name | Type |
|------|------|
| [azurerm_role_assignment.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azurerm/4.11.0/docs/resources/role_assignment) | resource |
| [azurerm_role_definition.buildingblock_deploy_hub](https://registry.terraform.io/providers/hashicorp/azurerm/4.11.0/docs/resources/role_definition) | resource |
| [azurerm_subscription.current](https://registry.terraform.io/providers/hashicorp/azurerm/4.11.0/docs/data-sources/subscription) | data source |
| [azurerm_federated_identity_credential.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/federated_identity_credential) | resource |
| [azurerm_resource_group.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource |
| [azurerm_role_assignment.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
| [azurerm_role_assignment.backplane_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_assignment) | resource |
| [azurerm_role_definition.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource |
| [azurerm_role_definition.backplane_hub](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/role_definition) | resource |
| [azurerm_user_assigned_identity.backplane](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) | resource |

## Inputs

| Name | Description | Type | Default | Required |
|------|-------------|------|---------|:--------:|
| <a name="input_name"></a> [name](#input\_name) | name of the building block, used for naming resources | `string` | n/a | yes |
| <a name="input_principal_ids"></a> [principal\_ids](#input\_principal\_ids) | set of principal ids that will be granted permissions to deploy the building block | `set(string)` | n/a | yes |
| <a name="input_scope"></a> [scope](#input\_scope) | Scope where the building block should be deployable, typically the parent of all Landing Zones. | `string` | n/a | yes |
| <a name="input_hub_scope"></a> [hub\_scope](#input\_hub\_scope) | Scope where the hub vnet lives (management group or subscription ID). The identity is granted vnet peering permissions here so it can peer the spoke into the hub. | `string` | n/a | yes |
| <a name="input_location"></a> [location](#input\_location) | Azure region for the UAMI resource group. | `string` | n/a | yes |
| <a name="input_name"></a> [name](#input\_name) | Name for the building block identity, resource group and role definitions. | `string` | n/a | yes |
| <a name="input_scope"></a> [scope](#input\_scope) | Scope where the spoke network can be deployed (management group or subscription ID), typically the parent of all landing zones. | `string` | n/a | yes |
| <a name="input_workload_identity_federation"></a> [workload\_identity\_federation](#input\_workload\_identity\_federation) | WIF issuer and subjects for federated authentication of the automation identity. | <pre>object({<br/> issuer = string<br/> subjects = list(string)<br/> })</pre> | n/a | yes |

## Outputs

| Name | Description |
|------|-------------|
| <a name="output_role_assignment_ids"></a> [role\_assignment\_ids](#output\_role\_assignment\_ids) | The IDs of the role assignments for the service principals. |
| <a name="output_role_assignment_principal_ids"></a> [role\_assignment\_principal\_ids](#output\_role\_assignment\_principal\_ids) | The principal IDs of the service principals that have been assigned the role. |
| <a name="output_role_definition_id"></a> [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the Connectivity building block to the hub. |
| <a name="output_role_definition_name"></a> [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the Connectivity building block to the hub. |
| <a name="output_scope"></a> [scope](#output\_scope) | The scope where the role definition and role assignments are applied. |
<!-- END_TF_DOCS -->
| <a name="output_hub_role_definition_id"></a> [hub\_role\_definition\_id](#output\_hub\_role\_definition\_id) | The ID of the role definition that enables peering the spoke into the hub vnet. |
| <a name="output_hub_role_definition_name"></a> [hub\_role\_definition\_name](#output\_hub\_role\_definition\_name) | The name of the role definition that enables peering the spoke into the hub vnet. |
| <a name="output_hub_scope"></a> [hub\_scope](#output\_hub\_scope) | The scope where the hub peering role definition and role assignment are applied. |
| <a name="output_identity"></a> [identity](#output\_identity) | The managed identity used as the automation principal for this building block. |
| <a name="output_role_definition_id"></a> [role\_definition\_id](#output\_role\_definition\_id) | The ID of the role definition that enables deployment of the spoke network to landing zone subscriptions. |
| <a name="output_role_definition_name"></a> [role\_definition\_name](#output\_role\_definition\_name) | The name of the role definition that enables deployment of the spoke network to landing zone subscriptions. |
| <a name="output_scope"></a> [scope](#output\_scope) | The scope where the spoke deploy role definition and role assignment are applied. |
<!-- END_TF_DOCS -->
103 changes: 83 additions & 20 deletions modules/azure/spoke-network/backplane/main.tf
Original file line number Diff line number Diff line change
@@ -1,38 +1,101 @@
data "azurerm_subscription" "current" {
resource "azurerm_resource_group" "backplane" {
name = var.name
location = var.location
}

resource "azurerm_user_assigned_identity" "backplane" {
name = var.name
location = var.location
resource_group_name = azurerm_resource_group.backplane.name
}

resource "azurerm_federated_identity_credential" "backplane" {
for_each = { for i, s in var.workload_identity_federation.subjects : tostring(i) => s }

name = "subject-${each.key}"
user_assigned_identity_id = azurerm_user_assigned_identity.backplane.id
audience = ["api://AzureADTokenExchange"]
issuer = var.workload_identity_federation.issuer
subject = each.value
}

#
# Hub Deploy Roles
# Spoke deploy role — grants the automation identity everything it needs on the
# subscriptions/management group that hosts the spoke landing zones: manage the
# spoke resource group and vnet, hand out ownership on the spoke RG, and create
# the spoke side of the vnet peering.
#

# note: this role will be assigned using the access role above
resource "azurerm_role_definition" "buildingblock_deploy_hub" {
name = "buildingblock-${var.name}-deploy-hub"
description = "Enables deployment of the ${var.name} building block to the hub"
scope = data.azurerm_subscription.current.id # assume we are running in the hub subscription anyway
resource "azurerm_role_definition" "backplane" {
name = "${var.name}-deploy"
description = "Enables deployment of the ${var.name} spoke network building block to landing zone subscriptions"
scope = var.scope

permissions {
actions = [
"Microsoft.Resources/subscriptions/resourceGroups/*",
# Register resource providers in Azure Resource Manager
"*/register/action",
"Microsoft.Resources/subscriptions/providers/read",

# Spoke resource group
"Microsoft.Resources/subscriptions/resourceGroups/read",
"Microsoft.Resources/subscriptions/resourceGroups/write",
"Microsoft.Resources/subscriptions/resourceGroups/delete",

# Spoke virtual network + subnets + peering
"Microsoft.Network/virtualNetworks/read",
"Microsoft.Network/virtualNetworks/write",
"Microsoft.Network/virtualNetworks/delete",
"Microsoft.Network/virtualNetworks/subnets/*",
"Microsoft.Network/virtualNetworks/virtualNetworkPeerings/*",
"Microsoft.Network/virtualNetworks/subnets/read",
"Microsoft.Network/virtualNetworks/subnets/write",
"Microsoft.Network/virtualNetworks/subnets/delete",
"Microsoft.Network/virtualNetworks/virtualNetworkPeerings/read",
"Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write",
"Microsoft.Network/virtualNetworks/virtualNetworkPeerings/delete",
"Microsoft.Network/virtualNetworks/peer/action",

# Permission we need to activate/register required Resource Providers
"Microsoft.Resources/subscriptions/providers/read",
"*/register/action",
# The building block hands ownership of the spoke resource group to the tenant
"Microsoft.Authorization/roleAssignments/read",
"Microsoft.Authorization/roleAssignments/write",
"Microsoft.Authorization/roleAssignments/delete",
]
}
}

resource "azurerm_role_assignment" "buildingblock_deploy_hub" {
for_each = var.principal_ids
#
# Hub peering role — the spoke network building block peers *both* sides of the
# connection, so the same identity also needs rights where the hub lives: read
# the hub vnet/resource group and create the hub side of the peering.
#
# Note: creating a peering requires `peer/action` on *both* linked vnets. Because
# a single identity holds `peer/action` at the spoke scope (above) and at the hub
# scope (here), the cross-scope `LinkedAuthorizationFailed` that a split identity
# would hit is avoided.
#
resource "azurerm_role_definition" "backplane_hub" {
name = "${var.name}-deploy-hub"
description = "Enables the ${var.name} spoke network building block to peer into the hub vnet"
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_assignment" "backplane" {
scope = var.scope
role_definition_id = azurerm_role_definition.backplane.role_definition_resource_id
principal_id = azurerm_user_assigned_identity.backplane.principal_id
}

role_definition_id = azurerm_role_definition.buildingblock_deploy_hub.role_definition_resource_id
description = azurerm_role_definition.buildingblock_deploy_hub.description
principal_id = each.key
scope = data.azurerm_subscription.current.id # assume we are running in the spoke subscription anyway
resource "azurerm_role_assignment" "backplane_hub" {
scope = var.hub_scope
role_definition_id = azurerm_role_definition.backplane_hub.role_definition_resource_id
principal_id = azurerm_user_assigned_identity.backplane.principal_id
}
37 changes: 25 additions & 12 deletions modules/azure/spoke-network/backplane/outputs.tf
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
output "identity" {
value = {
client_id = azurerm_user_assigned_identity.backplane.client_id
principal_id = azurerm_user_assigned_identity.backplane.principal_id
tenant_id = azurerm_user_assigned_identity.backplane.tenant_id
}
description = "The managed identity used as the automation principal for this building block."
}

output "role_definition_id" {
value = azurerm_role_definition.buildingblock_deploy_hub.id
description = "The ID of the role definition that enables deployment of the Connectivity building block to the hub."
value = azurerm_role_definition.backplane.id
description = "The ID of the role definition that enables deployment of the spoke network to landing zone subscriptions."
}

output "role_definition_name" {
value = azurerm_role_definition.buildingblock_deploy_hub.name
description = "The name of the role definition that enables deployment of the Connectivity building block to the hub."
value = azurerm_role_definition.backplane.name
description = "The name of the role definition that enables deployment of the spoke network to landing zone subscriptions."
}

output "role_assignment_ids" {
value = { for id in var.principal_ids : id => azurerm_role_assignment.buildingblock_deploy_hub[id].id }
description = "The IDs of the role assignments for the service principals."
output "hub_role_definition_id" {
value = azurerm_role_definition.backplane_hub.id
description = "The ID of the role definition that enables peering the spoke into the hub vnet."
}

output "role_assignment_principal_ids" {
value = { for id in var.principal_ids : id => azurerm_role_assignment.buildingblock_deploy_hub[id].principal_id }
description = "The principal IDs of the service principals that have been assigned the role."
output "hub_role_definition_name" {
value = azurerm_role_definition.backplane_hub.name
description = "The name of the role definition that enables peering the spoke into the hub vnet."
}

output "scope" {
value = data.azurerm_subscription.current.id
description = "The scope where the role definition and role assignments are applied."
value = var.scope
description = "The scope where the spoke deploy role definition and role assignment are applied."
}

output "hub_scope" {
value = var.hub_scope
description = "The scope where the hub peering role definition and role assignment are applied."
}
3 changes: 3 additions & 0 deletions modules/azure/spoke-network/backplane/provider.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
provider "azurerm" {
features {}
}
25 changes: 20 additions & 5 deletions modules/azure/spoke-network/backplane/variables.tf
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
variable "name" {
type = string
nullable = false
description = "name of the building block, used for naming resources"
description = "Name for the building block identity, resource group and role definitions."
validation {
condition = can(regex("^[-a-z0-9]+$", var.name))
error_message = "Only alphanumeric lowercase characters and dashes are allowed"
Expand All @@ -11,11 +11,26 @@ 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 spoke network can be deployed (management group or subscription ID), typically the parent of all landing zones."
}

variable "principal_ids" {
type = set(string)
variable "hub_scope" {
type = string
nullable = false
description = "Scope where the hub vnet lives (management group or subscription ID). The identity is granted vnet peering permissions here so it can peer the spoke into the hub."
}

variable "location" {
type = string
nullable = false
description = "Azure region for the UAMI resource group."
}

variable "workload_identity_federation" {
type = object({
issuer = string
subjects = list(string)
})
nullable = false
description = "set of principal ids that will be granted permissions to deploy the building block"
description = "WIF issuer and subjects for federated authentication of the automation identity."
}
5 changes: 2 additions & 3 deletions modules/azure/spoke-network/backplane/versions.tf
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
terraform {
required_version = ">= 1.0"
required_version = ">= 1.3.0"

required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = "4.11.0"
version = ">= 4.36.0"
}
}
}

Loading
Loading