Verified by Garnet Grid

How to Secure Your CI/CD Pipeline: Vulnerability Scanning and Access Control

Harden your CI/CD pipeline against supply chain attacks. Covers runner security, artifact signing, RBAC, pipeline secrets management, and audit logging.

Your CI/CD pipeline has more access than any developer. It touches your source code, builds your binaries, handles your secrets, and deploys to production. If an attacker compromises your pipeline, they own everything. The SolarWinds attack (2020), the Codecov breach (2021), and the CircleCI incident (2023) all demonstrate that supply chain attacks through CI/CD are real, devastating, and increasingly common.

This guide covers the five layers of CI/CD security: secrets management, permissions hardening, build integrity verification, deployment protection, and continuous monitoring.


Why CI/CD Is a Prime Target

Attack VectorReal-World ExampleImpact
Compromised build stepSolarWinds Orion (2020)18,000+ organizations backdoored through tampered build process
Leaked CI/CD secretsCircleCI (2023)Customer secrets exposed, requiring mass rotation
Poisoned dependencyCodecov (2021)Bash uploader modified to exfiltrate credentials
Compromised runnerCrypto mining on GitHub ActionsFree-tier runners hijacked for cryptocurrency mining
Insider threatMalicious commit from trusted devDirect code injection bypassing code review

The common thread: CI/CD pipelines are trusted implicitly and often have the broadest access of any system in your organization.


Step 1: Secure Pipeline Secrets

1.1 Never Store Secrets in Code

# ❌ NEVER do this — secrets in plain text
env:
  DATABASE_URL: "postgres://admin:P@ssw0rd@prod-db:5432/main"

# ✅ Use GitHub Secrets or a vault
env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}

Detection: Run git log --all -p | grep -iE 'password|secret|api_key|token' against your repository history. If anything matches, those secrets are compromised — rotate immediately and use git filter-branch or BFG Repo-Cleaner to remove them from history.

1.2 Use Environment-Scoped Secrets

Secrets should be scoped to specific environments with approval gates. Production secrets should never be accessible during a PR build.

# GitHub Actions — environment protection with required reviewers
jobs:
  deploy-prod:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.com
    steps:
      - name: Deploy
        env:
          # These secrets are ONLY accessible when deploying to 'production'
          # The deployment won't proceed until a reviewer approves
          AWS_ACCESS_KEY: ${{ secrets.PROD_AWS_ACCESS_KEY }}
          AWS_SECRET_KEY: ${{ secrets.PROD_AWS_SECRET_KEY }}
        run: ./deploy.sh

1.3 Rotate Secrets Automatically

Manual rotation is a security debt that accumulates. Automate it.

# AWS Secrets Manager — automatic rotation every 30 days
aws secretsmanager rotate-secret \
  --secret-id prod/database \
  --rotation-lambda-arn arn:aws:lambda:us-east-1:123456:function:rotate-db-secret \
  --rotation-rules '{"AutomaticallyAfterDays": 30}'

1.4 Secrets Hygiene Checklist

PracticePriorityImplementation
No secrets in source codeP0Pre-commit hooks (gitleaks, detect-secrets)
Environment-scoped secretsP0GitHub Environments, GitLab Protected Variables
Automatic rotation (30-90 days)P1AWS Secrets Manager, Azure Key Vault, Vault
Short-lived credentialsP0OIDC federation (see Step 2)
Secret scanning on pushP0GitHub Advanced Security, GitGuardian
Audit log for secret accessP1Vault audit logs, AWS CloudTrail

Step 2: Lock Down Pipeline Permissions

2.1 OIDC Federation (No Long-Lived Credentials)

Long-lived access keys are the #1 CI/CD security risk. OIDC federation eliminates them entirely. The CI runner requests a short-lived token from your cloud provider, scoped to the specific repository and branch.

permissions:
  id-token: write   # Required for OIDC
  contents: read     # Minimum required

steps:
  - name: Configure AWS Credentials (OIDC)
    uses: aws-actions/configure-aws-credentials@v4
    with:
      role-to-assume: arn:aws:iam::123456789:role/GitHubActionsRole
      aws-region: us-east-1
      # No access keys needed! Token is:
      # - Short-lived (1 hour default)
      # - Scoped to this specific repo + branch
      # - Auditable in CloudTrail

2.2 Least Privilege for Pipeline Service Accounts

Your pipeline needs only the permissions it uses. Audit and trim.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "PushImages",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken",
        "ecr:BatchCheckLayerAvailability",
        "ecr:PutImage"
      ],
      "Resource": "arn:aws:ecr:us-east-1:123456:repository/myapp"
    },
    {
      "Sid": "DeployService",
      "Effect": "Allow",
      "Action": ["ecs:UpdateService"],
      "Resource": "arn:aws:ecs:us-east-1:123456:service/myapp-prod/*"
    }
  ]
}

2.3 GitHub Actions Permissions Best Practice

Always set permissions at the job level in GitHub Actions. The default token has write access to many scopes — restrict to minimum.

# At the workflow level, set restrictive defaults
permissions:
  contents: read

jobs:
  build:
    permissions:
      contents: read
      packages: write  # Only if publishing packages
    # ...

Step 3: Verify Build Integrity

Supply chain attacks modify your artifacts between build and deployment. Signing and verification create a chain of trust.

3.1 Sign Your Container Images

# Cosign — sign container images with keyless signing (Sigstore)
cosign sign --yes myregistry.com/myapp:v1.2.3

# Verify signature before deployment (in your deployment pipeline)
cosign verify \
  --certificate-identity "https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  myregistry.com/myapp:v1.2.3

3.2 Generate and Scan SBOM (Software Bill of Materials)

An SBOM lists every dependency in your build. When a new CVE is published, you can instantly determine if you are affected.

# Syft — generate SBOM in SPDX format
syft myregistry.com/myapp:v1.2.3 -o spdx-json > sbom.json

# Grype — scan SBOM for known vulnerabilities, fail on high/critical
grype sbom:sbom.json --fail-on high

3.3 Pin Dependencies and Actions

# ❌ Risky — uses mutable tag, could be compromised
- uses: actions/checkout@v4

# ✅ Secure — pinned to immutable SHA
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Pinning to SHA prevents an attacker from modifying a published action version after you start using it.


Step 4: Protect Branch and Deployment Rules

# Branch protection via GitHub CLI
gh api repos/{owner}/{repo}/branches/main/protection \
  --method PUT \
  --field required_status_checks='{"strict":true,"contexts":["security-scan","tests"]}' \
  --field enforce_admins=true \
  --field required_pull_request_reviews='{"required_approving_review_count":2,"dismiss_stale_reviews":true}' \
  --field restrictions=null

Required Checks Before Merge

CheckPurposeBlocks Merge?Tool
Unit TestsCode correctness✅ Yespytest, Jest, Go test
SAST ScanStatic code vulnerabilities✅ YesSemgrep, CodeQL, SonarQube
Dependency AuditKnown CVEs in dependencies✅ Yes (high/critical)Dependabot, Snyk, Grype
Container ScanImage vulnerabilities✅ YesTrivy, Grype, Snyk Container
Code ReviewHuman approval (2+ reviewers)✅ YesGitHub, GitLab
License CheckOpen-source license compliance⚠️ Warning onlyFOSSA, Licensee
Secret ScanAccidental secret commits✅ Yesgitleaks, GitGuardian

Step 5: Monitor Pipeline Activity

Every deployment should produce an immutable audit log. When a security incident occurs, you need to answer: who deployed what, when, and from which commit.

  audit-logging:
    runs-on: ubuntu-latest
    steps:
      - name: Log Deployment to Audit System
        run: |
          curl -X POST "${{ secrets.AUDIT_WEBHOOK }}" \
            -H "Content-Type: application/json" \
            -d '{
              "event": "deployment",
              "environment": "production",
              "commit": "${{ github.sha }}",
              "actor": "${{ github.actor }}",
              "timestamp": "'$(date -u +%Y-%m-%dT%H:%M:%SZ)'",
              "workflow": "${{ github.workflow }}",
              "run_id": "${{ github.run_id }}",
              "image_digest": "'$IMAGE_DIGEST'",
              "sbom_hash": "'$SBOM_HASH'"
            }'

What to Monitor

  • Unusual deployment times — Deployments at 3 AM by a developer who normally works 9-5
  • Skipped checks — Admin overrides of required status checks
  • New contributors — First-time contributors to sensitive repositories
  • Self-merged PRs — PRs merged by the author without independent review
  • Secret access patterns — Unusual vault/secret access from pipeline service accounts

Self-Hosted Runner Security

If you use self-hosted runners (GitHub Actions, GitLab Runners), they require additional hardening:

ControlImplementationWhy
Ephemeral runnersSpin up fresh runner per job, destroy afterPrevents persistence from previous jobs
Network isolationRunners in dedicated VPC/subnet with limited egressLimits blast radius if compromised
No shared runners for public reposDedicated runners per orgPrevents crypto-mining and code exfiltration
Runner-level secretsInject secrets via vault at runtime, not environment variablesPrevents cross-job secret leakage

CI/CD Security Hardening Priorities

PriorityActionImpactEffort
1Remove hardcoded secrets from pipelinesCritical — number one breach vectorLow
2Pin all action and image versions to SHACritical — prevents supply chain attacksLow
3Enable branch protection (require reviews)High — prevents unauthorized deploymentsLow
4Scan dependencies for known CVEsHigh — catches known vulnerabilitiesMedium
5Scan container images before deploymentHigh — prevents vulnerable deploymentsMedium
6Implement OIDC for cloud auth (no long-lived keys)High — eliminates key managementMedium
7Add SAST scanningMedium — catches code-level vulnerabilitiesMedium
8Implement signed commits and artifactsMedium — ensures provenanceHigh

Supply Chain Attack Prevention

  • Pin GitHub Actions to commit SHA, not tags (tags can be moved maliciously)
  • Use private registries for container images and npm packages
  • Verify checksums on all downloaded binaries and dependencies
  • Enable Dependabot or Renovate for automated dependency updates with review

Pipeline Security Checklist

  • No secrets in source code (pre-commit hooks + scanning active)
  • Environment-scoped secrets with approval gates for production
  • OIDC federation in place (no long-lived credentials in CI/CD)
  • Least-privilege service account permissions (audited quarterly)
  • Container image signing enabled (Cosign/Sigstore)
  • SBOM generation and vulnerability scanning on every build
  • All actions/dependencies pinned to immutable SHAs
  • Branch protection with required checks (SAST, tests, review)
  • 2+ reviewer requirement for production deployment PRs
  • Audit logging for all pipeline activities (immutable, searchable)
  • Self-hosted runners hardened (ephemeral, network-isolated)

:::note[Source] This guide is derived from operational intelligence at Garnet Grid Consulting. For DevOps maturity assessments, visit garnetgrid.com. :::

Jakub Dimitri Rezayev
Jakub Dimitri Rezayev
Founder & Chief Architect • Garnet Grid Consulting

Jakub holds an M.S. in Customer Intelligence & Analytics and a B.S. in Finance & Computer Science from Pace University. With deep expertise spanning D365 F&O, Azure, Power BI, and AI/ML systems, he architects enterprise solutions that bridge legacy systems and modern technology — and has led multi-million dollar ERP implementations for Fortune 500 supply chains.

View Full Profile →