17.2 Zero Trust for CI/CD Pipelines¶
Replace Stored Secrets with Identity
When Codecov was breached, attackers harvested CI/CD secrets from thousands of organizations. Those secrets remained valid until manually rotated. OIDC federation eliminates this—credentials expire in minutes/hours, and there are no secrets to rotate.
For years, CI/CD security meant storing long-lived secrets—AWS access keys, service account tokens, deployment credentials—in pipeline configuration. These secrets never expire, work from anywhere, and provide full access once compromised. When attackers breached Codecov2 in 2021, they harvested CI/CD secrets from thousands of organizations. Those secrets remained valid until manually rotated—if organizations even knew to rotate them. Zero trust architecture eliminates this vulnerability by replacing stored secrets with identity-based authentication: build jobs prove who they are and receive short-lived credentials valid only for that specific execution.
This section applies zero trust principles to CI/CD pipelines, demonstrating how to move from secrets-based to identity-based authentication using OIDC federation and workload identity.
Zero Trust Principles for CI/CD¶
Zero trust assumes no implicit trust based on network location or prior authentication. Every request must be authenticated, authorized, and continuously validated.
Traditional CI/CD Trust Model:
┌────────────────────────────────────────────────────────────────┐
│ TRADITIONAL MODEL │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ Long-lived ┌─────────────────┐ │
│ │ CI/CD │ Secret │ Cloud │ │
│ │ Pipeline │ ───────────────────► │ Provider │ │
│ └─────────────┘ └─────────────────┘ │
│ │
│ Problems: │
│ • Secret never expires │
│ • Works from any context │
│ • Compromised = persistent access │
│ • Rotation is manual and error-prone │
│ │
└────────────────────────────────────────────────────────────────┘
Zero Trust CI/CD Model:
┌────────────────────────────────────────────────────────────────┐
│ ZERO TRUST MODEL │
├────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ 1. Request token ┌─────────────────┐ │
│ │ CI/CD │ ◄──────────────────► │ Identity │ │
│ │ Pipeline │ (OIDC) │ Provider │ │
│ └──────┬──────┘ └─────────────────┘ │
│ │ │
│ │ 2. Present token + policy check │
│ ▼ │
│ ┌─────────────────┐ 3. Short-lived ┌─────────────────┐ │
│ │ Cloud │ ◄───────────────► │ Policy │ │
│ │ Provider │ Credential │ Engine │ │
│ └─────────────────┘ └─────────────────┘ │
│ │
│ Benefits: │
│ • Credentials expire (minutes/hours) │
│ • Context validated (repo, branch, workflow) │
│ • Compromised = limited window │
│ • No secrets to rotate │
│ │
└────────────────────────────────────────────────────────────────┘
Core Principles Applied:
| Principle | Traditional CI/CD | Zero Trust CI/CD |
|---|---|---|
| Verify explicitly | Trust stored secret | Verify identity each time |
| Least privilege | Broad, static permissions | Scoped, dynamic permissions |
| Assume breach | Secrets valid indefinitely | Credentials expire rapidly |
| Never trust | Network location trusted | Every request authenticated |
Organizations that migrate to OIDC federation report significant security benefits when credential breaches occur elsewhere in their industry—they have nothing to rotate because every credential has already expired.
OIDC Federation for Cloud Access¶
OpenID Connect (OIDC) federation enables CI/CD platforms to issue identity tokens that cloud providers accept, eliminating the need for stored cloud credentials.
How OIDC Federation Works:
- CI/CD platform issues a signed JWT (identity token)
- Token contains claims about the workflow (repo, branch, actor)
- Cloud provider validates token signature and claims
- Cloud provider issues short-lived credentials if policy allows
GitHub Actions → AWS:
Step 1: Create OIDC Identity Provider in AWS
# Create the OIDC provider. AWS can retrieve the TLS thumbprint;
# if you supply thumbprints manually, verify the current values first.
aws iam create-open-id-connect-provider \
--url https://token.actions.githubusercontent.com \
--client-id-list "sts.amazonaws.com"
Step 2: Create IAM Role with Trust Policy
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
]
}
Step 3: Use in GitHub Actions Workflow
name: Deploy
on:
push:
branches: [main]
permissions:
id-token: write # Required for OIDC
contents: read
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
aws-region: us-east-1
# No access key ID or secret access key stored!
- run: aws s3 ls # Works with temporary credentials
GitHub Actions → Google Cloud:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: google-github-actions/auth@v2
with:
workload_identity_provider: 'projects/123456/locations/global/workloadIdentityPools/github/providers/github'
service_account: 'github-actions@project.iam.gserviceaccount.com'
- uses: google-github-actions/setup-gcloud@v2
- run: gcloud storage ls
GitHub Actions → Azure:
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- uses: azure/login@v2
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}
# Note: client-id and tenant-id are not secrets (public identifiers)
# No client secret stored!
GitLab CI → AWS:
deploy:
image: amazon/aws-cli
id_tokens:
AWS_TOKEN:
aud: https://gitlab.com
script:
- >
export $(printf "AWS_ACCESS_KEY_ID=%s AWS_SECRET_ACCESS_KEY=%s AWS_SESSION_TOKEN=%s"
$(aws sts assume-role-with-web-identity
--role-arn arn:aws:iam::123456789:role/GitLabRole
--role-session-name "GitLabSession"
--web-identity-token $AWS_TOKEN
--duration-seconds 3600
--query 'Credentials.[AccessKeyId,SecretAccessKey,SessionToken]'
--output text))
- aws s3 ls
Short-Lived Credentials¶
Short-lived credentials limit the window of opportunity for attackers.
Credential Lifetime Recommendations:
| Context | Recommended Lifetime | Rationale |
|---|---|---|
| Build job | 15-60 minutes | Matches typical job duration |
| Deployment | 15-30 minutes | Matches deployment window |
| Cross-account access | 1 hour maximum | Balance convenience and security |
| Production access | As short as practical | Higher risk requires tighter controls |
AWS STS Configuration:
# GitHub Actions: Request short-lived credentials
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/DeployRole
aws-region: us-east-1
role-duration-seconds: 900 # 15 minutes
role-session-name: deploy-${{ github.run_id }}
Service Account Token Projection (Kubernetes):
apiVersion: v1
kind: Pod
spec:
serviceAccountName: build-agent
containers:
- name: build
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 600 # 10 minutes
audience: build-system
Automatic Credential Refresh:
# Python: Use SDK automatic refresh
import boto3
from botocore.credentials import RefreshableCredentials
from botocore.session import get_session
def get_refreshable_session():
"""Create session with automatic credential refresh."""
session = get_session()
def refresh():
# STS AssumeRoleWithWebIdentity call
credentials = assume_role_with_web_identity()
return {
'access_key': credentials['AccessKeyId'],
'secret_key': credentials['SecretAccessKey'],
'token': credentials['SessionToken'],
'expiry_time': credentials['Expiration'].isoformat()
}
refreshable_credentials = RefreshableCredentials.create_from_metadata(
metadata=refresh(),
refresh_using=refresh,
method='sts-assume-role-with-web-identity'
)
session._credentials = refreshable_credentials
return boto3.Session(botocore_session=session)
Workload Identity Implementation¶
Workload identity assigns verifiable identity to build jobs, enabling fine-grained access control.
GitHub Actions OIDC Claims:
The identity token contains claims that can be used for policy decisions:
{
"iss": "https://token.actions.githubusercontent.com",
"sub": "repo:myorg/myrepo:ref:refs/heads/main",
"aud": "sts.amazonaws.com",
"ref": "refs/heads/main",
"sha": "abc123...",
"repository": "myorg/myrepo",
"repository_owner": "myorg",
"actor": "username",
"workflow": "Deploy",
"event_name": "push",
"ref_type": "branch",
"job_workflow_ref": "myorg/myrepo/.github/workflows/deploy.yml@refs/heads/main",
"runner_environment": "github-hosted"
}
Fine-Grained Trust Policies:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::123456789:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
]
}
Condition Examples:
| Condition | Effect |
|---|---|
repo:myorg/myrepo:* |
Any branch in specific repo |
repo:myorg/myrepo:ref:refs/heads/main |
Only main branch |
repo:myorg/myrepo:environment:production |
Only production environment |
repo:myorg/*:ref:refs/heads/main |
Main branch across org |
GCP Workload Identity Federation:
# Create workload identity pool
gcloud iam workload-identity-pools create "github-pool" \
--location="global" \
--description="GitHub Actions pool"
# Create provider
gcloud iam workload-identity-pools providers create-oidc "github-provider" \
--location="global" \
--workload-identity-pool="github-pool" \
--issuer-uri="https://token.actions.githubusercontent.com" \
--attribute-mapping="google.subject=assertion.sub,attribute.repository=assertion.repository"
# Grant access to service account
gcloud iam service-accounts add-iam-policy-binding \
"github-actions@project.iam.gserviceaccount.com" \
--role="roles/iam.workloadIdentityUser" \
--member="principalSet://iam.googleapis.com/projects/123/locations/global/workloadIdentityPools/github-pool/attribute.repository/myorg/myrepo"
OIDC Security Risks and Mitigations¶
OIDC Eliminates Secrets But Introduces New Risks
OIDC federation removes the risk of stolen long-lived secrets but introduces different security considerations. Misconfigured trust policies, exposed tokens, and overly permissive claims can create vulnerabilities as serious as the secrets they replaced.
While OIDC federation significantly improves CI/CD security, it introduces its own risks that must be understood and mitigated.
Risk 1: Overly Permissive Trust Policies
The most common OIDC security mistake is configuring trust policies that are too broad:
// DANGEROUS: Accepts tokens from any repository in the org
{
"Condition": {
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:myorg/*"
}
}
}
// BETTER: Restricts to specific repo and branch
{
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:sub": "repo:myorg/myrepo:ref:refs/heads/main"
}
}
}
| Misconfiguration | Risk | Mitigation |
|---|---|---|
| Wildcard repository matching | Any repo in org can assume role | Specify exact repository names |
| Missing branch restrictions | Feature branches can access production | Require ref:refs/heads/main for production |
| Missing environment constraints | Any workflow can assume role | Use GitHub environments with protection rules |
| Accepting any issuer | Tokens from untrusted IdPs accepted | Allowlist specific OIDC issuers |
Risk 2: Token Exposure in Logs and Artifacts
OIDC tokens, while short-lived, can be leaked through CI/CD logs, artifacts, or error messages:
# DANGEROUS: Token may appear in logs
- run: |
echo "Token: $ACTIONS_ID_TOKEN_REQUEST_TOKEN"
curl -H "Authorization: bearer $TOKEN" ...
# SAFER: Mask tokens and minimize logging
- run: |
echo "::add-mask::$ACTIONS_ID_TOKEN_REQUEST_TOKEN"
# Use tokens directly without echoing
Mitigation strategies:
- Enable log redaction for sensitive patterns
- Never echo or print tokens, even for debugging
- Review artifact uploads for accidental token inclusion
- Set short token lifetimes; OIDC token validity is provider-specific and commonly measured in minutes
Token exposure is not limited to logs and artifacts. In the 2026 TanStack "Mini Shai-Hulud" compromise, reporting traced the attack chain to pull_request_target, GitHub Actions cache poisoning, and runtime extraction of an OIDC token from the runner process. The attacker reportedly used trusted publishing to mint a short-lived npm publish token rather than stealing a stored npm token.3 We recommend granting id-token: write only to release jobs protected by branch, tag, and environment rules—not to PR validation or cache-populating jobs.
Risk 3: Claim Confusion and Injection
OIDC claims are trusted assertions from the identity provider. Misunderstanding claim semantics can lead to security gaps:
| Claim | Common Misunderstanding | Correct Interpretation |
|---|---|---|
actor |
The person who triggered the build | May be a bot, app, or different user for scheduled/dispatch workflows |
ref |
Always the protected branch | Could be any ref including attacker-controlled branches |
repository |
Always trusted | Forks may have the same repository name pattern |
workflow_ref |
The workflow being executed | Must include ref to prevent modified workflow attacks |
Risk 4: OIDC Provider Availability
If your CI/CD platform's OIDC endpoint or your cloud provider's token exchange service is unavailable, builds cannot obtain credentials:
Mitigation strategies:
- Implement retry logic with exponential backoff
- Consider fallback mechanisms for critical deployments (with appropriate alerting)
- Monitor OIDC endpoint availability
- Design for graceful degradation (fail builds rather than fall back to insecure methods)
Risk 5: Time-of-Check to Time-of-Use (TOCTOU)
Between token issuance and credential use, conditions may change:
1. Workflow requests OIDC token (conditions valid)
2. Token issued with claims
3. [User loses repository access]
4. Token still valid, credentials obtained
5. Action performed by now-unauthorized user
Mitigations:
- Keep token lifetimes short (15 minutes or less)
- Implement additional authorization checks at resource access time
- Monitor for anomalous access patterns
- Use GitHub environments with required reviewers for sensitive operations
OIDC Security Checklist:
## OIDC Trust Policy Review
### Specificity
- [ ] Repository specified exactly (no wildcards unless necessary)
- [ ] Branch/tag restrictions applied for production access
- [ ] Environment constraints used where available
### Token Handling
- [ ] Tokens are never logged or printed
- [ ] Token lifetime is appropriately short
- [ ] Automatic credential refresh is implemented
- [ ] `id-token: write` is limited to protected release or deployment jobs
- [ ] PR-triggered and cache-populating jobs cannot request publishing tokens
### Monitoring
- [ ] Failed authentication attempts are logged
- [ ] Unusual access patterns trigger alerts
- [ ] Trust policy changes require review
### Resilience
- [ ] OIDC endpoint failures are handled gracefully
- [ ] No fallback to less secure authentication methods
Continuous Verification¶
Zero trust requires verification throughout the build lifecycle, not just at the start.
Verification Points:
| Point | Verification |
|---|---|
| Trigger | Is this a legitimate trigger? (webhook signature, source) |
| Authentication | Is the identity valid? (OIDC token validation) |
| Authorization | Is this identity allowed this action? (policy check) |
| Runtime | Are actions within expected bounds? (behavioral analysis) |
| Artifacts | Are outputs integrity-verified? (signing, provenance) |
Webhook Signature Verification:
import hmac
import hashlib
def verify_github_webhook(payload: bytes, signature: str, secret: str) -> bool:
"""Verify GitHub webhook signature."""
expected = 'sha256=' + hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In webhook handler
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Hub-Signature-256')
if not verify_github_webhook(request.data, signature, WEBHOOK_SECRET):
return 'Invalid signature', 403
# Process legitimate webhook
return process_webhook(request.json)
Webhook Filters ≠ Webhook Signatures
Webhook signature verification (above) confirms that an event payload actually came from GitHub. But many CI/CD platforms also use webhook filters to decide which events trigger privileged builds — for example, allowing builds only when the actor's identity matches a trusted maintainer list. These filters are a separate trust layer and must be equally rigorous.
The CodeBreach incident (January 2026) demonstrated what happens when webhook filters are fragile. AWS CodeBuild webhook filters for four AWS-managed open-source repositories used unanchored regex to match trusted GitHub actor IDs. Because the patterns lacked ^ and $ anchors, they performed substring matches — meaning an attacker who obtained a GitHub user ID containing a trusted maintainer's ID as a substring could bypass the filter and trigger privileged builds with access to repository secrets and credentials. AWS reported no downstream compromise, but the potential blast radius included widely-consumed libraries like aws-sdk-js-v3.1
The lesson: treat webhook filters as security-critical code. Use exact-match allowlists or cryptographic identity verification, not fragile regex patterns. And layer them — webhook signatures prove the event is authentic; filters prove the actor is authorized; approval gates prove a human reviewed the change. See Book 1, Chapter 7, Section 7.6 for the full case study.
Runtime Policy Enforcement:
# OPA policy for build job verification
package cicd.build
import rego.v1
default allow = false
# Allow build if conditions met
allow if {
input.repository == "myorg/myrepo"
input.ref == "refs/heads/main"
input.workflow_ref == "myorg/myrepo/.github/workflows/deploy.yml@refs/heads/main"
input.runner_environment == "github-hosted"
input.actor_is_org_member == true
}
# Deny if suspicious patterns
deny contains msg if {
input.ref != "refs/heads/main"
input.environment == "production"
msg := "Production deployment only allowed from main branch"
}
deny contains msg if {
input.runner_environment == "self-hosted"
input.environment == "production"
msg := "Production deployment requires GitHub-hosted runners"
}
Artifact Verification:
# GitHub Actions: Sign and verify artifacts
jobs:
build:
permissions:
contents: read
id-token: write
attestations: write
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/attest-build-provenance@v1
with:
subject-path: 'dist/artifact.tar.gz'
deploy:
needs: build
steps:
- uses: actions/download-artifact@v4
# Verify before deployment
- run: |
gh attestation verify dist/artifact.tar.gz \
--owner myorg \
--repo myrepo
Policy-as-Code for Build Pipelines¶
Encode security policies as code for consistent enforcement.
Open Policy Agent (OPA) for CI/CD:
# policy/cicd.rego
package cicd
import rego.v1
# Enforce branch protection
deny contains msg if {
input.event == "push"
input.ref == "refs/heads/main"
not input.pull_request_merged
msg := "Direct push to main not allowed"
}
# Require signed commits
deny contains msg if {
input.event == "pull_request"
commit := input.commits[_]
not commit.verified
msg := sprintf("Commit %s is not signed", [commit.sha])
}
# Enforce deployment approval
deny contains msg if {
input.environment == "production"
not input.deployment_approved
msg := "Production deployment requires approval"
}
# Restrict which workflows can deploy
allow_deploy if {
input.workflow_ref == "myorg/myrepo/.github/workflows/deploy.yml@refs/heads/main"
input.repository == "myorg/myrepo"
}
deny contains msg if {
input.action == "deploy"
not allow_deploy
msg := "Unauthorized workflow attempting deployment"
}
Policy Enforcement in Pipeline:
# GitHub Actions: Policy check before deployment
jobs:
policy-check:
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v4
- uses: open-policy-agent/setup-opa@v2
- name: Check policies
run: |
cat > input.json <<'JSON'
{
"event": "${{ github.event_name }}",
"ref": "${{ github.ref }}",
"repository": "${{ github.repository }}",
"actor": "${{ github.actor }}",
"workflow_ref": "${{ github.workflow_ref }}",
"environment": "production",
"deployment_approved": true,
"action": "deploy"
}
JSON
opa eval --fail-defined -d policy/cicd.rego -i input.json 'data.cicd.deny[_]'
deploy:
needs: policy-check
# Only runs if policy check passes
Conftest for Workflow Validation:
# Validate CI/CD configuration against policies
conftest test .github/workflows/*.yml -p policy/
# Example policy for workflow validation
package main
import rego.v1
deny contains msg if {
job := input.jobs[_]
job["runs-on"] == "self-hosted"
msg := "Self-hosted runners not allowed for this repository"
}
deny contains msg if {
input.permissions == "write-all"
msg := "write-all permissions not allowed"
}
Migration from Secrets to Identity¶
Moving from stored secrets to identity-based authentication requires a phased approach.
Migration Phases:
Phase 1: Inventory and Assessment
# Secret Inventory
| Secret Name | Type | Age | Used In | Can Use OIDC? |
|-------------|------|-----|---------|---------------|
| AWS_ACCESS_KEY | Cloud | 2 years | deploy.yml | Yes |
| DOCKER_PASSWORD | Registry | 1 year | build.yml | Yes (token) |
| NPM_TOKEN | Package | 6 months | publish.yml | Yes (granular) |
| SIGNING_KEY | Signing | 3 years | release.yml | No (hardware) |
Phase 2: Parallel Implementation
# Run both methods during transition
jobs:
deploy-new:
if: ${{ vars.USE_OIDC == 'true' }}
permissions:
id-token: write
contents: read
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
deploy-legacy:
if: ${{ vars.USE_OIDC != 'true' }}
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
Phase 3: Validation and Cutover
# Verify OIDC works before removing secrets
steps:
- name: Test OIDC authentication
id: test-oidc
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.AWS_ROLE_ARN }}
continue-on-error: true
- name: Verify success
if: steps.test-oidc.outcome == 'success'
run: echo "OIDC working - safe to remove secrets"
- name: Alert on failure
if: steps.test-oidc.outcome == 'failure'
run: |
echo "::error::OIDC authentication failed"
exit 1
Phase 4: Secret Removal and Monitoring
# Remove secrets only after OIDC verified
# Monitor for any failures post-migration
# GitHub CLI: Delete secret (after verification)
gh secret delete AWS_ACCESS_KEY_ID
gh secret delete AWS_SECRET_ACCESS_KEY
Migration Checklist:
## OIDC Migration Checklist
### Pre-Migration
- [ ] Inventory all stored secrets
- [ ] Identify which can use OIDC
- [ ] Design IAM roles with appropriate trust policies
- [ ] Create OIDC providers in cloud accounts
- [ ] Document rollback procedure
### Migration
- [ ] Deploy OIDC configuration in parallel
- [ ] Test in non-production first
- [ ] Validate authentication works
- [ ] Monitor for issues
- [ ] Gradually shift traffic to OIDC
### Post-Migration
- [ ] Remove legacy secrets
- [ ] Update documentation
- [ ] Brief team on new approach
- [ ] Monitor authentication metrics
- [ ] Schedule periodic review
Recommendations¶
For DevOps Engineers:
-
Start with OIDC for cloud access. Replace AWS access keys, GCP service account keys, and Azure service principals with OIDC federation. This is the highest-impact change you can make.
-
Set aggressive credential lifetimes. Request 15-minute credentials for builds, not 1-hour. Shorter is better.
-
Use claim-based trust policies. Don't allow any repository to assume your roles. Specify exact repository, branch, and environment in trust policies.
For Platform Engineers:
-
Build identity infrastructure. Create OIDC providers, IAM roles, and trust policies as reusable templates. Make identity-based auth the easy path.
-
Implement policy-as-code. Use OPA or similar to enforce security policies programmatically. Manual review doesn't scale.
-
Monitor identity usage. Track which identities access which resources. Anomalies indicate either misconfiguration or compromise.
For Security Architects:
-
Eliminate stored secrets systematically. Audit all CI/CD secrets. For each one, determine if identity-based authentication can replace it.
-
Design for continuous verification. Authentication at the start isn't enough. Verify at each stage: trigger, authorization, runtime, and artifacts.
-
Plan the migration. Moving from secrets to identity is a journey. Phase it to manage risk while maintaining velocity.
Zero trust transforms CI/CD security from "protect the secrets" to "verify the identity." When there are no long-lived secrets to steal, credential theft becomes impossible. When every action is authenticated and authorized based on verifiable identity, attackers must compromise the identity system itself—a much harder target than a stored secret.
-
Wiz Research, "CodeBreach: Infiltrating the AWS Console Supply Chain and Hijacking AWS GitHub Repositories via CodeBuild," https://www.wiz.io/blog/wiz-research-codebreach-vulnerability-aws-codebuild, January 15, 2026; AWS Security Bulletin 2026-002. ↩
-
Codecov, "Bash Uploader Security Update," April 2021, https://about.codecov.io/apr-2021-post-mortem/ ↩
-
The Hacker News, "Mini Shai-Hulud Worm Compromises TanStack, UiPath, Mistral AI npm and PyPI Packages," May 2026, https://thehackernews.com/2026/05/mini-shai-hulud-worm-compromises.html ↩