Back to Cheat Sheets

🏗️ Insufficient PBAC

OWASP Top 10 CI/CD Security Risks - CICD-SEC-5

HIGH RISK

📋 What Is It?

Pipeline-Based Access Controls (PBAC) are the permissions granted to the environment a pipeline runs in - the runner or agent and the identity, secrets, and network reach it carries during a job. Insufficient PBAC is when that execution environment holds access far beyond what the specific job needs: a compile step that can read every org secret, assume a wildcard cloud role, reach the production control plane, and leave artefacts for the next job on the same machine. Because a pipeline is a programmable execution surface, the runner's privileges become the job's privileges - whatever code lands there inherits them.

CICD-SEC-5 OWASP Rank
Blast Radius Focus Area
Ephemeral Beats Reused

⚠️ Common Exploits

  • All Secrets in Every Job: A compile step can read the production deploy key it never needed
  • Standing Broad Cloud Role: A wildcard IAM role on the runner = one build compromises the account
  • Non-Ephemeral Runners: Leftover creds, caches, and workspaces harvested by the next job
  • Shared Runners Across Trust: Fork PRs run on the same pool as production-credentialed pipelines
  • Control-Plane Reach: Build jobs can hit the metadata service, k8s API, or prod DB
  • Cache / Artifact Poisoning: Low-trust jobs write what high-trust jobs consume unverified
  • Runner Implants: Trojaned toolchain on a reused runner hits every later job
  • Container Escape: Mounted Docker socket / privileged container reaches the host

🔴 Attack Flow

1. Get code running (PR build, dependency, poisoned test/script)
↓
2. Enumerate the runner's access (env, ~/.aws, metadata, network)
↓
3. Abuse standing privilege (read all secrets, assume broad role)
↓
4. Reach systems the build should never touch (prod, control plane)
↓
5. IMPACT: Cloud-account takeover, no application bug required!

❌ Vulnerable Code

# .github/workflows/ci.yml on: pull_request_target: # runs in base context WITH secrets permissions: write-all # token can write everything env: # every secret handed to every step AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} # long-lived static key AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} PROD_DEPLOY_KEY: ${{ secrets.PROD_DEPLOY_KEY }} SIGNING_GPG_KEY: ${{ secrets.SIGNING_GPG_KEY }} jobs: build-test-deploy: runs-on: [self-hosted, prod] # non-ephemeral, also serves prod steps: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} # UNTRUSTED PR code - run: make build && make test # untrusted code, ALL secrets present - run: ./deploy.sh # same job/runner deploys to prod # Standing runner role - every job inherits full account access: # { "Effect":"Allow", "Action":"*", "Resource":"*" }

✅ Secure Code

# 1) Untrusted PR validation - ephemeral runner, NO secrets, read-only token. on: pull_request permissions: contents: read jobs: test: runs-on: ubuntu-latest # hosted, ephemeral, no prod creds steps: - uses: actions/checkout@v4 # fork code, nothing sensitive to steal - run: make build && make test # 2) Deploy - separate workflow, trusted branch only, short-lived OIDC, scoped role. on: push: branches: [main] permissions: id-token: write # request an OIDC token contents: read jobs: deploy: environment: production # approval + branch limit runs-on: ubuntu-latest steps: - uses: aws-actions/configure-aws-credentials@v4 with: # role scoped to THIS repo+environment, deploy-only, expires in minutes role-to-assume: arn:aws:iam::111122223333:role/deploy-app-only aws-region: us-east-1 - run: ./deploy.sh # no static keys # Scoped trust policy: sub = repo:acme/app:environment:production # Scoped permissions: s3:PutObject/GetObject on arn:...:prod-bucket/app/* only

✓ Prevention Checklist

  • Inject only the secrets a job actually needs - never all
  • Use ephemeral, single-use runners; destroy after each job
  • Never share a runner pool between public and private repos
  • Replace standing broad roles with per-job scoped OIDC tokens
  • Segment runner network from prod, metadata, and control plane
  • Split build from deploy across separately scoped runners
  • Set explicit least-privilege token permissions (read by default)
  • Mark deploy secrets protected + masked, branch-scoped
  • Verify integrity of caches and artifacts across trust levels
  • Never let build jobs run on the CI controller / host

🔍 Detection & Tools

Least-Privilege & Isolation:

GitHub OIDC GitLab ID Tokens Actions Runner Controller StepSecurity Harden-Runner Ephemeral Runners AWS IAM Access Analyzer

Policy & Audit:

OPA / Gatekeeper Trivy CloudTrail OpenSSF Scorecard Legitify

How to Test:

  • Check whether a build job can read secrets it never uses
  • Query the metadata endpoint from a job - should be blocked
  • Confirm runners are ephemeral and torn down after jobs
  • Verify fork PRs cannot reach private/self-hosted runners
  • Audit runner IAM roles for wildcard actions/resources
  • Test that a build runner cannot reach prod / the k8s API

🌍 Real-World (Incident Classes)

  • PPE on Over-Privileged Runners: Public repos ran PR-triggered pipelines on self-hosted runners holding production secrets and broad cloud roles; a crafted PR modified a build/test step to encode and exfiltrate the environment - the classic Poisoned Pipeline Execution outcome amplified by the runner's standing access.
  • Non-Ephemeral Runner Reuse: Long-lived self-hosted runners retained cloud credentials, registry logins, and kubeconfigs on disk; a later low-trust (fork) job landed on the same host and read the leftover state, inheriting access it was never granted.
  • Shared Cache / Artifact Poisoning: A cache or artifact store writable by low-trust jobs was consumed unverified by higher-trust deploy pipelines, so malicious content written once propagated into signed, shipped artefacts.
  • Cross-Trust Shared Runner Pool: Untrusted fork code executed on a runner that also served a private, production-credentialed pipeline, exposing another project's secrets and runner config.

📌 Quick Tips

  • DO NOT inject all secrets into every job
  • DO NOT attach standing wildcard roles to runners
  • DO NOT share runners across trust boundaries
  • DO use ephemeral, single-use runners
  • DO scope short-lived OIDC per job
  • DO segment build runners from production
  • DO split build and deploy responsibilities

📜 Compliance

Related Standards:

  • SLSA Build L3 - Isolated, Ephemeral
  • NIST 800-53 AC-6 - Least Privilege
  • NIST SSDF PO.5 - Secure Build Environments
  • ISO 27001 A.9 - Access Control
  • CIS Software Supply Chain Benchmark