Definition
- 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.
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.
# 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
# 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
// 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.
# 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.
# 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
# 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.
{
"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"
}
]
}
// 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.
<!-- 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">>
# 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
# 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.
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?
# 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
# 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
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.
---