Modern DevOps workflows increasingly rely on secure, automated authentication between CI/CD pipelines and cloud providers. GitHub’s OpenID Connect (OIDC) integration offers a powerful way to achieve this without hardcoding long-lived credentials in your repositories.
What Are GitHub OIDC Tokens?
OIDC tokens allow GitHub Actions to authenticate directly with cloud providers using short-lived, dynamically generated credentials. Instead of storing secrets in GitHub, your workflow exchanges an OIDC token for temporary credentials from the cloud provider. This approach aligns with zero-trust principles and significantly reduces the risk of credential leaks.
Benefits of Using OIDC for Cloud Deployments
- Eliminates Static Secrets: No need to store long-lived API keys or passwords in GitHub secrets. This reduces attack surfaces and simplifies secret management.
- Improved Security Posture: Tokens are short-lived and scoped, minimizing the impact of compromised credentials.
- Fine-Grained Access Control: Cloud providers can issue credentials with precise permissions based on conditions like repository, branch, or workflow.
- Compliance and Auditability: Dynamic credentials make it easier to meet compliance requirements and provide clear audit trails.
Limitations: Long-Running Actions
While OIDC tokens are excellent for short-lived workflows, they have a time-bound nature. Most tokens expire within minutes, and the temporary cloud credentials they generate often last for an hour or two. This creates challenges for:
- Long-running jobs (e.g., data processing, machine learning training).
- Workflows that require persistent access beyond the token’s lifespan.
If your deployment or job exceeds these time limits, the token and associated credentials will expire, causing failures mid-process.
How to Overcome the Short-Lived Limitation
For GitHub actions, you have a few options:
Matrix Strategy:
Examine your workflow to see if using a matrix strategy can reduce the execution time. Consider also including the login step within the matrix job can further ensure the credential doesn’t expire.
|
[YAML] azure-deploy: runs-on: ubuntu-latest needs: detect-changes if: needs.detect-changes.outputs.matrix != '[]' strategy: matrix: include: $ fail-fast: false
|
Recurrent Login Steps:
If there are steps in your workflow known to be long running with subsequent steps that may be impacted later, add a login action between those steps.
|
// [YAML] steps: - name: Checkout code uses: actions/checkout@v4 with: token: $ - name: Azure login uses: azure/login@v2 with: client-id: $ tenant-id: $ subscription-id: $ - name: Deploy Workload 1 uses: ./.github/actions/deploy-workload id: deploy-1 with: target-environment: $ workload: $-1 - name: Azure login uses: azure/login@v2 with: client-id: $ tenant-id: $ subscription-id: $ - name: Deploy Workload 2 uses: ./.github/actions/deploy-workload id: deploy-2 with: target-environment: $ workload: $-2 |
Programmatic Refresh:
When the previous approaches don’t achieve the required goal, as in the case of a long-running script action, you can programmatically refresh your credentials.
On the surface this sounds simple, but in practice it can be tricky to get it working reliably. Our solution is a PowerShell module that can be imported into the script environment during the workflow execution that can be used to refresh the credentials.
Consider the PowerShell script below that deploys a set of workloads to Azure. It first imports the login module, then it loops through the workloads, refreshing the login credentials on each pass.
|
# [PowerShell] Param ( [array]$workloads = $ENV:WORKLOADS.split(',') ) Begin { $InformationPreference = 'Continue'; Write-host $('=' * 80); Write-Information "Workloads:`t$($workloads -join ', ')"; Write-Information "Workload Count:`t $($workloads.count)"; Import-Module $PSScriptRoot/GitHubOIDCLogin.psm1 -DisableNameChecking; Write-host $('=' * 80); } Process { foreach ($workload in $workloads) { # Refresh the OIDC token before each workload deployment Refresh-AzureLogin -ForceRefresh; # Deploy workload Deploy-Workload -workloadName $workload } } End {}
|
This script would be included in the GitHub action workflow as a script step. Be sure to include the id-token permissions in the workflow.
|
// [YAML] permissions: id-token: write contents: read
jobs: deploy-infrastructure: runs-on: ubuntu-latest environment: $ env: AZURE_CLIENT_ID: $ AZURE_TENANT_ID: $ steps: - name: Checkout code uses: actions/checkout@v4 with: submodules: true token: $ // Be sure to include this step as the PS Module requires it - name: Azure login uses: azure/login@v2 id: azurelogin with: client-id: $ tenant-id: $ subscription-id: $
- name: Deploy Workloads - name: "Deploy Workloads" id: deploy shell: pwsh env: WORKLOADS: $ run: | # Execute deployment script $scriptPath = "$/scripts/deploy-workload.ps1" if (Test-Path $scriptPath) { & $scriptPath
} else { Write-Error "Deployment script not found at: $scriptPath" exit 1 }
|
Lastly, the PowerShell module that makes this all work:
|
function Get-GitHubOIDCToken { <# .SYNOPSIS Gets an OIDC token from GitHub Actions #>
try { # Validate required environment variables if (-not $env:ACTIONS_ID_TOKEN_REQUEST_TOKEN) { throw "ACTIONS_ID_TOKEN_REQUEST_TOKEN environment variable is not set" } if (-not $env:ACTIONS_ID_TOKEN_REQUEST_URL) { throw "ACTIONS_ID_TOKEN_REQUEST_URL environment variable is not set" }
$headers = @{ Authorization = "Bearer $env:ACTIONS_ID_TOKEN_REQUEST_TOKEN" } $oidcUrl = "$env:ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange"
$response = Invoke-RestMethod -Uri $oidcUrl -Headers $headers -Method Get -ErrorAction Stop
if (-not $response.value) { throw "Failed to retrieve OIDC token from GitHub" }
return $response.value } catch { Write-Error "Failed to get GitHub OIDC token: $($_.Exception.Message)" throw } }
function Login-AzureWithOIDC { <# .SYNOPSIS Logs into Azure using GitHub OIDC token .PARAMETER ClientId The Azure AD application client ID (defaults to AZURE_CLIENT_ID env var) .PARAMETER TenantId The Azure AD tenant ID (defaults to AZURE_TENANT_ID env var) .PARAMETER SubscriptionId The Azure subscription ID (defaults to AZURE_SUBSCRIPTION_ID env var) #> param ( [string]$ClientId = $env:AZURE_CLIENT_ID, [string]$TenantId = $env:AZURE_TENANT_ID, [string]$SubscriptionId = $env:AZURE_SUBSCRIPTION_ID )
try { # Validate required parameters if (-not $ClientId) { throw "ClientId is required. Set AZURE_CLIENT_ID environment variable or pass ClientId parameter." } if (-not $TenantId) { throw "TenantId is required. Set AZURE_TENANT_ID environment variable or pass TenantId parameter." }
Write-Information "Getting the OIDC token..." -InformationAction Continue $oidcToken = Get-GitHubOIDCToken
Write-Information "Logging in to Azure using service principal with OIDC..." -InformationAction Continue
# Use direct service principal login with OIDC token instead of access token # This is more secure and the recommended approach for GitHub Actions $loginResult = az login --service-principal ` --username $ClientId ` --tenant $TenantId ` --federated-token $oidcToken ` --output json 2>&1
if ($LASTEXITCODE -ne 0) { Write-Error "Azure CLI login failed. Output: $loginResult" throw "Azure CLI login failed with exit code $LASTEXITCODE" }
Write-Information "Azure login successful using OIDC token." -InformationAction Continue
# Set subscription if provided if ($SubscriptionId) { Write-Information "Setting subscription context to: $SubscriptionId" -InformationAction Continue $subscriptionResult = az account set --subscription $SubscriptionId 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error "Failed to set subscription. Output: $subscriptionResult" throw "Failed to set subscription $SubscriptionId" } Write-Information "Subscription context set successfully." -InformationAction Continue }
# Verify login by getting current account info $accountInfo = az account show --output json 2>&1 | ConvertFrom-Json if ($LASTEXITCODE -eq 0 -and $accountInfo) { Write-Host "✅ Azure login successful!" -ForegroundColor Green Write-Host " Tenant: $($accountInfo.tenantId)" -ForegroundColor Gray Write-Host " Subscription: $($accountInfo.name) ($($accountInfo.id))" -ForegroundColor Gray Write-Host " User: $($accountInfo.user.name)" -ForegroundColor Gray } else { throw "Failed to verify Azure login status" } } catch { Write-Error "Failed to login to Azure: $($_.Exception.Message)" throw } }
function Test-AzureLogin { <# .SYNOPSIS Tests if Azure CLI is currently logged in and the token is valid #> try { $accountInfo = az account show --output json 2>$null | ConvertFrom-Json return ($LASTEXITCODE -eq 0 -and $accountInfo) } catch { return $false } }
function Refresh-AzureLogin { <# .SYNOPSIS Refreshes Azure login if needed (for workflows longer than 55 minutes) .PARAMETER ClientId The Azure AD application client ID (defaults to AZURE_CLIENT_ID env var) .PARAMETER TenantId The Azure AD tenant ID (defaults to AZURE_TENANT_ID env var) .PARAMETER SubscriptionId The Azure subscription ID (defaults to AZURE_SUBSCRIPTION_ID env var) .PARAMETER ForceRefresh Force refresh even if current login appears valid #> param ( [string]$ClientId = $env:AZURE_CLIENT_ID, [string]$TenantId = $env:AZURE_TENANT_ID, [string]$SubscriptionId = $env:AZURE_SUBSCRIPTION_ID, [switch]$ForceRefresh )
try { if (-not $ForceRefresh) { Write-Information "Checking current Azure login status..." -InformationAction Continue if (Test-AzureLogin) { Write-Information "Azure login is still valid, no refresh needed." -InformationAction Continue return } }
Write-Information "Refreshing Azure login..." -InformationAction Continue Login-AzureWithOIDC -ClientId $ClientId -TenantId $TenantId -SubscriptionId $SubscriptionId Write-Host "✅ Azure login refreshed successfully!" -ForegroundColor Green } catch { Write-Error "Failed to refresh Azure login: $($_.Exception.Message)" throw } }
Export-ModuleMember -Function Login-AzureWithOIDC, Test-AzureLogin, Refresh-AzureLogin
|
Final Thoughts
While the use of OIDC tokens within your workflows is the preferred option for access to cloud resources, there are potentially some challenges in long running jobs.
Using the approaches detailed here should provide you with the ability to overcome those challenges in most situations.