Introduction to Azure DevOps Workload identity federation (OIDC) with Terraform

Jared Holgate

Welcome! This post is going to cover the end-to-end process of configuring and using Workload identity federation in Azure DevOps for your Terraform deployments. We’ll cover:

  • Why use Workload identity federation
  • What is Workload identity federation and how does it work
  • How can your organisation start using Workload identity federation
  • How to configure Workload identity federation using the Azure DevOps Terraform provider
  • How to use Workload identity federation in an Azure DevOps Pipeline with the Terraform task

Why use Workload identity federation

Up until now the only way to avoid storing service principal secrets for Azure DevOps pipelines was to use a self-hosted Azure DevOps agents with managed identities. Now with Workload identity federation we remove that limitation and enable you to use short-lived tokens for authenticating to Azure. This significantly improves your security posture and removes the need to figure out how to share and rotate secrets. Workload identity federation works with many Azure DevOps tasks, not just the Terraform ones we are focussing on in this article, so you can use it for deploying code and other configuration tasks. I encourage you to learn more about the supported tasks here.

What is Workload identity federation and how does it work

Workload identity federation is an OpenID Connect implementation for Azure DevOps that allow you to use short-lived credential free authentication to Azure without the need to provision self-hosted agents with managed identity. You configure a trust between your Azure DevOps organisation and an Azure service principal. Azure DevOps then provides a token that can be used to authenticate to the Azure API.

You can read more about how Workload identity federation works here.

In the first iteration Workload identity federation for Azure DevOps works within the scope of a Service Connection. A new type of Azure Resource Manager Service Connection is available that enables this:

Image ServiceConnectionTypes

Any task that supports a Service Connection and has been updated to use Workload identity federation can then use your configured trust to interact with Azure resources.

On the Azure side we need to configure Federated Credentials on an App Registration or User Assigned Managed Identity service principal. There are a few settings needed for this, which you can find in the Azure DevOps Service Connection or you can figure out up front:

  • Issuer URL: This is the a URL in the format https://vstoken.dev.azure.com/<organisation-id> where organisation-id is the GUID of your Azure DevOps organisation. For example https://vstoken.dev.azure.com/f66a4bc2-08ad-4ec0-a25e-e769dab3b294.
  • Subject identifier: This is the mapping to your service connection in the format sc://<organisation-name>/<project-name>/<service-connection-name> where organisation-name is your Azure DevOps organisation name, project-name is your Azure DevOps project name and service-connection-name is the name of your service connection. For example sc://my-organisation/my-project/my-service-connection.
  • Audience: This is always api://AzureADTokenExchange.

Here is what it looks like in the Azure Portal:

Image MIFederatedCreds

Once we have the service connection and federated credentials configured, we can use the service connection to authenticate to Azure. You’ll just need to grant your service principal some permissions in the subscription.

How can your organisation start using Workload identity federation

Workload identity federation in now in public preview and turned on by default. If you want to check, you can find the feature flag under the organisation ‘Preview Features’ menu. If it is turned on you’ll see the new Azure Resource Manager Service Connection types. You can find lots more information here.

Image FeatureFlag

How to configure Workload identity federation using the Azure DevOps Terraform provider

The Azure DevOps Terraform provider has been updated to support the creation of Workload identity federation Service Connections. The documentation can be found here.

The following example shows how to setup a Service Connection and User Assigned Managed Identity with Federated Credentials:

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">=3.0.0"
    }
    azuredevops = {
      source = "microsoft/azuredevops"
      version = ">= 0.9.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azuredevops_project" "example" {
  name               = "Example Project"
  visibility         = "private"
  version_control    = "Git"
  work_item_template = "Agile"
  description        = "Managed by Terraform"
}

resource "azurerm_resource_group" "identity" {
  name     = "identity"
  location = "UK South"
}

resource "azurerm_user_assigned_identity" "example" {
  location            = azurerm_resource_group.identity.location
  name                = "example-identity"
  resource_group_name = azurerm_resource_group.identity.name
}

resource "azuredevops_serviceendpoint_azurerm" "example" {
  project_id                             = azuredevops_project.example.id
  service_endpoint_name                  = "example-federated-sc"
  description                            = "Managed by Terraform"
  service_endpoint_authentication_scheme = "WorkloadIdentityFederation"
  credentials {
    serviceprincipalid = azurerm_user_assigned_identity.example.client_id
  }
  azurerm_spn_tenantid      = "00000000-0000-0000-0000-000000000000"
  azurerm_subscription_id   = "00000000-0000-0000-0000-000000000000"
  azurerm_subscription_name = "Example Subscription Name"
}

resource "azurerm_federated_identity_credential" "example" {
  name                = "example-federated-credential"
  resource_group_name = azurerm_resource_group.identity.name
  parent_id           = azurerm_user_assigned_identity.example.id
  audience            = ["api://AzureADTokenExchange"]
  issuer              = azuredevops_serviceendpoint_azurerm.example.workload_identity_federation_issuer
  subject             = azuredevops_serviceendpoint_azurerm.example.workload_identity_federation_subject
}

The items of note are:

  • We have to supply the client id of our service principal in the credentials block service connection, so it knows which service principal is configured with federation.
  • We supply the string WorkloadIdentityFederation in the service_endpoint_authentication_scheme attribute to tell it the type of service connection we want to create.
  • We use the workload_identity_federation_issuer and workload_identity_federation_subject outputs of our service connection to populate the federated credentials. This is a shortcut for getting this information, which you of course get from elsewhere if you prefer.

Once you run this Terraform and create these resources you’ll be ready to use the Service Connection in your Azure DevOps Pipeline.

How to use Workload identity federation in an Azure DevOps Pipeline with the Terraform task

We have updated the two most popular Terraform Tasks to support Workload identity federation, these are:

The following example shows how to use the Microsoft DevLabs task in an Azure DevOps Pipeline:

jobs:
- deployment: deploy
  displayName: Deploy with Terraform
  pool: 
    vmImage: ubuntu-latest 
  environment: dev
  strategy:
    runOnce:
      deploy:
        steps:
        - checkout: self
          displayName: Checkout Terraform Module
        - task: TerraformInstaller@0
          displayName: Install Terraform
          inputs:
            terraformVersion: 'latest'
        - task: TerraformTaskV4@4
          displayName: Terraform Init
          inputs:
            provider: 'azurerm'
            command: 'init'
            workingDirectory: '$(workingDirectory)'
            backendServiceArm: '${{ variables.serviceConnection }}'
            backendAzureRmResourceGroupName: '$(BACKEND_AZURE_RESOURCE_GROUP_NAME)'
            backendAzureRmStorageAccountName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_NAME)'
            backendAzureRmContainerName: '$(BACKEND_AZURE_STORAGE_ACCOUNT_CONTAINER_NAME)'
            backendAzureRmKey: 'terraform.tfstate'
          env:
            ARM_USE_AZUREAD: true # This environment variable tells the backend to use AzureAD auth rather than trying a create a key. It means we can limit the permissions applied to the storage account and container to least priviledge: https://developer.hashicorp.com/terraform/language/settings/backends/azurerm#use_azuread_auth
        - task: TerraformTaskV4@4
          displayName: Terraform Apply
          inputs:
            provider: 'azurerm'
            command: 'apply'
            workingDirectory: '$(workingDirectory)'
            commandOptions: '-auto-approve -var="resource_group_name=$(AZURE_RESOURCE_GROUP_NAME)"'
            environmentServiceNameAzureRM: '${{ variables.serviceConnection }}'
          env:
            ARM_USE_AZUREAD: true

Right now you are probably thinking “how is this any different to what I do now?” and you’d be right to think that because there is no difference. The Workload identity federation implementation is abstracted away into the choice of Service Connection type. The task knows based on the type of Service Connection how to authenticate to Azure and set the relevant environment variables for you.

If you want to know how you can do it yourself outside of the terraform task, you can see an example of using the Azure CLI task here and here.

Conclusion

We’ve shown you how to configure and use Workload identity federation for Azure DevOps, we want you to start using it right away.

If you want a more in depth example, you can refer to this sample repository.

Thanks for reading, feel free to ask questions.

0 comments

Discussion is closed.

Feedback usabilla icon