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:
- Federated credential scenario:
GitHub Actions deploying Azure resources - Issuer:
https://token.actions.githubusercontent.com - Organization: The name of your GitHub Organization
- Organization ID: The numerical ID. Can be found in
https://api.github.com/users/<organization> - Repository: The name of your repository
- Repository ID: The numerical ID. Can be found in
https://api.github.com/repos/<organization>/<repository>, which needs an authenticated request if the repository is private - Entity: Pick between Environment, Branch, Pull Request or Tag
- Audience:
api://AzureADTokenExchange - Subject identifier: automatically created based on the previous information given
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:
- Storage Blob Data Contributor for read/write access.
- Storage Blob Data Reader for read-only access.
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:
AZURE_CLIENT_ID, which is the Client ID of your User-Assigned Managed IdentityAZURE_TENANT_IDAZURE_SUBSCRIPTION_ID
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
- Apply the principle of least privilege: For example, in our Storage Account case, scope the RBAC role directly to the specific storage container rather than the entire storage account or subscription.
- Use Environment Protection: Tie federated credentials to specific GitHub Environments (e.g., prod) rather than branches, so you can enforce manual approval gates before deployment.