Access Azure Storage from GitHub Actions with a Managed Identity

In this tutorial, we will use OpenID Connect (OIDC) to authenticate GitHub Actions to an Azure Storage Account without storing long-lived client secrets. By establishing a federated credential, GitHub dynamically requests short-lived access tokens from Microsoft Entra ID by proving the workflow originates from a trusted repository, branch, or environment.

1. Create a User-Assigned Managed Identity and its federated credential

Since GitHub Actions runs outside of Azure, you must create a User-Assigned Managed Identity.

Once created, open the new Managed Identity, navigate to Settings -> Federated credentials and add one. Use the following configuration:

2. Assign the RBAC role in your Storage Account

With the identity in place, give it a role on the target Storage Account or container:

3. Store Azure Identifiers in GitHub

You do not need to store sensitive passwords or client secrets. However, the workflow still needs to know which identity and tenant to target. Add these values as GitHub Repository Variables:

All three ids can be found in Azure Portal.

4. Configure the GitHub workflow

Your workflow requires the id-token: write permission to request the OIDC token from Entra ID.

name: Upload to Azure Storage

on:
  push:
    branches:
      - main

permissions:
  id-token: write
  contents: read

jobs:
  upload:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Log in to Azure with OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ vars.AZURE_CLIENT_ID }}
          tenant-id: ${{ vars.AZURE_TENANT_ID }}
          subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

      - name: Upload file to Blob Storage
        run: |
          az storage blob upload \
            --account-name <name-of-your-storage-account> \
            --container-name <name-of-the-container> \
            --name hello.txt \
            --file ./hello.txt \
            --overwrite true \
            --auth-mode login

When it fails

Error Cause
Using auth-type: SERVICE_PRINCIPAL. Not all values are present. Ensure 'client-id' and 'tenant-id' are supplied. One of the ids arrived empty. ${{ vars.X }} never fails on an undefined variable, it just resolves to nothing. Check they are repository variables and not secrets, and check the spelling.
AADSTS70021: No matching federated identity record found The Subject claim does not match. A push to main sends repo:ORG/REPO:ref:refs/heads/main.
403 AuthorizationPermissionMismatch Login worked, authorization did not. The identity is missing Storage Blob Data Contributor.
ContainerNotFound The command does not create the container. Create it first.

Best Practices

comments powered by Disqus