Speeding up GitHub Workflow Federated authentication to the Azure Platform
This week we were running into issues because our pipelines in GitHub Actions were taking too long to authenticate to the Azure platform using the azure/login action with federated credentials. Initially it took around 25-30 seconds to authenticate, but over time this increased to over 3-4 minutes, which is not acceptable in our CI/CD pipelines.
So I decided to investigate the issue and found a workaround that significantly speeds up the authentication process. In this blog post, I’ll share how I managed to reduce the authentication time from several minutes to just a few seconds.
Why
We have several GitHub Actions workflows that deploy resources to Azure. To authenticate securely, we use the azure/login action with federated credentials, which allows us to avoid storing long-lived secrets in GitHub. However, we noticed that the authentication process was taking an unacceptable long time, which was delaying our deployments. In some cases we do matrix jobs which logon to several customers, these minutes really add up in total runtime of the pipeline.
Investigation
I started by looking into familair options for me, I knew Emanual Palm created a PowerShell module to logon without azure/login. This worked great, but unfortunately in some customers approval for external modules/actions take time or are plainly not allowed (outside of Microsoft).
So I wondered if there any native methods to logon to Azure using federated credentials without the overhead of the azure/login action.
I found in Azure PowerShell it’s Connect-AzAccount supports an argument -FederatedToken, which allows you to pass a JWT token of a federated identity provider, in our case GitHub Actions. This is exactly what azure/login function does for us under the hood, but it seems to have less overhead.
Solution
An example of how the Connect-AzAccount with these details looks like:
1$arguments = @{
2 Tenant = $env:AZURE_TENANT_ID
3 SubscriptionId = $env:AZURE_SUBSCRIPTION_ID
4 FederatedToken = $env:AZURE_FEDERATED_TOKEN
5 ApplicationId = $env:AZURE_CLIENT_ID
6}
7Connect-AzAccount @argumentsAzure PowerShell
I borrowed the following workflow example from Emanuel his blog post, definitely check his excellent post out for more details and explanation how this actually works at: Azure Workload Identity Federation.
If I integrate this into a demo GitHub Actions workflow, it looks like this:
1name: Test Workload Identity authentication
2
3on: workflow_dispatch
4
5jobs:
6 test:
7 environment: My-Azure-Environment # Make sure you set safeguards to your environments like branch requirements
8 permissions:
9 id-token: write
10 runs-on: ubuntu-latest
11 steps:
12 - name: Get token
13 shell: pwsh
14 env: # These can stay the same as in azure/login
15 TENANT_ID: ${{ vars.TENANT_ID }}
16 CLIENT_ID: ${{ secrets.APP_ID }}
17 SUBSCRIPTION_ID: ${{ secrets.SUB_ID }}
18 run: |
19 # As blogged by Emanuel Palm: https://pipe.how/get-oidctoken, slightly modified
20 $Url = $env:ACTIONS_ID_TOKEN_REQUEST_URL
21 $Params = @{
22 Uri = "$Url&audience=api://AzureADTokenExchange"
23 Authentication = 'Bearer'
24 Token = $env:ACTIONS_ID_TOKEN_REQUEST_TOKEN | ConvertTo-SecureString -AsPlainText -Force
25 }
26 # Get the OIDC token from GitHub Actions and use it to authenticate to Azure
27 $arguments = @{
28 Tenant = $env:TENANT_ID
29 SubscriptionId = $env:SUBSCRIPTION_ID
30 ApplicationId = $env:CLIENT_ID
31 FederatedToken = (Invoke-RestMethod @Params).value
32 }
33 # Unfortunately Az.Accounts is not installed by default on GitHub runners, so we need to install it first
34 # We could cache the module or use ModuleFast from Justin Grote, but for simplicity in this example, this also works
35 Install-Module -Name Az.Accounts -Force -Scope CurrentUser
36 Import-Module -Name Az.Accounts
37
38 Connect-AzAccount @argumentsAzure CLI
Ofcourse I haven’t forgotten the Azure CLI lovers out there. You can achieve the same with the Azure CLI using the az login command with the --federated-token argument:
1name: Test Workload Identity authentication
2
3on: workflow_dispatch
4
5jobs:
6 test:
7 environment: My-Azure-Environment # Make sure you set safeguards to your environments like branch requirements
8 permissions:
9 id-token: write
10 runs-on: ubuntu-latest
11 steps:
12 - name: Get token
13 shell: pwsh
14 env:
15 TENANT_ID: ${{ vars.TENANT_ID }}
16 CLIENT_ID: ${{ secrets.APP_ID }}
17 SUBSCRIPTION_ID: ${{ secrets.SUB_ID }}
18 run: |
19 $Url = $env:ACTIONS_ID_TOKEN_REQUEST_URL
20 $Params = @{
21 Uri = "$Url&audience=api://AzureADTokenExchange"
22 Authentication = 'Bearer'
23 Token = $env:ACTIONS_ID_TOKEN_REQUEST_TOKEN | ConvertTo-SecureString -AsPlainText -Force
24 }
25 $OidcToken = (Invoke-RestMethod @Params).value
26 az login --federated-token $OidcToken --service-principal --username $env:CLIENT_ID --tenant $env:TENANT_IDImproved Azure PowerShell
In our real-world scenario, we also added caching of the Az.Accounts module to speed up the logon process.
1 test:
2 environment: My-Azure-Environment # Make sure you set safeguards to your environments like branch requirements
3 permissions:
4 id-token: write # This is required for requesting the ID token of the pipeline
5 contents: read
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v5
9 - name: Install and cache PowerShell modules
10 uses: potatoqualitee/psmodulecache@v6.2.1
11 with:
12 modules-to-cache: Az.Accounts
13 - name: Get token
14 shell: pwsh
15 env:
16 TENANT_ID: ${{ vars.TENANT_ID }}
17 CLIENT_ID: ${{ secrets.APP_ID }}
18 SUBSCRIPTION_ID: ${{ secrets.SUB_ID }}
19 run: |
20 $Url = $env:ACTIONS_ID_TOKEN_REQUEST_URL
21 $Params = @{
22 Uri = "$Url&audience=api://AzureADTokenExchange"
23 Authentication = 'Bearer'
24 Token = $env:ACTIONS_ID_TOKEN_REQUEST_TOKEN | ConvertTo-SecureString -AsPlainText -Force
25 }
26
27 $arguments = @{
28 Tenant = $env:TENANT_ID
29 SubscriptionId = $env:SUBSCRIPTION_ID
30 ApplicationId = $env:CLIENT_ID
31 FederatedToken = (Invoke-RestMethod @Params).value
32 }
33 Connect-AzAccount @argumentsResults
I combined the test in a single pipeline for both logons, and the results were impressive:
gh run view 1234567
✓ main Test Workload Identity authentication WeAreInSpark/OurFantasticRepository#172 · 1234567
Triggered via workflow_dispatch about 2 minutes ago
JOBS
✓ test in 13s (ID 123456)
✓ test2 in 11s (ID 123456)As you can see, the Azure PowerShell logon took only 13 seconds, and the Azure CLI logon took just 11 seconds. This is a significant improvement compared to the previous several minutes it took using the azure/login action. I hope this helps others facing similar issues with federated authentication in GitHub Actions. Happy coding!
#github workflows #github actions #azure #azure/login #federated credentials