CI/CD Pipeline Security: Protecting Your Software Supply Chain
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
- Exposed secrets in pipeline configurations
- Insufficient access controls
- Unverified third-party actions/plugins
- No artifact signing or verification
- Overly permissive pipeline permissions
- Missing security scanning
- Untrusted dependencies
- 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
yamlname: 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
yamlname: 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
yamlname: 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
yamlname: 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
yamlname: 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
yamlname: 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
yamlname: 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
yamlname: 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
yamlname: 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.


