Azure Workload Identity Federation and GitHub Actions
Massimo Bonanni
Posted on May 2, 2023
The hard life of a GitHub Action
GitHub Actions (documentation here) can be used to create entire environment in Azure and, in general, interact with the Azure platform.
But GitHub Actions run outside your Azure tenant (your "kingdom") and, for this reason, must be authenticated by your Azure Active Directory, and like all applications that run outside your tenant, they can use a Service Principal to be recognized.
In the beginning is the Service Principal
The first step you need to do to secure your GitHub Actions in Azure, is to create a Service Principal.
You have different ways to do that, but if you want to use the Azure portal, you can start from Active Directory page, choose the "App registration" blade and the "New registration" button:
Azure portal opens the "Register an application" page and the only think you have to do is give a name to your Service Principal and click on the "Register" button.
In a minute, you will have your Service Principal and the portal shows you its "Overview" page where you can find some useful information to configure your GitHub Actions.
Unfortunately, the service principal alone is not enough to achieve our goal. We need to create a secret that our Actions will need to know in order to authenticate, and a RBAC role assignment (more info here) for the Service Principal to have the right permissions to manage resources within our subscriptions.
Both these operations are easy to do: to generate a secret for the service principal, you need to click on the "Certificates & secrets" blade, open the "Client secrets" tab and click on the "New client secret" button.
To add the new secret, you just edit the description (I suggest using a description that is short but explains what use you are creating the secret for) and the duration.
After the secret creation remember to take notes of its value, because it accessible only immediately after the creation.
A secret has the bad (or good if we look at it from a security point of view) habit of having a deadline. The maximum duration that you can select is equal to two years and, obviously, you have to make sure that whoever uses the Service Principal for be recognized (in our case the GitHub Action) always has the updated secret.
In short, the secret must be managed and this could involve a great deal of work if there are many Actions that use it or if we have many Service Principals.
This is the reason why, for example, you use the Managed Identities
when you want to secure the interaction between two services in Azure....but your GitHub Actions aren't in Azure!! 😟
The triad of values ApplicationID
, TenantID
and SecretValue
are the Service Principal credentials you need in your GitHub Action.
Using secrets in GitHub Actions
Before showing you how to avoid using the secret in your GitHub Actions thanks to Federated Workload identities, let's see what are the steps to allow the GitHub Actions to interact with Azure thanks to the Service Principal's credentials.
Take a look of the following simple GitHub Action:
name: WIF App registration with Secret
on:
workflow_dispatch
env:
LOCATION: "northeurope"
RESOURCE_GROUP_NAME: "WIF-AppReg-Secret"
SUBSCRIPTION_ID: "********-****-****-****-************"
jobs:
job01:
runs-on: ubuntu-latest
steps:
- uses: Azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
allow-no-subscriptions: true
- name: Create resource group
uses: azure/CLI@v1
with:
inlineScript: |
az group create --location ${{ env.LOCATION }} --name ${{ env.RESOURCE_GROUP_NAME }} --subscription ${{ env.SUBSCRIPTION_ID }}
This Action uses two actions to open an authenticated session in Azure (Azure/login@v1
) and run a simple Azure CLI command (azure/CLI@v1
) to create a resource group.
To open an authenticated session in Azure, you need the credentials and credentials are represented by the previous triad ApplicationID
, TenantID
and SecretValue
we mentioned earlier.
You must create the following JSON (substitute the right ids in the clientId
, clientSecret
and tenantId
properties):
{
"clientId": "********-****-****-****-************",
"clientSecret": "Lsu************************bAe",
"tenantId": "********-****-****-****-************"
}
And create an Action Secret called AZURE_CREDENTIALS
that contains the previous JSON as shows in the following picture:
It is easy but...the clientSecret
property must be updated (actually the entire secret in GitHub must be updated because secrets, in GitHub, cannot be modified but only overwritten) every time the secret in the app registration changes. And, finally, you must take care about clientSecret
(and also clientId
and tenantId
) because if someone stoles those values, he can be impersonate your app registration and it isn't a good think!!
Workload Identity federation
This approach was born to trust tokens from external identity provider, such as GitHub or Google (or other in the future).
You first create a relationship between the identity (that can be a managed identity or an App registration) and the external identity provider.
Once this relationship is created, every time the workload wants to authenticate itself against AzureAD, it retrieves a token from the external IdP and, uses it to request access token from AAD.
It is not magic, behind the scene, AzureAD uses OpenID Connect (more info here).
GitHub Actions is one of the scenario you can leverage this approach.
To create the relationship between your App Registration and GitHub, you can open the "Certificates & secrets" blade for the App registration (as you did in the full credentials approach) and, instead create a secret, you use the "Federated credentials" tab.
The "Add credential" button opens the configuration page for the trust:
In the previous page, you must configure the repo you want to trust in terms of organization (or username) and repo name.
Then you need to choose the entity you want to trust and you can choose from Environment
, Branch
, Pull request
or Tag
. The value you choose must the configuration of your GitHub Actions. For example, if you action will start every time someone you push a new code in the main branch, you choose branch
entity type and insert main
in the branch name in the configuration.
The value you use for entity type will use by the OIDC flow to filer the scope of the OIDC requests.
Once you create the trust relationship, you can add clientId
and tenantId
values as action secrets in the repo and write the following action:
name: WIF App registration with federation
permissions:
id-token: write # This is required for requesting the JWT
contents: read # This is required for actions/checkout
on:
push:
branches:
- main
env:
LOCATION: "northeurope"
RESOURCE_GROUP_NAME: "WIF-AppReg-Federation"
jobs:
job01:
runs-on: ubuntu-latest
steps:
- uses: Azure/login@v1
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: Creat resource group
uses: azure/CLI@v1
with:
inlineScript: |
az group create --location ${{ env.LOCATION }} --name ${{ env.RESOURCE_GROUP_NAME }}
This action does exactly the same operations of the previous one, but here you haven't any clientSecret
to manage.
NOTE: Keep attention to the permissions in the previous action. They are mandatory if you want that the action could retrieve token from the IdP. if you remove them, you receive an error!!
Posted on May 2, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.