diff --git a/modules/azuredevops/service-connection-subscription/backplane/README.md b/modules/azuredevops/service-connection-subscription/backplane/README.md index a29f930..0397472 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/README.md +++ b/modules/azuredevops/service-connection-subscription/backplane/README.md @@ -4,11 +4,12 @@ This module provisions the infrastructure required to support the Azure DevOps S ## What It Provisions -- **Azure AD Service Principal**: For service connection management automation +- **Azure AD Application and Service Principal**: For service connection management automation - **Azure Key Vault**: Stores Azure DevOps Personal Access Token (PAT) - **Custom Role Definition**: Minimal permissions for reading Key Vault secrets - **Role Assignment**: Grants the service principal access to Key Vault -- **Federated Identity Credential** (optional): For workload identity federation (OIDC) authentication + +**Note**: Federated identity credentials are now created automatically by the building block module using the actual service connection details from Azure DevOps. ## Prerequisites @@ -21,7 +22,7 @@ This module provisions the infrastructure required to support the Azure DevOps S ## Usage -### Basic Backplane (Service Principal Authentication) +### Basic Backplane ```hcl module "azuredevops_service_connection_backplane" { @@ -36,25 +37,6 @@ module "azuredevops_service_connection_backplane" { } ``` -### Backplane with Workload Identity Federation - -```hcl -module "azuredevops_service_connection_backplane" { - source = "./backplane" - - azure_devops_organization_url = "https://dev.azure.com/myorg" - service_principal_name = "azuredevops-serviceconn-terraform" - key_vault_name = "kv-azdo-sc-prod" - resource_group_name = "rg-azdo-sc-prod" - location = "West Europe" - scope = "/subscriptions/00000000-0000-0000-0000-000000000000" - enable_workload_identity_federation = true - azure_devops_organization_id = "33333333-3333-3333-3333-333333333333" - azure_devops_project_name = "MyProject" - service_connection_name = "Azure-Production-Federated" -} -``` - ## Post-Deployment Steps 1. Create an Azure DevOps PAT with `Service Connections (Read, Query & Manage)` scope @@ -65,9 +47,9 @@ module "azuredevops_service_connection_backplane" { ## Workload Identity Federation -When `enable_workload_identity_federation = true`, this module configures: -- **Issuer**: `https://vstoken.dev.azure.com/{organization_id}` -- **Subject**: `sc://{org_url}/{project}/{connection_name}` +Federated identity credentials are automatically created by the building block module after the service connection is provisioned. This ensures the issuer and subject values exactly match what Azure DevOps generates: +- **Issuer**: Automatically determined by Azure DevOps +- **Subject**: Automatically determined based on service connection details - **Audience**: `api://AzureADTokenExchange` This eliminates the need for client secrets by using OIDC token exchange. @@ -97,7 +79,6 @@ No modules. | Name | Type | |------|------| | [azuread_application.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application) | resource | -| [azuread_application_federated_identity_credential.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | | [azuread_service_principal.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/service_principal) | resource | | [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) | resource | | [azurerm_resource_group.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | @@ -110,23 +91,19 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| -| [azure\_devops\_organization\_id](#input\_azure\_devops\_organization\_id) | Azure DevOps organization ID (GUID) for workload identity federation | `string` | n/a | yes | | [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | -| [azure\_devops\_project\_name](#input\_azure\_devops\_project\_name) | Azure DevOps project name for workload identity federation | `string` | n/a | yes | | [key\_vault\_name](#input\_key\_vault\_name) | Name of the Key Vault to store the Azure DevOps PAT | `string` | n/a | yes | | [location](#input\_location) | Azure region for resources | `string` | `"West Europe"` | no | | [resource\_group\_name](#input\_resource\_group\_name) | Resource group name for the Key Vault | `string` | n/a | yes | | [scope](#input\_scope) | Azure scope for role definitions (subscription or management group) | `string` | n/a | yes | -| [service\_connection\_name](#input\_service\_connection\_name) | Azure DevOps service connection name for workload identity federation | `string` | n/a | yes | | [service\_principal\_name](#input\_service\_principal\_name) | Name for the Azure DevOps service principal | `string` | `"azure-devops-terraform"` | no | ## Outputs | Name | Description | |------|-------------| +| [application\_object\_id](#output\_application\_object\_id) | Application Object ID (not client ID) of the Azure AD application for federated identity credential setup | | [azure\_devops\_organization\_url](#output\_azure\_devops\_organization\_url) | Azure DevOps organization URL | -| [federated\_credential\_issuer](#output\_federated\_credential\_issuer) | Issuer URL for workload identity federation | -| [federated\_credential\_subject](#output\_federated\_credential\_subject) | Subject identifier for workload identity federation | | [key\_vault\_id](#output\_key\_vault\_id) | ID of the Key Vault for storing Azure DevOps PAT | | [key\_vault\_name](#output\_key\_vault\_name) | Name of the Key Vault for storing Azure DevOps PAT | | [key\_vault\_uri](#output\_key\_vault\_uri) | URI of the Key Vault for storing Azure DevOps PAT | diff --git a/modules/azuredevops/service-connection-subscription/backplane/main.tf b/modules/azuredevops/service-connection-subscription/backplane/main.tf index 594577f..53e83b0 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/main.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/main.tf @@ -67,12 +67,3 @@ resource "azurerm_role_assignment" "azure_devops_manager" { role_definition_id = azurerm_role_definition.azure_devops_manager.role_definition_resource_id principal_id = azuread_service_principal.azure_devops.object_id } - -resource "azuread_application_federated_identity_credential" "azure_devops" { - application_id = azuread_application.azure_devops.id - display_name = "${var.service_connection_name}-federated-credential" - description = "Federated identity credential for Azure DevOps service connection" - audiences = ["api://AzureADTokenExchange"] - issuer = "https://vstoken.dev.azure.com/${var.azure_devops_organization_id}" - subject = "sc://${var.azure_devops_organization_url}/${var.azure_devops_project_name}/${var.service_connection_name}" -} diff --git a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf index 94bb9a3..868ac09 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/outputs.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/outputs.tf @@ -33,12 +33,7 @@ output "azure_devops_organization_url" { value = var.azure_devops_organization_url } -output "federated_credential_issuer" { - description = "Issuer URL for workload identity federation" - value = "https://vstoken.dev.azure.com/${var.azure_devops_organization_id}" -} - -output "federated_credential_subject" { - description = "Subject identifier for workload identity federation" - value = "sc://${var.azure_devops_organization_url}/${var.azure_devops_project_name}/${var.service_connection_name}" +output "application_object_id" { + description = "Application Object ID (not client ID) of the Azure AD application for federated identity credential setup" + value = azuread_application.azure_devops.object_id } diff --git a/modules/azuredevops/service-connection-subscription/backplane/variables.tf b/modules/azuredevops/service-connection-subscription/backplane/variables.tf index 0302061..40d6646 100644 --- a/modules/azuredevops/service-connection-subscription/backplane/variables.tf +++ b/modules/azuredevops/service-connection-subscription/backplane/variables.tf @@ -29,18 +29,3 @@ variable "scope" { description = "Azure scope for role definitions (subscription or management group)" type = string } - -variable "azure_devops_organization_id" { - description = "Azure DevOps organization ID (GUID) for workload identity federation" - type = string -} - -variable "azure_devops_project_name" { - description = "Azure DevOps project name for workload identity federation" - type = string -} - -variable "service_connection_name" { - description = "Azure DevOps service connection name for workload identity federation" - type = string -} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md b/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md index 5d604d4..c33073d 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/APP_TEAM_README.md @@ -17,11 +17,11 @@ This building block connects your Azure DevOps pipelines to Azure subscriptions, | Assign Azure roles to service principal | ✅ | ❌ | | Create service connection | ✅ | ❌ | | Provide service principal credentials | ✅ | ❌ | +| Automatically configure federated credentials | ✅ | ❌ | | Authorize pipelines to use connection | ⚠️ | ⚠️ | | Use service connection in pipelines | ❌ | ✅ | | Deploy Azure resources via pipelines | ❌ | ✅ | | Monitor deployments | ❌ | ✅ | -| Manage federated credentials | ✅ | ❌ | ## 💡 Best Practices @@ -293,9 +293,9 @@ Run manually to verify connectivity and permissions. **Solution**: 1. Contact Platform Team to verify: - Service principal exists and is active - - Federated identity credential is properly configured - - Azure DevOps organization ID matches the issuer -2. Platform Team will investigate and fix the federated credential configuration + - Backplane was deployed successfully + - Azure AD application is configured correctly +2. The federated credential is automatically created - if there are issues, the Platform Team will need to check the Terraform deployment logs ### Cannot deploy to resource group @@ -324,12 +324,13 @@ Run manually to verify connectivity and permissions. ### No Credential Rotation Required! -This service connection uses **Workload Identity Federation (OIDC)**, which means: +This service connection uses **Workload Identity Federation (OIDC)** with **automatic federated credential setup**, which means: ✅ **No secrets to manage** - authentication uses short-lived tokens ✅ **Automatic token rotation** - tokens expire quickly and are refreshed automatically ✅ **Zero maintenance** - no manual credential rotation needed ✅ **Better security** - no long-lived credentials that can leak or be compromised +✅ **Automatic configuration** - federated credentials are created automatically by the Platform Team ### What This Means for You @@ -341,10 +342,12 @@ This service connection uses **Workload Identity Federation (OIDC)**, which mean **The Platform Team manages**: - Service principal configuration -- Federated identity credential setup +- Automated federated identity credential setup - Azure role assignments - Trust relationship between Azure DevOps and Azure AD +The federated credential is automatically created when your service connection is provisioned, using the exact values from Azure DevOps to ensure perfect compatibility. + ## 📚 Related Documentation - [Azure DevOps Service Connections](https://learn.microsoft.com/en-us/azure/devops/pipelines/library/service-endpoints) diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/README.md b/modules/azuredevops/service-connection-subscription/buildingblock/README.md index 1a24b1e..1a14b38 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/README.md +++ b/modules/azuredevops/service-connection-subscription/buildingblock/README.md @@ -17,11 +17,22 @@ Creates and manages Azure subscription service connections in Azure DevOps proje - Azure subscription ID to connect to - Azure DevOps PAT stored in Key Vault with `Service Connections (Read, Query & Manage)` scope - Existing Azure AD service principal with appropriate permissions on the target subscription -- Service principal with federated identity credential configured for Azure DevOps +- **Application Object ID** (not client ID) from the backplane for federated credential setup + +## Important: Application Object ID vs Client ID + +⚠️ **Critical**: The `application_object_id` variable requires the **Application Object ID**, not the Client ID (Application ID). + +- ✅ **Use**: `azuread_application.*.object_id` - Returns the Object ID (GUID) +- ❌ **Don't use**: `azuread_application.*.client_id` - Returns the Client ID (wrong ID) +- ❌ **Don't use**: `azuread_application.*.id` - Returns `/applications/{object_id}` format (will be double-formatted) + +The module transforms the Object ID to `/applications/{object_id}` format required by the federated identity credential resource. ## Features - Configures Azure DevOps service connection using workload identity federation (OIDC) +- Automatically creates federated identity credential with actual Azure DevOps values - No client secrets required - uses secure token-based authentication - Optional automatic authorization for all pipelines - Enhanced security through short-lived tokens @@ -44,26 +55,31 @@ module "azuredevops_service_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = "11111111-1111-1111-1111-111111111111" azure_tenant_id = "22222222-2222-2222-2222-222222222222" + application_object_id = azuread_application.azure_devops.object_id } ``` ### Service Connection with Auto-Authorization ```hcl -module "authorized_service_connection" { +module "backplane" { + source = "./backplane" + # backplane configuration +} + +module "azure_connection" { source = "./buildingblock" - azure_devops_organization_url = "https://dev.azure.com/myorg" - key_vault_name = "kv-azdo-sc-prod" - resource_group_name = "rg-azdo-sc-prod" + azure_devops_organization_url = module.backplane.azure_devops_organization_url + key_vault_name = module.backplane.key_vault_name + resource_group_name = module.backplane.resource_group_name - project_id = "12345678-1234-1234-1234-123456789012" - service_connection_name = "Azure-Dev" + project_id = module.azuredevops_project.project_id + service_connection_name = "Azure-Prod" azure_subscription_id = "87654321-4321-4321-4321-210987654321" - service_principal_id = "11111111-1111-1111-1111-111111111111" - azure_tenant_id = "22222222-2222-2222-2222-222222222222" - authorize_all_pipelines = true - description = "Development environment service connection" + service_principal_id = var.service_principal_id + azure_tenant_id = var.azure_tenant_id + application_object_id = module.backplane.application_object_id } ``` @@ -76,10 +92,8 @@ This module exclusively uses **Workload Identity Federation (OIDC)** for enhance The service principal must: 1. Be created and configured outside this module (typically in the backplane) 2. Have appropriate role assignments on the target Azure subscription -3. Have a federated identity credential configured for Azure DevOps with: - - Issuer: `https://vstoken.dev.azure.com/{organization_id}` (GUID, not name) - - Subject: `sc://{org_name}/{project_name}/{connection_name}` - - Audience: `api://AzureADTokenExchange` + +The federated identity credential is automatically created by this module after the service connection is provisioned, using the actual issuer and subject values from Azure DevOps. This ensures perfect alignment between the service connection and the federated credential configuration. ### Benefits @@ -124,6 +138,7 @@ module "azure_connection" { azure_subscription_id = "87654321-4321-4321-4321-210987654321" service_principal_id = var.service_principal_id azure_tenant_id = var.azure_tenant_id + application_object_id = module.backplane.application_object_id } ``` @@ -151,6 +166,7 @@ steps: ## Security Considerations - Service principal must be created and managed outside this module +- Federated identity credential is automatically created using actual Azure DevOps values - Workload identity federation uses short-lived tokens (no secrets stored) - Use least privilege principle when assigning roles to the service principal - Enable manual authorization for production service connections @@ -160,8 +176,8 @@ steps: ## Limitations - Service principal must be created and managed separately -- Changing service connection name requires recreation -- Federated identity credential must be configured in the service principal (typically via backplane) +- Changing service connection name requires recreation of both the connection and federated credential +- Federated identity credential is automatically managed by this module - Only workload identity federation is supported (no client secret authentication) @@ -170,6 +186,7 @@ steps: | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.0 | +| [azuread](#requirement\_azuread) | ~> 3.6.0 | | [azuredevops](#requirement\_azuredevops) | ~> 1.1.1 | | [azurerm](#requirement\_azurerm) | ~> 4.51.0 | @@ -181,6 +198,7 @@ No modules. | Name | Type | |------|------| +| [azuread_application_federated_identity_credential.azure_devops](https://registry.terraform.io/providers/hashicorp/azuread/latest/docs/resources/application_federated_identity_credential) | resource | | [azuredevops_resource_authorization.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/resource_authorization) | resource | | [azuredevops_serviceendpoint_azurerm.main](https://registry.terraform.io/providers/microsoft/azuredevops/latest/docs/resources/serviceendpoint_azurerm) | resource | | [azurerm_key_vault.devops](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/data-sources/key_vault) | data source | @@ -191,6 +209,7 @@ No modules. | Name | Description | Type | Default | Required | |------|-------------|------|---------|:--------:| +| [application\_object\_id](#input\_application\_object\_id) | Azure AD Application Object ID (not client ID) - use azuread\_application.*.object\_id | `string` | n/a | yes | | [authorize\_all\_pipelines](#input\_authorize\_all\_pipelines) | Automatically authorize all pipelines to use this service connection | `bool` | `false` | no | | [azure\_devops\_organization\_url](#input\_azure\_devops\_organization\_url) | Azure DevOps organization URL (e.g., https://dev.azure.com/myorg) | `string` | n/a | yes | | [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure Subscription ID to connect to | `string` | n/a | yes | diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf index 22eb7e7..8db2639 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/main.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/main.tf @@ -41,3 +41,12 @@ resource "azuredevops_resource_authorization" "main" { authorized = true type = "endpoint" } + +resource "azuread_application_federated_identity_credential" "azure_devops" { + application_id = "/applications/${var.application_object_id}" + display_name = "${var.service_connection_name}-federated-credential" + description = "Federated identity credential for Azure DevOps service connection" + audiences = ["api://AzureADTokenExchange"] + issuer = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_issuer + subject = azuredevops_serviceendpoint_azurerm.main.workload_identity_federation_subject +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl index 19e7f8a..bf349b3 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl +++ b/modules/azuredevops/service-connection-subscription/buildingblock/service-connection-subscription.tftest.hcl @@ -4,11 +4,11 @@ variables { resource_group_name = "rg-devops" pat_secret_name = "ado-pat" project_id = "eece6ccc-c821-46a1-9214-80df6da9e13f" - repository_id = "e5612cf3-36f1-4db5-b9d4-6431704233f3" service_connection_name = "test-service-connection" azure_subscription_id = "f808fff2-adda-415a-9b77-2833c041aacf" service_principal_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" + application_object_id = "53cc4637-18e2-44f6-8721-dfc08c030dde" azure_tenant_id = "5f0e994b-6436-4f58-be96-4dc7bebff827" } diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf index 0f0bcd6..27f7211 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/variables.tf @@ -55,3 +55,8 @@ variable "authorize_all_pipelines" { type = bool default = false } + +variable "application_object_id" { + description = "Azure AD Application Object ID (not client ID) - use azuread_application.*.object_id" + type = string +} diff --git a/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf b/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf index 6201b42..6e1d052 100644 --- a/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf +++ b/modules/azuredevops/service-connection-subscription/buildingblock/versions.tf @@ -10,5 +10,9 @@ terraform { source = "microsoft/azuredevops" version = "~> 1.1.1" } + azuread = { + source = "hashicorp/azuread" + version = "~> 3.6.0" + } } }