Skip to main content

CICD Security Pipeline

Introduction​

A CI/CD Security Pipeline (or DevSecOps Pipeline) integrates security practices into your Continuous Integration and Continuous Deployment processes. Rather than treating security as an afterthought or final check, a security pipeline embeds security testing and verification at every stage of your development lifecycle.

By implementing a security pipeline, you can:

  • Detect vulnerabilities early in development
  • Automatically enforce security policies
  • Create auditable security records
  • Prevent security issues from reaching production
  • Build security awareness across your development team

This guide will show you how to create a basic CI/CD security pipeline for your projects, even if you're just getting started with CI/CD concepts.

Understanding the Secure CI/CD Pipeline​

A secure CI/CD pipeline includes security checks at every stage of development:

Let's break down each of these security components and learn how to implement them.

1. Static Code Analysis (SAST)​

Static Application Security Testing analyzes your source code without executing it, looking for security vulnerabilities like SQL injection, XSS, and more.

Example: Setting up a basic SAST scan with SonarQube in GitHub Actions​

yaml
name: SAST Scan

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
sonarqube:
name: SonarQube Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarQube Scan
uses: SonarSource/sonarqube-scan-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
SONAR_HOST_URL: ${{ secrets.SONAR_HOST_URL }}

SonarQube results will show issues like:

CRITICAL: SQL Injection vulnerability found in app/controllers/users_controller.rb:45
MAJOR: Cross-site scripting (XSS) vulnerability in app/views/posts/show.html.erb:12

2. Dependency Scanning (SCA)​

Software Composition Analysis checks for vulnerabilities in your project dependencies and third-party libraries.

Example: Setting up dependency scanning with OWASP Dependency-Check​

yaml
name: Dependency Scan

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run OWASP Dependency-Check
uses: dependency-check/Dependency-Check_Action@main
with:
project: 'My Project'
path: '.'
format: 'HTML'
out: 'reports'
args: >
--failOnCVSS 7
--enableRetired
- name: Upload report
uses: actions/upload-artifact@v3
with:
name: Dependency Check Report
path: ${{ github.workspace }}/reports

Output example:

CVE-2022-22965: Spring Framework RCE vulnerability (CVSS: 9.8) found in spring-core-5.3.10.jar
CVE-2021-44228: Log4Shell vulnerability (CVSS: 10.0) found in log4j-core-2.14.0.jar

3. Secret Scanning​

Prevent sensitive credentials like API keys and passwords from being committed to your repository.

Example: Using pre-commit hooks with git-secrets​

First, install git-secrets:

bash
git clone https://github.com/awslabs/git-secrets.git
cd git-secrets
make install

Set up in your repository:

bash
cd your-repository
git secrets --install
git secrets --register-aws # Adds patterns for AWS keys

Add custom patterns:

bash
git secrets --add 'private_key'
git secrets --add 'api_key[[:space:]]*=[[:space:]]*[[:alnum:]]+'
git secrets --add --allowed 'AKIAIOSFODNN7EXAMPLE' # Allow test examples

Now, when you try to commit code with secrets:

bash
$ git commit -m "Add new feature"
git-secrets: (ERROR) Matched prohibited pattern
--
api_key = a8a7sd8a79sd87as98d7
--

4. Container Scanning​

If you're using containers, you need to scan them for vulnerabilities before deployment.

Example: Scanning Docker containers with Trivy in GitLab CI​

yaml
container_scanning:
stage: test
image:
name: aquasec/trivy:latest
entrypoint: [""]
variables:
DOCKER_IMAGE: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
script:
- trivy image --severity HIGH,CRITICAL $DOCKER_IMAGE
only:
- main

Example output:

CRITICAL: CVE-2023-12345 - Buffer overflow in OpenSSL 1.1.1n affects nginx:1.21
HIGH: CVE-2023-67890 - Remote code execution in Python 3.9.10

5. Dynamic Application Security Testing (DAST)​

DAST tests running applications by simulating attacks from the outside.

Example: Setting up OWASP ZAP scanning in Jenkins​

groovy
pipeline {
agent any

stages {
stage('Deploy Test Environment') {
steps {
sh 'docker-compose up -d app'
sh 'sleep 30' // Wait for app to start
}
}

stage('DAST Scan') {
steps {
sh '''
docker run --rm -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-baseline.py \
-t http://host.docker.internal:8080 \
-r dast-report.html
'''
}
}
}

post {
always {
archiveArtifacts artifacts: 'dast-report.html', fingerprint: true
sh 'docker-compose down'
}
}
}

ZAP will generate a report with findings like:

ALERT: [Cross-Site Scripting (Reflected)] http://host.docker.internal:8080/search?q=test%3Cscript%3Ealert(1)%3C/script%3E
ALERT: [X-Content-Type-Options Header Missing] http://host.docker.internal:8080

6. Infrastructure as Code Scanning​

Scan your infrastructure definitions for security issues and compliance violations.

Example: Scanning Terraform code with tfsec​

yaml
name: Infrastructure Scanning

on:
push:
paths:
- '**.tf'

jobs:
tfsec:
name: TFSec Infrastructure Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Run tfsec
uses: aquasecurity/tfsec-action@v1.0.0
with:
soft_fail: true

Sample output:

CRITICAL: AWS S3 Bucket has no encryption defined (terraform/s3.tf:15)
HIGH: Security group rule allows traffic from public internet (terraform/security_groups.tf:22)

Building a Complete Security Pipeline​

Now let's create a complete security pipeline example using GitHub Actions that incorporates several security checks:

yaml
name: Secure CI/CD Pipeline

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
static-analysis:
name: Static Code Analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Bandit
run: pip install bandit
- name: Run Bandit
run: bandit -r ./src -f json -o bandit-results.json
- name: Upload Bandit results
uses: actions/upload-artifact@v3
with:
name: bandit-results
path: bandit-results.json

dependency-check:
name: Dependency Scanning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: pip install safety
- name: Run safety check
run: safety check --json > safety-results.json
- name: Upload Safety results
uses: actions/upload-artifact@v3
with:
name: safety-results
path: safety-results.json

secret-scanning:
name: Secret Scanning
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Detect Secrets
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

build-scan:
name: Build and Image Scan
needs: [static-analysis, dependency-check, secret-scanning]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build Docker image
run: docker build -t my-app:${{ github.sha }} .
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: 'my-app:${{ github.sha }}'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v2
with:
sarif_file: 'trivy-results.sarif'

deploy-staging:
name: Deploy to Staging
needs: build-scan
runs-on: ubuntu-latest
if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
steps:
- uses: actions/checkout@v3
- name: Deploy to staging
run: echo "Deploying to staging environment"
# Add your deployment script here

dast-scan:
name: Dynamic Application Security Testing
needs: deploy-staging
runs-on: ubuntu-latest
if: ${{ success() && github.event_name == 'push' && github.ref == 'refs/heads/main' }}
steps:
- name: Run ZAP Scan
uses: zaproxy/action-baseline@v0.7.0
with:
target: 'https://staging-app-url.example.com'

Best Practices for CI/CD Security Pipelines​

  1. Shift Left: Move security scanning as early in the development process as possible
  2. Fail Fast: Configure your pipeline to fail on high-severity issues
  3. Scan Dependencies Regularly: Schedule regular scans even without code changes
  4. Maintain Allowlists: Add known false positives to allowlists to reduce noise
  5. Centralize Results: Use a central dashboard to track security findings
  6. Automate Remediation: Where possible, automate security fixes
  7. Scan Infrastructure: Don't forget to scan your infrastructure code
  8. Customize Rules: Tune security tools to match your project's risk profile

CI/CD Security Pipeline Implementation Steps​

Let's walk through implementing a basic security pipeline for a Python web application:

1. Create a baseline security configuration file​

First, create a .security-baseline.yml file in your project:

yaml
# .security-baseline.yml
security:
sast:
enabled: true
fail_on: critical
dependency_check:
enabled: true
fail_on: high
secrets:
enabled: true
fail_on: any
container_scan:
enabled: true
fail_on: critical
dast:
enabled: true
fail_on: critical

2. Set up pre-commit hooks for local security​

Create a .pre-commit-config.yaml file:

yaml
# .pre-commit-config.yaml
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.4.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files

- repo: https://github.com/pycqa/bandit
rev: 1.7.5
hooks:
- id: bandit
args: ['-ll']

- repo: https://github.com/gitleaks/gitleaks
rev: v8.16.3
hooks:
- id: gitleaks

Install pre-commit:

bash
pip install pre-commit
pre-commit install

3. Integrate with CI/CD (using GitHub Actions example)​

Create .github/workflows/security-pipeline.yml:

yaml
name: Security Pipeline

on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
schedule:
- cron: '0 0 * * 0' # Weekly scan

jobs:
security-checks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'

- name: Load security baseline config
id: config
run: |
echo "::set-output name=sast_enabled::$(yq e '.security.sast.enabled' .security-baseline.yml)"
echo "::set-output name=deps_enabled::$(yq e '.security.dependency_check.enabled' .security-baseline.yml)"

- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install bandit safety
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi

- name: Run SAST scan
if: steps.config.outputs.sast_enabled == 'true'
run: bandit -r . -f json -o bandit-results.json

- name: Run dependency check
if: steps.config.outputs.deps_enabled == 'true'
run: safety check --json > safety-results.json

- name: Secret scanning
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

# Add more security checks as needed

- name: Upload security results
uses: actions/upload-artifact@v3
with:
name: security-results
path: |
bandit-results.json
safety-results.json

4. Add a security reporting dashboard​

You can use tools like OWASP DefectDojo, Sonar, or ThreadFix to aggregate security results. For a beginner example, let's create a simple HTML report:

yaml
  generate-report:
needs: security-checks
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: security-results

- name: Generate HTML report
run: |
echo '<html><head><title>Security Report</title>' > report.html
echo '<style>body{font-family:sans-serif}table{border-collapse:collapse;width:100%}td,th{border:1px solid #ddd;padding:8px}</style>' >> report.html
echo '</head><body><h1>Security Scan Results</h1>' >> report.html

echo '<h2>SAST Results</h2>' >> report.html
echo '<table><tr><th>Severity</th><th>File</th><th>Issue</th></tr>' >> report.html
cat bandit-results.json | jq -r '.results[] | "<tr><td>\(.issue_severity)</td><td>\(.filename)</td><td>\(.issue_text)</td></tr>"' >> report.html
echo '</table>' >> report.html

echo '<h2>Dependency Results</h2>' >> report.html
echo '<table><tr><th>Severity</th><th>Package</th><th>Vulnerability</th></tr>' >> report.html
cat safety-results.json | jq -r '.[] | "<tr><td>\(.[5])</td><td>\(.[0])</td><td>\(.[3])</td></tr>"' >> report.html
echo '</table>' >> report.html

echo '</body></html>' >> report.html

- name: Upload HTML report
uses: actions/upload-artifact@v3
with:
name: security-report
path: report.html

Security Gates: Making Decisions Based on Results​

Security gates determine whether a build should proceed based on security findings:

yaml
  security-gate:
needs: [security-checks, generate-report]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v3
with:
name: security-results

- name: Check SAST critical findings
run: |
CRITICAL_COUNT=$(cat bandit-results.json | jq '.results[] | select(.issue_severity=="HIGH" or .issue_severity=="CRITICAL")' | jq -s 'length')
if [ $CRITICAL_COUNT -gt 0 ]; then
echo "::error::$CRITICAL_COUNT critical security issues found! Pipeline failed."
exit 1
fi

- name: Check dependency critical findings
run: |
CRITICAL_COUNT=$(cat safety-results.json | jq '.[] | select(.[5]=="Critical" or .[5]=="High")' | jq -s 'length')
if [ $CRITICAL_COUNT -gt 0 ]; then
echo "::error::$CRITICAL_COUNT critical dependency issues found! Pipeline failed."
exit 1
fi

Real-world Implementation Example​

Let's implement a security pipeline for a Python Flask application:

  1. Project structure:
my-flask-app/
├── .github/
│ └── workflows/
│ └── security-pipeline.yml
├── app/
│ ├── __init__.py
│ ├── models.py
│ ├── routes.py
│ └── templates/
│ └── index.html
├── tests/
│ └── test_app.py
├── .pre-commit-config.yaml
├── .security-baseline.yml
├── Dockerfile
├── requirements.txt
└── run.py
  1. Install security checks locally:
bash
pip install bandit safety pre-commit
pre-commit install
  1. Add a vulnerable dependency in requirements.txt for demonstration:
flask==2.0.1
sqlalchemy==1.4.23
werkzeug==2.0.1
pyyaml==5.4.1 # Has known vulnerabilities
  1. Add insecure code in app/routes.py:
python
@app.route('/search')
def search():
query = request.args.get('q')
# INSECURE: Vulnerable to SQL Injection
result = db.engine.execute(f"SELECT * FROM products WHERE name LIKE '%{query}%'")
return render_template('results.html', results=result)
  1. Run security checks manually:
bash
# SAST check
bandit -r ./app

# Dependency check
safety check -r requirements.txt
  1. The pipeline will automatically find these issues when you push to your repository.

Summary​

Implementing a CI/CD security pipeline brings security practices into your development workflow, helping you identify and fix security issues early. Key components include:

  1. Static Analysis (SAST) - Finding code vulnerabilities
  2. Dependency Scanning (SCA) - Detecting vulnerable libraries
  3. Secret Scanning - Preventing credential leaks
  4. Container Scanning - Ensuring secure container images
  5. Dynamic Testing (DAST) - Testing running applications
  6. Infrastructure Scanning - Securing your infrastructure code

By implementing these practices in your CI/CD pipeline, you create a robust security process that catches issues early and builds security awareness in your development team.

Additional Resources​

Exercises​

  1. Basic: Set up a pre-commit hook with Bandit in a Python project.
  2. Intermediate: Implement a full GitHub Actions workflow with SAST and dependency scanning.
  3. Advanced: Create a complete DevSecOps pipeline with security gates and reporting dashboards.

Try implementing these security checks in your own CI/CD pipeline and see how many vulnerabilities you can catch before they reach production!



If you spot any mistakes on this website, please let me know at feedback@compilenrun.com. I’d greatly appreciate your feedback! :)