Back to CI/CD Pipeline
Q4Question 4 of 56

How to secure the CI/CD pipeline itself: OIDC federation, ephemeral runners, pinned action SHAs, least-privilege deploy roles: and how to defend against supply-chain attacks

Key Concepts
  • OIDC federation replaces long-lived cloud keys with short-lived tokens exchanged for temporary credentials that expire in minutes.
  • Ephemeral runners are destroyed after each job, preventing secrets or malware from persisting between builds.
  • Pinning third-party actions to exact commit SHAs prevents a moved tag from injecting malicious code into your pipeline.
  • Least-privilege deploy roles limit the blast radius if a pipeline is compromised.
  • An internal artifact registry with dependency pinning and checksum verification defends against supply-chain and dependency-confusion attacks.
  • Artifact signing with provenance attestations ensures only trusted, verified artifacts are deployed.

Definition

Treat the pipeline as production: short-lived OIDC credentials, throwaway runners, SHA-pinned actions, least-privilege deploy roles, and for the supply chain an internal registry, pinned dependencies, and signed artifacts with provenance.
  • OIDC federation – the pipeline exchanges a short-lived identity token for temporary cloud credentials instead of storing long-lived access keys.
  • Ephemeral runners – each job runs on a freshly provisioned machine that is destroyed afterward, preventing state or secret persistence.
  • Pinned action SHAs – third-party pipeline actions are referenced by exact commit SHA so a tag cannot be moved to malicious code.
  • Least-privilege deploy roles – the IAM role grants only the specific permissions for the specific deployment, limiting blast radius.
  • Internal registry – dependencies are pulled from a controlled, scanned source, not directly from the public internet.
  • Artifact signing – build outputs are cryptographically signed with provenance attestations; the deployment target verifies before running.

The CI/CD pipeline is the armored truck that carries money to National Bank of India branches. OIDC is giving the driver a single-use pass that expires at the gate instead of a master key. Ephemeral runners are using a fresh truck for every trip and crushing it afterward. Pinning SHAs is checking the engine serial number, not just a label someone could swap. Least privilege means the truck can open only the one branch door it needs. Supply-chain defenses are buying parts only from a vetted warehouse and stamping every delivery.

Understanding the Concepts

Why is the CI/CD pipeline a prime target?

The pipeline can deploy to production, so whoever controls it controls your systems. It holds secrets, accesses cloud accounts, and pushes code to production environments. Compromising the pipeline is often easier than compromising the production system directly.

DIAGRAM
 Pipeline threat model:

 ┌──────────────────────────────────────────┐
 │           CI/CD Pipeline                 │
 │                                          │
 │  Has access to:                          │
 │  - Cloud credentials (deploy to prod)    │
 │  - Container registry (push images)      │
 │  - Kubernetes cluster (deploy workloads) │
 │  - Source code (read/write)              │
 │                                          │
 │  Attack vectors:                         │
 │  1. Stolen long-lived cloud keys         │
 │  2. Compromised third-party actions      │
 │  3. Poisoned dependencies                │
 │  4. Persistent malware on shared runners │
 │  5. Over-privileged deploy roles         │
 └──────────────────────────────────────────┘

How does OIDC federation replace long-lived keys?

OIDC federation eliminates stored secrets entirely. The CI provider issues a short-lived signed token, the cloud provider exchanges it for temporary credentials.

YAML
# WRONG: long-lived AWS keys stored as GitHub secrets
- name: Configure AWS
  uses: aws-actions/configure-aws-credentials@v4
  with:
    aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}      # Static, never expires
    aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}  # Catastrophic if leaked
    aws-region: ap-south-1
YAML
# CORRECT: OIDC federation -- no stored secrets
permissions:
  id-token: write  # Required for OIDC
  contents: read

- name: Configure AWS via OIDC
  uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502  # pinned SHA
  with:
    role-to-assume: arn:aws:iam::123456789012:role/bank-deploy-role
    aws-region: ap-south-1
    # No access key or secret key -- OIDC token exchanged for temp credentials
JSON
// IAM trust policy -- only allows tokens from this specific repo and branch
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"},
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": {
        "token.actions.githubusercontent.com:sub": "repo:national-bank/transfer-service:ref:refs/heads/main"
      }
    }
  }]
}

Why use ephemeral runners?

Ephemeral runners are freshly provisioned for each job and destroyed when the job finishes. This prevents one build from leaving secrets or malware for the next build.

YAML
# WRONG: persistent self-hosted runner shared across jobs
# A compromised job can plant malware that infects the next job
runs-on: self-hosted  # Same machine reused, state persists

# CORRECT: ephemeral runner destroyed after each job
runs-on: ubuntu-latest  # GitHub-hosted, fresh for every job
# Or: self-hosted with ephemeral flag
# --ephemeral flag makes the runner deregister after one job

How do you pin third-party actions to SHAs?

A tag is mutable: the author or an attacker can repoint it at malicious code. Pinning to the full commit SHA ensures you run exactly the reviewed code.

YAML
# WRONG: mutable tag -- can be moved to malicious code
- uses: actions/checkout@v4  # Tag can be repointed

# CORRECT: pinned to exact commit SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11  # v4.1.1 exact SHA
BASH
# How to find the SHA for a tag
git ls-remote https://github.com/actions/checkout.git refs/tags/v4.1.1
# Output: b4ffde65f46336ab88eb53be808477a3936bae11

How do you scope deploy roles to least privilege?

The deploy role should grant only the specific permissions for the specific deployment, not broad administrator access.

JSON
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PushToECR",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:PutImage",
        "ecr:InitiateLayerUpload",
        "ecr:UploadLayerPart",
        "ecr:CompleteLayerUpload"
      ],
      "Resource": "arn:aws:ecr:ap-south-1:123456789012:repository/bank-transfer-service"
    },
    {
      "Sid": "UpdateKubernetes",
      "Effect": "Allow",
      "Action": ["eks:DescribeCluster"],
      "Resource": "arn:aws:eks:ap-south-1:123456789012:cluster/bank-prod"
    }
  ]
}
JSON
// WRONG: over-privileged deploy role
{
  "Effect": "Allow",
  "Action": "*",
  "Resource": "*"
}
// If the pipeline is compromised, the attacker owns the entire AWS account

How do you defend against supply-chain attacks?

An internal artifact registry, dependency pinning with verified checksums, and artifact signing form a chain of trust.

XML
<!-- pom.xml -- pull from internal registry, not public Maven Central --syn-kw">>
syn-kw"><repositoriessyn-kw">>
    syn-kw"><repositorysyn-kw">>
        syn-kw"><idsyn-kw">>bank-internalsyn-kw"></idsyn-kw">>
        syn-kw"><urlsyn-kw">>https://codeartifact.ap-south-1.amazonaws.com/maven/bank-repo/syn-kw"></urlsyn-kw">>
    syn-kw"></repositorysyn-kw">>
syn-kw"></repositoriessyn-kw">>
BASH
# Verify dependency checksums in Gradle
# gradle/verification-metadata.xml ensures every dependency matches its expected hash
# A hash mismatch fails the build -- dependency cannot be swapped silently
BASH
# Sign the container image with cosign
cosign sign --key cosign.key bank/transfer-service:abc123

# Verify at deployment time
cosign verify --key cosign.pub bank/transfer-service:abc123
# Unsigned or tampered images are rejected

What does the complete chain of trust look like?

Each link in the chain assumes the others may fail, which is what makes the whole pipeline resilient.

DIAGRAM
 Chain of Trust:

 Throwaway runner ──▶ assumes OIDC short-lived identity
     │
     ▼
 Runs only SHA-pinned action steps
     │
     ▼
 Pulls only pinned + checksum-verified dependencies from internal registry
     │
     ▼
 Produces signed artifact with provenance attestation
     │
     ▼
 Deploys through least-privilege role
     │
     ▼
 Target verifies signature + provenance before running

What are common mistakes in pipeline security?

YAML
# WRONG: storing long-lived AWS keys in GitHub secrets
env:
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_KEY }}  # Never expires, catastrophic if leaked

# CORRECT: OIDC federation -- nothing stored, nothing to leak
uses: aws-actions/configure-aws-credentials@<sha>
with:
  role-to-assume: arn:aws:iam::123456789012:role/bank-deploy
YAML
# WRONG: referencing actions by mutable tag
- uses: some-org/some-action@v1  # Tag can be moved to malicious code

# CORRECT: pinned to exact SHA
- uses: some-org/some-action@abc123def456  # Immutable reference

Visual Overview

DIAGRAM
 Pipeline Security Controls
 ============================

 ┌─────────────────────────────────────────────────┐
 │              CI/CD Pipeline                      │
 │                                                  │
 │  ┌────────────┐  ┌────────────┐  ┌────────────┐ │
 │  │ OIDC Token │  │ Ephemeral  │  │ SHA-Pinned │ │
 │  │ (no stored │  │ Runner     │  │ Actions    │ │
 │  │  secrets)  │  │ (destroyed │  │ (immutable │ │
 │  │            │  │  per job)  │  │  code ref) │ │
 │  └─────┬──────┘  └────────────┘  └────────────┘ │
 │        │                                         │
 │        ▼                                         │
 │  ┌────────────┐  ┌────────────┐  ┌────────────┐ │
 │  │ Least-Priv │  │ Internal   │  │ Checksum   │ │
 │  │ Deploy     │  │ Registry   │  │ Verified   │ │
 │  │ Role       │  │ (no public │  │ Deps       │ │
 │  │            │  │  pull)     │  │            │ │
 │  └─────┬──────┘  └────────────┘  └────────────┘ │
 │        │                                         │
 │        ▼                                         │
 │  ┌────────────────────────────────────────────┐  │
 │  │  Signed Artifact + Provenance Attestation  │  │
 │  │  Target verifies before running            │  │
 │  └────────────────────────────────────────────┘  │
 └─────────────────────────────────────────────────┘

 Long-Lived Key vs OIDC
 =======================

 Long-lived key:  Created ──▶ Stored ──▶ Used ──▶ [LEAKED] ──▶ Attacker has access forever
                                                               (until someone rotates)

 OIDC token:      Minted per run ──▶ Exchanged ──▶ Expires in 15 min ──▶ Useless

Key Takeaway

The CI/CD pipeline is a production system because it holds the keys to deploy code. The first principle is eliminating long-lived secrets: OIDC federation replaces static cloud access keys with short-lived tokens exchanged for temporary credentials that expire in minutes. Ephemeral runners prevent state or malware from persisting between builds. SHA-pinned actions prevent a moved tag from injecting malicious code. Least-privilege deploy roles limit the blast radius of a compromise.

For the supply chain, an internal registry defends against dependency-confusion attacks, checksum verification ensures every build resolves to the exact same bytes, and artifact signing with provenance lets the deployment target reject anything unsigned or tampered. Together these form a chain of trust where each link assumes the others may fail.

At National Bank of India our deploy pipeline had stored a long-lived cloud key, referenced actions by mutable tag, and skipped checksum verification. We moved to OIDC, pinned actions to SHAs, verified dependency checksums, and signed artifacts, turning the pipeline from a soft target into a defensible chain of trust.

Remember one sentence: treat the pipeline as production by eliminating long-lived secrets with OIDC, destroying runners after each job, pinning actions to SHAs, and signing every artifact.

---

Share
Use ← → arrow keys to navigate