Azure Workload Identity Federation


Workload identity is an overloaded term. If I asked 10 people for a definition I’d likely get 10 different answers.

It’s that Kubernetes identity thing, isn’t it?

Yes, kind of.

It’s a license in Entra, right?

Yes. It was renamed though, but you should consider getting Premium.

It’s a general term for identities associated with a software workload.

Ding ding ding, that’s my kind of definition!

They’re all correct, and there are more correct answers too (which makes it only slightly confusing), but today we’ll take a look at what people usually mean, with examples in both Azure DevOps Pipelines and GitHub Actions using my module AzAuth.

Federated or Client Credentials?

When people say workload identity, what they usually mean is the pattern of workload identity federation, regardless of the somewhat agreed-upon definition in the introduction of this post.

When people say workload identity federation, what they actually mean is a client credentials authentication flow with a federated credential.

If “federated credential” sounds familiar, you may have either been on the internet in the past 6 months (because everything is starting to support them), or seen them on your service principals or user-assigned managed identities, which is where we can enable this flow.

For user-assigned managed identities it’s the only credential option available.

For service principals it’s right next to the certificates and client secrets.

Service Principal Credentials

While those seem like very different credentials, they have more in common than being in the same menu in the Azure Portal. All three credential types use the same client credentials authentication flow, just different versions.

If you want a better understanding of authentication in Azure, I wrote a post about the fundamentals and the different authentication flows.

Trusting External Platforms

Entra implements an extension to OAuth 2.0 called Open ID Connect (OIDC). If an external platform also implements OIDC, we can use the pattern of workload identity federation to configure a workload identity in Entra to trust a specific identity in an external platform.

A common use case for this is to manage Azure resources from external platforms such as:

Compared to other authentication methods this is incredibly useful, because:

Authentication Flow Overview

Azure DevOps is used as an example in the illustration below, but it could be any external platform implementing OIDC assuming their tokens match the client assertion format.

sequenceDiagram participant Pipeline as Pipeline 🚀 participant AZDO as Azure DevOps ☁️ participant Entra as Entra ID ☁️ Pipeline->>AZDO: ID Token request for Pipeline
Sends Access Token AZDO->>Pipeline: ID Token response Pipeline->>Entra: Auth request for Workload Identity
Sends ID Token note left of Entra: Workload Identity has a
Federated Credential configured
for the Azure DevOps Pipeline Entra-->>Entra: Verify ID Token
Subject and Issuer Entra->>AZDO: Sends ID Token AZDO->>Entra: Validates ID Token Entra->>Pipeline: Workload Identity Access Token response
  1. The pipeline in Azure DevOps requests a token to prove its identity.
  2. Azure DevOps answers with an ID token for the pipeline.
  3. The pipeline asks for an access token from Entra, providing its ID token.
  4. Entra checks if trust exists between the workload identity and the pipeline.
  5. If trust exists, it asks Azure DevOps (issuer) to validate it.
  6. Azure DevOps confirms that it’s a valid token for the pipeline (subject).
  7. Entra issues a new access token for the workload identity.

Once the token exchange has been made, the pipeline is free to act as the workload identity and operate on Azure resources.

Configuring a Federated Credential

If we configure our credential through the Azure Portal, we’re given some help regarding the different scenarios.

Federated Credential Scenarios

All options except “Other issuer” are just ways to help you fill out the information. In fact, you could use that last option for every scenario if you wanted to.

Selecting that option presents us with all the fields that we actually need to set. If we would do it using the API instead, this is the same information we would need to specify.

Federated Credential for Other Issuer

Let’s take a look at examples first for Azure DevOps Pipelines and then GitHub Actions.

Azure DevOps Setup

Taking a pipeline in Azure DevOps as an example scenario, we actually need to configure it from two ways. Not only the federated credential, but we also need to create and configure a service connection in our Azure DevOps organization to use it.

Create Service Connection

If we have the access to create the identity in Entra, we can use the automatic option and let Azure DevOps create a new identity and the federated credential for us.

Automatic Service Connection with Workload Identity Federation

Otherwise we’ll have to create the service connection and credential manually, which we do in a few different steps.

First give the service connection a name and an optional description.

Step 1 of Manual Service Connection with Workload Identity Federation

In the second step we get the information that we need to configure our federated credential for our identity in Entra.

Step 2 of Manual Service Connection with Workload Identity Federation

Copy or write down the issuer and subject identifier for now, then head back and fill out your federated credential with that information. Save the service connection meanwhile as a draft if you need to.

Federated Credential for Azure DevOps

Save the federated credential once you’ve entered the information, and then head back to your service connection to finish the setup. You’ll need the name and id of your subscription or management group (or machine learning workspace), the client id for your identity, and the id of your tenant.

When you save the service connection, Azure DevOps is going to reach out to Entra for a handshake to finish the configuration and ensure that the trust is established.

NOTE: If you’re using conditional access policies to restrict which IP addresses service principals can authenticate from (using the Workload ID Premium features), you need to make a temporary change to allow Azure DevOps to authenticate for the initial handshake from the IP addresses in the region of your organization, even if you’re using self-hosted agents since this request is not made from a pipeline agent but from the Azure DevOps service itself.

Before having a look at how to use the workload identity from a pipeline, let’s take a look at how to configure a federated credential for GitHub.

GitHub Actions Setup

When choosing scenario for our federated credential in the Azure Portal, GitHub is one of the scenarios where we’re given a bit of extra help to configure it.

Federated Credential for GitHub

As mentioned earlier, we could still configure it with the same information in the “Other issuer” scenario, but this way we get some extra clarity regarding the options.

The main thing to think about is where how GitHub Action will run. Will it run on a specific branch, on a pull request, in a specific environment? This determines the subject of the id token once the workflow retrieves it, and it has to match our federated credential.

Compared to the whole service connection dance for Azure DevOps, GitHub has fewer parts to setup, so let’s move on to how the pipeline code might look for the platforms.

Using a Federated Credential

Once we’ve set up the federated credential on our identity, we can use it from the platform we’ve configured. We could add up to 20 federated credentials on a single identity, so technically we could set up one single workload identity that could be used from both Azure DevOps and GitHub at the same time.

Azure Pipeline

When we configure our pipelines in Azure DevOps it’s advantageous to do so using yaml, since it lets us version control it together with the rest of our code.

A simple pipeline to get a token using AzAuth in PowerShell could look like this:

trigger:
- main

pool:
  vmImage: ubuntu-latest

steps:
- task: AzurePowerShell@5
  env:
    SYSTEM_ACCESSTOKEN: $(System.AccessToken)
  inputs:
    azureSubscription: '<my-service-connection>'
    pwsh: true
    azurePowerShellVersion: 'LatestVersion'
    ScriptType : 'InlineScript'
    Inline: |
      # These are here as an example, and would be better set as parameters or variables for your pipeline
      $ClientId = '<client-id>'
      $TenantId = '<tenant-id>'

      # An improvement would be to not install the module each run
      Install-Module AzAuth

      # The service connection ID can be found stored in any of the environment variables named ENDPOINT_DATA_<ServiceConnectionId>_<Something>URL
      try {
          $ServiceConnectionId = (Get-ChildItem -Path Env: -Recurse -Include ENDPOINT_DATA_*)[0].Name.Split('_')[2]
      } catch {
          throw "Unable to determine service connection ID!"
      }
      
      # Set up a URI for the Azure DevOps API endpoint to get the ID token from
      $Uri = "${env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI}${env:SYSTEM_TEAMPROJECTID}/_apis/distributedtask/hubs/build/plans/${env:SYSTEM_PLANID}/jobs/${env:SYSTEM_JOBID}/oidctoken?serviceConnectionId=${ServiceConnectionId}&api-version=7.1-preview.1"      
    
      $InvokeSplat = @{
        'Uri' = $Uri
        'Method' = 'POST'
        'Headers' = @{
          'Authorization' = "Bearer $($env:SYSTEM_ACCESSTOKEN)"
        }
        'ContentType' = 'application/json'
      }
      $OidcToken = (Invoke-RestMethod @InvokeSplat).oidcToken

      $AccessToken = Get-AzToken -WorkloadIdentity -ExternalToken $OidcToken -ClientId $ClientId -TenantId $TenantId

Worth noting is that we’re only able to utilize our service connections from certain tasks that are enabled for it. In this case Get-AzAccessToken from the Az.Accounts module which is already available would work just as well and save us some code, but I think there’s some value in showing the full process of getting the id token as well.

To follow the process step by step, we first need to be able to utilize the access token for our pipeline, available to our task through the SYSTEM_ACCESSTOKEN environment variable. Using this token, we can request an OIDC token, or id token, for our specific pipeline. This token is the one we need to exchange with Entra for an access token for our workload identity, which we do using the AzAuth module.

GitHub Action

GitHub Actions are also written using yaml, although a slightly different syntax.

Below is an embedded existing workflow for my module AzAuth where I use workload identity federation to test the module automatically when a new pull request is made to the module.

It’s set up so that all pull requests are able to use the secrets in the repository, but only after my approval so that no secrets are exposed to malicious code. It has two jobs, inspired by this blog post.

The first one allows me to enforce an approval gate before the workflow continues.

The second one is where the workload identity federation comes in. In GitHub we need to ensure that the pipeline token has permission to request an OIDC, or id token, done by setting id-token to write under permissions. This can be done on different scopes, in this case I make sure to only set it for the second job.

Then we can use the environment variables ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN to request an id token for our workflow. Once we have that we can request an access token for our workload identity from Entra using AzAuth, assuming that we configured the federated credential correctly, in this case by pull request.

Conclusion

If you haven’t taken a look at using workload identity federation for your pipelines or workflows, or even Kubernetes workloads, now is the time! It’s easier than ever to get started, and it cuts down on a lot of the less secure parts of authentication, like managing passwords and keys.

It even allows you to extend and explore new patterns of authentication between your services, and is in my opinion one of the most elegant solutions for many common cross-service workflows.

Comments

comments powered by Disqus