Skip to main content
Back to Blog
Development
January 5, 20265 min read

Integrating Security Into Your CI/CD Pipeline

Learn how to shift left on security by integrating automated security testing into your CI/CD pipeline. Catch vulnerabilities before they reach production.

Patrick Ejelle-Ndille

Cybersecurity Analyst & Full Stack Cloud Developer

"We'll add security later" is the most expensive phrase in software development. By the time vulnerabilities reach production, fixing them costs 100x more than catching them during development.

The solution? Shift left. Integrate security testing into your CI/CD pipeline and catch issues automatically, before they become problems.

Why Shift Left on Security?

Traditional security happens at the end—penetration testing before release, security audits annually. This approach has serious problems:

  • Late discovery - Vulnerabilities found after code is written
  • Expensive fixes - Rearchitecting vs. quick code changes
  • Delayed releases - Security reviews become bottlenecks
  • Developer friction - Security feels like a blocker

The Shift Left Advantage

TraditionalShift Left
Find bugs in productionFind bugs in development
Manual security reviewsAutomated scanning
Security as gatekeeperSecurity as enabler
Expensive remediationCheap fixes

Building a Secure Pipeline

Here's what a security-integrated CI/CD pipeline looks like:

Code Commit │ ▼ ┌─────────────────┐ │ Pre-commit │ ← Secrets scanning, linting │ Hooks │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Build │ ← Dependency scanning │ │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Test │ ← SAST, unit tests │ │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Security Scan │ ← Container scanning, DAST │ │ └─────────────────┘ │ ▼ ┌─────────────────┐ │ Deploy │ ← Infrastructure scanning │ │ └─────────────────┘

Stage 1: Pre-commit Hooks

Catch issues before they even enter the repository.

Secrets Detection

Prevent credentials from being committed:

# .pre-commit-config.yaml repos: - repo: https://github.com/gitleaks/gitleaks rev: v8.18.0 hooks: - id: gitleaks - repo: https://github.com/pre-commit/pre-commit-hooks rev: v4.5.0 hooks: - id: detect-private-key - id: detect-aws-credentials

Code Linting

Catch security anti-patterns early:

- repo: https://github.com/PyCQA/bandit rev: 1.7.5 hooks: - id: bandit args: ['-r', 'src/'] - repo: https://github.com/eslint/eslint rev: v8.55.0 hooks: - id: eslint files: \.(js|ts|tsx)$ args: ['--plugin', 'security']

Stage 2: Dependency Scanning

Your dependencies are part of your attack surface.

GitHub Actions Example

name: Security Scan on: [push, pull_request] jobs: dependency-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run npm audit run: npm audit --audit-level=high - name: Run Snyk uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: args: --severity-threshold=high

Handling Vulnerabilities

Set clear policies:

SeverityActionTimeline
CriticalBlock deployImmediate fix
HighBlock deployFix within 24h
MediumWarningFix within 1 week
LowTrackFix in next sprint

Stage 3: Static Application Security Testing (SAST)

Analyze source code for security vulnerabilities.

CodeQL for GitHub

name: CodeQL Analysis on: push: branches: [main] pull_request: branches: [main] jobs: analyze: runs-on: ubuntu-latest permissions: security-events: write steps: - uses: actions/checkout@v4 - name: Initialize CodeQL uses: github/codeql-action/init@v2 with: languages: javascript, typescript - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v2

Semgrep for Custom Rules

- name: Run Semgrep uses: returntocorp/semgrep-action@v1 with: config: >- p/security-audit p/typescript p/react

Stage 4: Container Security

If you're using containers, scan them.

Trivy Scanner

container-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Build image run: docker build -t myapp:${{ github.sha }} . - name: Run Trivy uses: aquasecurity/trivy-action@master with: image-ref: myapp:${{ github.sha }} format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' - name: Upload results uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif'

Dockerfile Best Practices

# Use specific versions, not latest FROM node:20-alpine@sha256:abc123... # Run as non-root user RUN addgroup -S appgroup && adduser -S appuser -G appgroup USER appuser # Don't copy unnecessary files COPY --chown=appuser:appgroup package*.json ./ RUN npm ci --only=production COPY --chown=appuser:appgroup . . # Use multi-stage builds to reduce attack surface FROM node:20-alpine AS builder # ... build steps ... FROM node:20-alpine COPY --from=builder /app/dist ./dist

Stage 5: Dynamic Application Security Testing (DAST)

Test your running application for vulnerabilities.

OWASP ZAP Integration

dast-scan: runs-on: ubuntu-latest needs: deploy-staging steps: - name: OWASP ZAP Scan uses: zaproxy/action-full-scan@v0.7.0 with: target: 'https://staging.myapp.com' rules_file_name: '.zap/rules.tsv' allow_issue_writing: false

Stage 6: Infrastructure as Code Scanning

Catch misconfigurations before deployment.

Checkov for Terraform

iac-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Run Checkov uses: bridgecrewio/checkov-action@master with: directory: terraform/ framework: terraform soft_fail: false

Common Findings

Checkov catches issues like:

  • S3 buckets without encryption
  • Security groups allowing 0.0.0.0/0
  • RDS instances publicly accessible
  • IAM policies that are too permissive

Complete Pipeline Example

Here's a full GitHub Actions workflow:

name: Secure CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] jobs: secrets-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 }} dependency-scan: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm audit --audit-level=high - uses: snyk/actions/node@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} sast: runs-on: ubuntu-latest permissions: security-events: write steps: - uses: actions/checkout@v4 - uses: github/codeql-action/init@v2 with: languages: javascript - uses: github/codeql-action/analyze@v2 build-and-scan: runs-on: ubuntu-latest needs: [secrets-scan, dependency-scan, sast] 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' deploy: runs-on: ubuntu-latest needs: build-and-scan if: github.ref == 'refs/heads/main' steps: - name: Deploy to production run: echo "Deploying..."

Measuring Success

Track these metrics:

MetricTarget
Mean time to remediate critical< 24 hours
Vulnerabilities in production0 critical/high
Pipeline pass rate> 95%
False positive rate< 10%

Common Pitfalls

Avoid these mistakes:

  1. Too many false positives - Developers start ignoring alerts
  2. Blocking on everything - Pipeline becomes unusable
  3. No remediation process - Findings pile up
  4. Skipping scans - "Just this once" becomes habit
  5. Not updating tools - Miss new vulnerability patterns

The Bottom Line

Security in CI/CD isn't about finding every possible vulnerability—it's about catching the important ones automatically, consistently, and early.

Start small: add secrets detection and dependency scanning. Then gradually add SAST, container scanning, and DAST. Your future self will thank you.


Need help building a secure CI/CD pipeline? I help teams integrate security into their development workflows. Let's discuss your automation needs.

cicd
devsecops
automation
testing

Was this article helpful?

Need Help With Development?

If you're facing challenges related to this topic or need expert guidance, I'm here to help. Let's discuss how I can assist your organization.