"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
| Traditional | Shift Left |
|---|---|
| Find bugs in production | Find bugs in development |
| Manual security reviews | Automated scanning |
| Security as gatekeeper | Security as enabler |
| Expensive remediation | Cheap 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-credentialsCode 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=highHandling Vulnerabilities
Set clear policies:
| Severity | Action | Timeline |
|---|---|---|
| Critical | Block deploy | Immediate fix |
| High | Block deploy | Fix within 24h |
| Medium | Warning | Fix within 1 week |
| Low | Track | Fix 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@v2Semgrep for Custom Rules
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/security-audit
p/typescript
p/reactStage 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 package*.json ./
RUN npm ci --only=production
COPY . .
# Use multi-stage builds to reduce attack surface
FROM node:20-alpine AS builder
# ... build steps ...
FROM node:20-alpine
COPY /app/dist ./distStage 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: falseStage 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: falseCommon 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:
| Metric | Target |
|---|---|
| Mean time to remediate critical | < 24 hours |
| Vulnerabilities in production | 0 critical/high |
| Pipeline pass rate | > 95% |
| False positive rate | < 10% |
Common Pitfalls
Avoid these mistakes:
- Too many false positives - Developers start ignoring alerts
- Blocking on everything - Pipeline becomes unusable
- No remediation process - Findings pile up
- Skipping scans - "Just this once" becomes habit
- 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.
Was this article helpful?
Patrick Ejelle-Ndille
Author
Cybersecurity analyst and developer helping businesses secure their digital assets.
About Me