Azure Workload identity federation with GitHub Actions and user-managed identity

Overview

Usually when running software workload (e.g. script or container-based applications) an identity is involved for authentication and access resources. Software workload running inside Microsoft Azure can use a Service Principal or (User)-Managed Identity. However, when running software workload outside Azure most of the times we have to use credentials (like secrets or certificates) in order to access Azure AD protected resources such as Azure Key Vault or Azure Storage. This can pose a risk since secrets needs to be stored somewhere and regularly rotated to improve security.

Teams or organizations which are using GitHub Actions with Azure can use Workload identity federation to eliminate the use of credentials. How this works is quite easy. To access the Azure AD protected resources a service principal (App Registration) or User managed identity can be used. I see a lot of examples around with a Service Princpal and Federated credentials which is perfectly fine of course. However, in my example I want to use a User managed identity. As the name suggests this identity is fully managed by Azure and can be linked with a lot of other Azure resources without using any credentials. You can follow the steps below to create this workflow.

  1. Configure a GitHub repository to use a GitHub workflow which contains steps to login and access resources in Azure

  2. Create and configure the user managed identity with federated credentials to trust tokens from the external identity provider (IdP), in this case GitHub. Behind the scene Microsoft uses the OpenID Connect (OIDC) protocol

  3. Managed identity gets access to list subscriptions and create/read Azure AD protected resources

💡 Great news for Azure DevOps users! Microsoft announced that the Workload identity federation feature will enter public preview as of Q3 2023. Users can create secret-free service connections in Azure Pipelines to deploy to Azure. I will create a new blog when the feature is live.

Prerequisites

Create and configure GitHub Actions

In order to access the Azure AD protected resources from GitHub Actions a GitHub organization needs to be created. You can follow this guide to create an organization if you haven't already. In this example I will use my personal GitHub account to create an organization for free. Next, I will create a GitHub repository inside the organization.

Click on your profile icon on the upper right -> Your organizations -> Click on the listed organization -> Repositories -> New repository

Below you'll find a GitHub Actions Workflow example to login into Azure and list subscriptions. Push this file to the repository location .github/workflows.

 1# File: .github/workflows/OIDC_workflow.yml
 2
 3name: Run Azure Login with OIDC
 4on:
 5  push:
 6    branches:
 7      - main
 8
 9permissions:
10  id-token: write
11  contents: read
12
13jobs:
14  build-and-deploy:
15    runs-on: ubuntu-latest
16    steps:
17      - name: Az CLI login
18        uses: azure/login@v1
19        with:
20          client-id: ${{ secrets.AZURE_CLIENT_ID }}
21          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
22          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
23  
24      - name: Azure CLI script
25        uses: azure/CLI@v1
26        with:
27          azcliversion: latest
28          inlineScript: |
29            az account show
30            az group list            

Create secrets

The Yaml file contains some secret contexts (recognized as ${{ secrets. }}) stored in the GitHub repository. Looking closer at the Azure login part no client-secret is involved since we are now going to access Azure with a trusted relationship instead of a secret. Add the client-id, tenant-id and subscription-id to GitHub by going to the repository settings. You can find these attributes in Azure as shown below.

Attribute Purpose
client-id The client id of the managed identity in Azure
tenant-id The Azure AD tenant ID
subscription-id The subscription ID where the managed identity resides
Managed_identity_secrets
GitHub
Managed_identity
Azure user-managed identity
AAD
Azure AD tenant id

Create and configure the user managed identity with federated credentials

Once the GitHub organization, repository and workflow is in place it's time to head over to Azure and configure the user-managed identity to create the trusted relationship. Managed identities are free of cost in Azure and it's very easy to create one if it doesn't exist.

Open the managed identity resource and click on Federated credentials in the menu. Now click on Add Credential.

Federated_credentials

Fill in the mandatory fields. In my example I chose Entity Branch to be equal to main which means that the GitHub repository can only use the managed identity if GitHub Actions is using the main branch when authenticating to Azure. Another Entity could be Environment to only give access to a certain GitHub Environment, for example to separate Development and Production deployments.

Add_Federated_Credential-2

Role assignment

In order to use the managed identity to access Azure AD protected resources it should have permissions to read and/or create. For testing purposes we can add the identity as a Contributor to the Azure Subscription where it resides. On the managed identity resource go to Azure role assignments and click Add role assignment and fill in the required fields:

Role_assignment

Sample GitHub Actions workflow

At this point the managed identity contains the necessary permissions to run software workload from GitHub Actions on Azure. Go to GitHub and make a minor change to the repository to trigger the workflow. The console should show the subscription name and resource group in Azure.

Sample_workflow_oidc

 1Run azure/CLI@v1
 2  with:
 3    azcliversion: latest
 4    inlineScript: az account show
 5  az group list
 6  env:
 7    AZURE_HTTP_USER_AGENT: 
 8    AZUREPS_HOST_ENVIRONMENT: 
 9Starting script execution via docker image mcr.microsoft.com/azure-cli:latest
10{
11  "environmentName": "AzureCloud",
12  "homeTenantId": "***",
13  "id": "***",
14  "isDefault": true,
15  "managedByTenants": [],
16  "name": "Azure subscription 1",
17  "state": "Enabled",
18  "tenantId": "***",
19  "user": {
20    "name": "***",
21    "type": "servicePrincipal"
22  }
23}
24[
25  {
26    "id": "/subscriptions/***/resourceGroups/arash-mi-demo",
27    "location": "westeurope",
28    "managedBy": null,
29    "name": "arash-mi-demo",
30    "properties": {
31      "provisioningState": "Succeeded"
32    },
33    "tags": null,
34    "type": "Microsoft.Resources/resourceGroups"
35  }
36]
37az script ran successfully.
38cleaning up container...
39MICROSOFT_AZURE_CLI_1689432037058_CONTAINER