SpyglassMTG Blog

  • Blog
  • Understanding GitHub OIDC Tokens for Cloud Deployments

Understanding GitHub OIDC Tokens for Cloud Deployments

Understanding GitHub OIDC Tokens for Cloud Deployments

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
  1. Eliminates Static Secrets: No need to store long-lived API keys or passwords in GitHub secrets. This reduces attack surfaces and simplifies secret management.
  2. Improved Security Posture: Tokens are short-lived and scoped, minimizing the impact of compromised credentials.
  3. Fine-Grained Access Control: Cloud providers can issue credentials with precise permissions based on conditions like repository, branch, or workflow.
  4. 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.

See: https://docs.github.com/en/actions/how-tos/write-workflows/choose-what-workflows-do/run-job-variations

[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.

 

AI Exploring SQL RAG: Revolutionizing GenAI with SQL Generation