CI/CD Pipeline Security: Protecting Your Software Supply Chain

CI/CD Pipeline Security: Protecting Your Software Supply Chain

Whitespots Team ·
cicd
devops
supply-chain
automation

Introduction

CI/CD pipelines are critical infrastructure that can become attack vectors if not properly secured. From stolen secrets to supply chain attacks, compromised pipelines can lead to widespread breaches. This guide covers essential security practices for protecting your CI/CD infrastructure and build processes.

Common CI/CD Security Issues

  1. Exposed secrets in pipeline configurations
  2. Insufficient access controls
  3. Unverified third-party actions/plugins
  4. No artifact signing or verification
  5. Overly permissive pipeline permissions
  6. Missing security scanning
  7. Untrusted dependencies
  8. No audit logging

Secrets Management

Vulnerable Pipeline with Hardcoded Secrets

yaml
# VULNERABLE GitHub Actions workflow name: Deploy on: push jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Deploy to production run: | # Hardcoded credentials - BAD! export AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE export AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG export DATABASE_PASSWORD=supersecret123 ./deploy.sh

Secure Pipeline with Secrets Management

yaml
# SECURE GitHub Actions workflow name: Deploy on: push: branches: - main permissions: contents: read id-token: write # For OIDC jobs: deploy: runs-on: ubuntu-latest environment: production # Requires approval steps: - name: Checkout code uses: actions/checkout@v4 with: persist-credentials: false - name: Configure AWS credentials (OIDC) uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: arn:aws:iam::ACCOUNT:role/GitHubActionsRole aws-region: us-east-1 # No long-lived credentials needed! - name: Get secrets from AWS Secrets Manager run: | export DB_PASSWORD=$(aws secretsmanager get-secret-value \ --secret-id production/database/password \ --query SecretString \ --output text) echo "::add-mask::$DB_PASSWORD" # Mask in logs - name: Deploy env: # Use GitHub secrets for other credentials API_KEY: ${{ secrets.API_KEY }} run: ./deploy.sh

GitHub Actions OIDC Setup

hcl
# Terraform - AWS IAM role for GitHub Actions OIDC resource "aws_iam_openid_connect_provider" "github_actions" { url = "https://token.actions.githubusercontent.com" client_id_list = ["sts.amazonaws.com"] thumbprint_list = [ "6938fd4d98bab03faadb97b34396831e3780aea1" ] } resource "aws_iam_role" "github_actions" { name = "GitHubActionsRole" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.github_actions.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com" } StringLike = { "token.actions.githubusercontent.com:sub" = "repo:org/repo:*" } } } ] }) } resource "aws_iam_role_policy" "github_actions" { role = aws_iam_role.github_actions.id policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:PutObject", "s3:GetObject" ] Resource = "arn:aws:s3:::deployment-bucket/*" }, { Effect = "Allow" Action = [ "secretsmanager:GetSecretValue" ] Resource = "arn:aws:secretsmanager:*:*:secret:production/*" } ] }) }

Pipeline Permissions and Access Control

Restrictive GitHub Actions Permissions

yaml
name: Build and Test on: pull_request: branches: [main] # Global permissions (most restrictive by default) permissions: contents: read jobs: test: runs-on: ubuntu-latest permissions: contents: read # Read repo contents pull-requests: write # Comment on PRs checks: write # Update check runs steps: - uses: actions/checkout@v4 - name: Run tests run: npm test - name: Comment on PR if: failure() uses: actions/github-script@v7 with: script: | github.rest.issues.createComment({ issue_number: context.issue.number, owner: context.repo.owner, repo: context.repo.repo, body: 'Tests failed! Please review.' })

GitLab CI with Least Privilege

yaml
# .gitlab-ci.yml - Secure configuration variables: # Disable git strategy to prevent credential exposure GIT_STRATEGY: fetch GIT_DEPTH: 1 stages: - test - build - deploy # Default settings default: # Use specific runner tags tags: - docker # Security scanning before_script: - echo "Running security checks..." test: stage: test image: node:18-alpine script: - npm ci - npm run test coverage: '/Lines\s*:\s*(\d+\.\d+)%/' artifacts: reports: junit: junit.xml coverage_report: coverage_format: cobertura path: coverage/cobertura-coverage.xml build: stage: build image: docker:24 services: - docker:24-dind before_script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin $CI_REGISTRY script: - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA only: - main deploy:production: stage: deploy image: alpine:latest environment: name: production url: https://app.example.com # Require manual approval for production when: manual # Only from protected branches only: - main script: - apk add --no-cache aws-cli - aws ecs update-service --cluster production --service app --force-new-deployment # Use specific variables scope variables: AWS_DEFAULT_REGION: us-east-1

Third-Party Action/Plugin Security

Pinning Actions to SHA

yaml
# VULNERABLE: Using tag (can be changed) - uses: actions/checkout@v4 # SECURE: Pin to specific SHA - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Verifying Third-Party Actions

yaml
name: Security Checks on: pull_request jobs: verify-actions: runs-on: ubuntu-latest steps: # Only use verified publishers - name: Checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 # Review action source before using - name: Run tests uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: 18 # Prefer official actions from trusted sources - name: Upload artifact uses: actions/upload-artifact@5d5d22a31266ced268874388b861e4b58bb5c2f3 # v4.3.1

Creating Allowlist

yaml
# .github/workflows/action-allowlist.yml name: Verify Actions on: pull_request: paths: - '.github/workflows/*.yml' jobs: check-actions: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Check allowed actions run: | ALLOWED_ACTIONS=( "actions/checkout" "actions/setup-node" "actions/upload-artifact" "docker/build-push-action" ) for workflow in .github/workflows/*.yml; do for action in $(grep -oP 'uses:\s*\K[^@]+' $workflow); do if [[ ! " ${ALLOWED_ACTIONS[@]} " =~ " ${action} " ]]; then echo "Unauthorized action: $action in $workflow" exit 1 fi done done

Dependency Security

Dependency Scanning

yaml
name: Dependency Security on: schedule: - cron: '0 0 * * *' # Daily pull_request: jobs: dependency-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Snyk uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high - name: Run npm audit run: | npm audit --audit-level=high npm audit signatures - name: Check for supply chain attacks run: | npx socket security --all - name: SBOM Generation run: | npm install -g @cyclonedx/cyclonedx-npm cyclonedx-npm --output-file sbom.json

Lock File Verification

yaml
name: Verify Lock Files on: [pull_request] jobs: verify-lockfile: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Verify package-lock.json run: | npm ci git diff --exit-code package-lock.json - name: Check for malicious packages run: npx socket security

Artifact Signing and Verification

Signing with Cosign

yaml
name: Build and Sign on: push: branches: [main] jobs: build-and-sign: runs-on: ubuntu-latest permissions: contents: read packages: write id-token: write # For keyless signing steps: - uses: actions/checkout@v4 - name: Setup Cosign uses: sigstore/cosign-installer@v3 - name: Login to registry uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build image uses: docker/build-push-action@v5 with: push: true tags: ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Sign image (keyless) run: | cosign sign --yes ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Generate SBOM run: | syft ghcr.io/${{ github.repository }}:${{ github.sha }} -o spdx-json > sbom.spdx.json cosign attach sbom --sbom sbom.spdx.json ghcr.io/${{ github.repository }}:${{ github.sha }}

Verifying Signed Artifacts

yaml
name: Deploy on: workflow_dispatch: jobs: deploy: runs-on: ubuntu-latest steps: - name: Setup Cosign uses: sigstore/cosign-installer@v3 - name: Verify image signature run: | cosign verify \ --certificate-identity-regexp="^https://github.com/${{ github.repository }}" \ --certificate-oidc-issuer=https://token.actions.githubusercontent.com \ ghcr.io/${{ github.repository }}:${{ github.sha }} - name: Deploy only if verified run: ./deploy.sh

Security Scanning in Pipeline

Comprehensive Security Scanning

yaml
name: Security Pipeline on: pull_request: push: branches: [main] jobs: security-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 # Secret scanning - name: TruffleHog Secret Scan uses: trufflesecurity/trufflehog@main with: path: ./ base: ${{ github.event.repository.default_branch }} head: HEAD # SAST - name: Run Semgrep uses: returntocorp/semgrep-action@v1 with: config: >- p/security-audit p/secrets p/owasp-top-ten # Dependency scanning - name: Dependency Review uses: actions/dependency-review-action@v4 if: github.event_name == 'pull_request' # Container scanning - name: Build image run: docker build -t myapp:test . - name: Run Trivy uses: aquasecurity/trivy-action@master with: image-ref: myapp:test format: sarif output: trivy-results.sarif severity: CRITICAL,HIGH - name: Upload Trivy results uses: github/codeql-action/upload-sarif@v3 with: sarif_file: trivy-results.sarif # Infrastructure scanning - name: Terraform security scan uses: aquasecurity/tfsec-action@v1.0.0 with: working_directory: ./terraform

Pipeline Isolation and Sandboxing

Using Ephemeral Environments

yaml
name: Secure Build jobs: build: runs-on: ubuntu-latest container: image: node:18-alpine # Run as non-root options: --user 1001:1001 steps: - uses: actions/checkout@v4 # Build in isolated environment - name: Build run: | npm ci --ignore-scripts # Prevent running untrusted scripts npm run build # No credentials in build environment - name: Run tests run: npm test

Self-Hosted Runner Security

yaml
# GitHub Actions - Self-hosted runner config name: Secure Self-Hosted on: [push] jobs: build: # Use labeled runners runs-on: [self-hosted, linux, x64, isolated] steps: - uses: actions/checkout@v4 - name: Build in container uses: docker://node:18-alpine with: args: npm ci && npm run build

Audit Logging and Monitoring

yaml
name: Audit Pipeline on: push: pull_request: workflow_dispatch: jobs: audit: runs-on: ubuntu-latest steps: - name: Log pipeline execution run: | echo "Pipeline started by: ${{ github.actor }}" echo "Trigger: ${{ github.event_name }}" echo "Commit: ${{ github.sha }}" echo "Ref: ${{ github.ref }}" - name: Send audit log if: always() run: | curl -X POST https://logging-endpoint.example.com/audit \ -H "Authorization: Bearer ${{ secrets.LOGGING_TOKEN }}" \ -d '{ "actor": "${{ github.actor }}", "event": "${{ github.event_name }}", "repo": "${{ github.repository }}", "commit": "${{ github.sha }}", "workflow": "${{ github.workflow }}", "status": "${{ job.status }}" }'

CI/CD Security Checklist

  • ✅ Never hardcode secrets in pipelines
  • ✅ Use OIDC for cloud authentication
  • ✅ Pin third-party actions to SHA
  • ✅ Implement least privilege permissions
  • ✅ Require approval for production deployments
  • ✅ Sign and verify all artifacts
  • ✅ Scan dependencies regularly
  • ✅ Run security scanners in pipeline
  • ✅ Use isolated build environments
  • ✅ Enable audit logging
  • ✅ Restrict who can modify pipelines
  • ✅ Use protected branches
  • ✅ Implement SBOM generation
  • ✅ Verify artifact integrity before deployment
  • ✅ Monitor pipeline activity
  • ✅ Regular security reviews of pipeline configs

Conclusion

CI/CD pipeline security is critical for protecting your software supply chain. By implementing secrets management, access controls, artifact signing, and comprehensive security scanning, you prevent pipelines from becoming attack vectors.

Pipeline security requires continuous vigilance, regular audits, and staying updated on emerging threats. For comprehensive CI/CD security assessments and pipeline hardening, contact the Whitespots team for expert consultation.