Skip to content
Codeloom

Courses / CI/CD & GitHub Actions

Lesson 11 of 16

CI/CD Pipeline Security Best Practices

Harden your CI/CD pipelines with secret scanning, SAST integration, dependency audits, and supply chain security measures.

Intermediate 13 min read

What you'll learn

  • How to integrate secret scanning into your CI pipeline
  • Running SAST tools automatically on every pull request
  • Auditing dependencies for known vulnerabilities
  • Protecting your software supply chain with SBOM and signing
  • Applying least-privilege principles to CI/CD runners and tokens

Prerequisites

  • Basic CI/CD pipeline experience
  • Familiarity with GitHub Actions or similar CI system

A compromised CI/CD pipeline is one of the most damaging attack vectors in modern software development. An attacker who gains access to your pipeline can inject malicious code into every build, steal secrets, and push compromised artifacts to production. Securing your pipeline is not optional; it is as critical as securing your application code.

Secret Scanning in the Pipeline

Secrets accidentally committed to source control are the most common CI/CD security failure. Catching them before they reach a remote branch prevents credential exposure.

Using Gitleaks

Gitleaks scans git history and staged changes for patterns matching API keys, tokens, and passwords:

# .github/workflows/security.yml
name: Security Scan

on:
  pull_request:
  push:
    branches: [main]

jobs:
  secret-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

For more control, run Gitleaks directly and configure custom rules:

# .gitleaks.toml
title = "Custom Gitleaks Config"

[[rules]]
id = "internal-api-key"
description = "Internal API key pattern"
regex = '''INTERNAL_KEY_[A-Za-z0-9]{32}'''
tags = ["key", "internal"]

[allowlist]
paths = [
  '''\.test\.ts$''',
  '''__fixtures__''',
]

Using TruffleHog

TruffleHog goes further by verifying whether detected secrets are actually live:

  secret-scan-trufflehog:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: trufflesecurity/trufflehog@main
        with:
          extra_args: --only-verified

Static Application Security Testing (SAST)

SAST tools analyze source code for vulnerabilities without executing it. Integrating SAST into your pull request workflow catches security issues before code review.

CodeQL for GitHub Repositories

GitHub’s CodeQL is free for public repositories and provides deep semantic analysis:

  codeql-analysis:
    runs-on: ubuntu-latest
    permissions:
      security-events: write
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript, python
          queries: +security-extended
      - uses: github/codeql-action/autobuild@v3
      - uses: github/codeql-action/analyze@v3

The security-extended query suite catches more issues than the default, including data flow analysis for injection vulnerabilities.

Semgrep for Multi-Language Scanning

Semgrep is lightweight, fast, and supports custom rules:

  semgrep:
    runs-on: ubuntu-latest
    container:
      image: semgrep/semgrep
    steps:
      - uses: actions/checkout@v4
      - run: semgrep scan --config auto --config p/security-audit --sarif -o results.sarif
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: results.sarif
        if: always()

Writing custom Semgrep rules for your codebase catches organization-specific antipatterns:

# .semgrep/no-eval.yml
rules:
  - id: no-eval-usage
    patterns:
      - pattern: eval(...)
    message: "eval() is banned. Use JSON.parse() or a safe parser instead."
    languages: [javascript, typescript]
    severity: ERROR

Dependency Vulnerability Scanning

Third-party dependencies are the largest attack surface for most applications. Automated scanning catches known CVEs before they reach production.

GitHub Dependabot

Enable Dependabot alerts and automatic security updates:

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    open-pull-requests-limit: 10
    labels:
      - "dependencies"
      - "security"
    groups:
      dev-dependencies:
        dependency-type: "development"
        update-types: ["minor", "patch"]

npm audit in CI

Run npm audit as a pipeline step that fails on high-severity vulnerabilities:

  dependency-audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npm audit --audit-level=high

For Python projects, use pip-audit:

      - run: pip install pip-audit
      - run: pip-audit --strict --desc

Trivy for Container Scanning

If you build Docker images, scan them for OS and application vulnerabilities:

  container-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t myapp:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          format: 'sarif'
          output: 'trivy-results.sarif'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'
      - uses: github/codeql-action/upload-sarif@v3
        with:
          sarif_file: trivy-results.sarif
        if: always()

Software Supply Chain Security

Supply chain attacks target the tools and dependencies you use to build software, not the software itself.

Generating an SBOM

A Software Bill of Materials (SBOM) lists every component in your build. Generate one automatically:

  generate-sbom:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: anchore/sbom-action@v0
        with:
          format: spdx-json
          output-file: sbom.spdx.json
      - uses: actions/upload-artifact@v4
        with:
          name: sbom
          path: sbom.spdx.json

Signing Artifacts with Cosign

Sign your container images so consumers can verify they were built by your pipeline:

  sign-image:
    runs-on: ubuntu-latest
    permissions:
      id-token: write
      packages: write
    steps:
      - uses: sigstore/cosign-installer@v3
      - run: cosign sign --yes ghcr.io/your-org/myapp@${{ steps.build.outputs.digest }}

Cosign’s keyless signing uses OIDC identity from the GitHub Actions runner, so no private keys need to be stored.

Pinning Action Versions by SHA

Never use mutable tags for third-party actions in security-sensitive workflows:

# Insecure: tag can be moved to point to malicious code
- uses: actions/checkout@v4

# Secure: SHA is immutable
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Use Renovate or Dependabot to automate SHA updates when new versions are released.

Least-Privilege Pipeline Permissions

GitHub Actions workflows default to broad permissions. Restrict them explicitly:

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

jobs:
  deploy:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      deployments: write
      id-token: write
    steps:
      - uses: actions/checkout@v4
      # ... deployment steps

Token Scoping

The GITHUB_TOKEN has configurable permissions. For self-hosted runners, also consider:

  • Short-lived tokens. Use OIDC to request cloud provider credentials that expire in minutes, not stored long-lived access keys.
  • Environment-scoped secrets. Restrict production secrets to the production environment with required reviewers.
  • No write access for PRs from forks. GitHub restricts this by default; do not override it.
  deploy-prod:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://myapp.example.com
    permissions:
      id-token: write
      contents: read
    steps:
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789:role/deploy-role
          aws-region: us-east-1
      # Short-lived credentials, no stored keys

Branch Protection and Required Checks

Security scans are useless if developers can bypass them. Enforce them through branch protection:

  1. Require status checks to pass before merging. Mark your secret-scan, SAST, and dependency-audit jobs as required.
  2. Require pull request reviews. At least one approval from a code owner.
  3. Dismiss stale reviews when new commits are pushed.
  4. Restrict who can push to main directly.

Configure this in repository settings or via the GitHub API:

gh api repos/your-org/your-repo/branches/main/protection \
  --method PUT \
  --field required_status_checks='{"strict":true,"contexts":["secret-scan","codeql-analysis","dependency-audit"]}' \
  --field enforce_admins=true \
  --field required_pull_request_reviews='{"required_approving_review_count":1,"dismiss_stale_reviews":true}'

A Complete Security Pipeline

Here is a full workflow combining all the techniques:

name: Security Pipeline

on:
  pull_request:
  push:
    branches: [main]

permissions:
  contents: read
  security-events: write

jobs:
  secrets:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - uses: gitleaks/gitleaks-action@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  sast:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: github/codeql-action/init@v3
        with:
          languages: javascript
      - uses: github/codeql-action/autobuild@v3
      - uses: github/codeql-action/analyze@v3

  dependencies:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '22'
      - run: npm ci
      - run: npm audit --audit-level=high

  container:
    runs-on: ubuntu-latest
    if: github.event_name == 'push'
    steps:
      - uses: actions/checkout@v4
      - run: docker build -t myapp:${{ github.sha }} .
      - uses: aquasecurity/trivy-action@master
        with:
          image-ref: 'myapp:${{ github.sha }}'
          severity: 'CRITICAL,HIGH'
          exit-code: '1'

Summary

Pipeline security is a layered defense. Secret scanning prevents credential leaks, SAST catches code-level vulnerabilities, dependency auditing addresses known CVEs, and supply chain measures ensure your build tools and artifacts are trustworthy. Apply least-privilege permissions, pin action versions by SHA, and enforce security checks through branch protection rules. No single tool solves everything; the combination of all these layers is what makes a pipeline secure.

Progress is saved locally to your browser.