Hands-on: Complete Secure Pipeline
Vibe Prompt
"Help me build a complete secure CI/CD Pipeline: Code Push → SAST → Build → Container Scan → Deploy Staging → DAST → Deploy Production."
The Complete Pipeline Architecture
Before diving into the YAML, let's understand the architecture. This pipeline implements a Defense-in-Depth strategy. We layer security controls so that if one layer misses a vulnerability, a subsequent layer catches it. We enforce a "Shift Left" philosophy by running the fastest, most developer-actionable scans (SAST/SCA) first, failing fast to prevent wasted compute resources on broken code.
name: Secure CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
types: [opened, synchronize, reopened]
jobs:
# ============================================================
# STAGE 1: STATIC ANALYSIS & DEPENDENCY CHECKS (FAIL FAST)
# ============================================================
security-checks:
name: Security Checks (SAST + SCA + IaC)
runs-on: ubuntu-latest
# We use a matrix strategy to run language-specific tools in parallel
# but for this tutorial we keep it linear for clarity.
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Required for SonarQube/CodeQL history analysis
# ----------------------------------------------------------
# 1. SAST - SonarCloud / SonarQube
# WHAT: Pattern-based static analysis for code smells, bugs,
# and security hotspots across 30+ languages.
# WHY: Catches logic errors and maintainability issues early.
# Provides Quality Gates (coverage, duplication, rating).
# HOW: Requires SONAR_TOKEN secret. Configure project key in
# sonar-project.properties or via UI.
# ----------------------------------------------------------
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# Optional: Pass PR context for decoration
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# ----------------------------------------------------------
# 2. SAST - GitHub CodeQL (Deep Semantic Analysis)
# WHAT: Query-based engine (QL) tracking data flow from
# sources (user input) to sinks (SQL exec, shell exec).
# WHY: Finds complex vulnerabilities (SQLi, XSS, RCE, Path Traversal)
# that regex-based scanners miss. Free for public repos.
# HOW: Initialize -> Autobuild/Manual Build -> Analyze.
# We specify languages explicitly to save time.
# ----------------------------------------------------------
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript, typescript, python, java
# For compiled languages, disable autobuild and add manual build steps
# config-file: .github/codeql/codeql-config.yml
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:javascript" # SARIF category for multi-lang upload
# ----------------------------------------------------------
# 3. SCA - Snyk (Software Composition Analysis)
# WHAT: Scans manifest files (package.json, pom.xml, go.mod,
# requirements.txt) against a vulnerability DB.
# WHY: 80-90% of modern app code is open source. Known CVEs in
# dependencies are the #1 attack vector (Log4Shell, Spring4Shell).
# HOW: Auth via SNYK_TOKEN. `--severity-threshold=high` fails
# build only on High/Critical. Monitor command tracks deps over time.
# ----------------------------------------------------------
- name: Snyk Security Scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
command: test
args: --severity-threshold=high --sarif-file-output=snyk.sarif
# Upload SARIF to GitHub Security Tab for unified view
- name: Upload Snyk SARIF to GitHub
uses: github/codeql-action/upload-sarif@v3
if: always() # Upload even if scan fails to see results
with:
sarif_file: snyk.sarif
category: snyk-sca
# ----------------------------------------------------------
# 4. IaC Scanning - Checkov / Bridgecrew
# WHAT: Scans Kubernetes YAML, Terraform, CloudFormation,
# Dockerfile, Helm charts for misconfigurations.
# WHY: Misconfigured cloud resources (public S3, privileged pods,
# missing network policies) cause massive breaches.
# HOW: Point to `k8s/` directory. Framework `kubernetes` enables
# specific K8s checks (CKV_K8S_*).
# ----------------------------------------------------------
- name: Checkov IaC Scan
uses: bridgecrewio/checkov-action@master
with:
directory: k8s/
framework: kubernetes
# Optional: --skip-check CKV_K8S_XX to suppress false positives
# Output: cli, json, junitxml for CI integration
# ============================================================
# STAGE 2: BUILD & CONTAINER SECURITY
# ============================================================
build-and-scan:
name: Build & Container Scan
needs: security-checks # Gate: Only build if static checks pass
runs-on: ubuntu-latest
permissions:
contents: read
packages: write # Required to push to GHCR
security-events: write # For Trivy SARIF upload
steps:
- name: Checkout Repository
uses: actions/checkout@v4
# ----------------------------------------------------------
# Docker Build Optimization (BuildKit, Cache)
# WHAT: Build production image. Tag with Git SHA for immutability
# and traceability. Use BuildKit for layer caching.
# WHY: Reproducible builds. SHA tag allows rollback.
# "Latest" tag is anti-pattern for production.
# HOW: `docker buildx build --push` for multi-arch, but here
# we build locally then push to control scan timing.
# ----------------------------------------------------------
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build Docker Image
run: |
docker buildx build \
--cache-from type=gha \
--cache-to type=gha,mode=max \
-t ghcr.io/${{ github.repository }}:${{ github.sha }} \
--load . # Load into local docker daemon for Trivy
# ----------------------------------------------------------
# 5. Container Scan - Trivy (Vulnerability + Misconfig + Secrets)
# WHAT: Scans OS packages (apt, apk, rpm), language libs
# (bundler, composer, npm, pip), and config files inside image.
# WHY: Base images age instantly. CVE-2024-XXXX in glibc/openssl
# requires rebuild. Trivy is fast, accurate, low false positives.
# HOW: `exit-code: '1'` fails job on findings.
# `severity: 'HIGH,CRITICAL'` ignores Low/Medium noise.
# Output SARIF for GitHub Security Tab.
# ----------------------------------------------------------
- name: Trivy Container Scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ghcr.io/${{ github.repository }}:${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
exit-code: '1'
severity: 'HIGH,CRITICAL'
ignore-unfixed: true # Don't fail on vulns with no patch yet
vuln-type: 'os,library'
scanners: 'vuln,secret,config'
- name: Upload Trivy SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
category: trivy-container
# ----------------------------------------------------------
# Push Image to Registry (GHCR)
# Only happens if Trivy passes (exit-code 0).
# ----------------------------------------------------------
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Push Docker Image
run: |
docker push ghcr.io/${{ github.repository }}:${{ github.sha }}
# Optional: Tag 'latest' or branch name for Staging auto-deploy
- name: Tag and Push Branch Tag
if: github.ref != 'refs/heads/main'
run: |
docker tag ghcr.io/${{ github.repository }}:${{ github.sha }} ghcr.io/${{ github.repository }}:${{ github.head_ref || github.ref_name }}
docker push ghcr.io/${{ github.repository }}:${{ github.head_ref || github.ref_name }}
# ============================================================
# STAGE 3: DEPLOY STAGING & DYNAMIC ANALYSIS (DAST)
# ============================================================
deploy-staging:
name: Deploy Staging & DAST
needs: build-and-scan
runs-on: ubuntu-latest
environment: staging # Enables protection rules / secrets scoping
steps:
- name: Checkout Repository
uses: actions/checkout@v4
# ----------------------------------------------------------
# Kubernetes Deployment (Blue/Green or Rolling)
# WHAT: Update image tag in K8s deployment.
# WHY: Staging must mirror Production config (resources,
# sidecars, network policies) for valid DAST results.
# HOW: `kubectl set image` for imperative update (good for CI).
# GitOps (ArgoCD/Flux) is better for Production.
# ----------------------------------------------------------
- name: Configure kubectl
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.KUBECONFIG_STAGING }}
- name: Deploy to Staging
run: |
kubectl set image deployment/myapp-staging \
app=ghcr.io/${{ github.repository }}:${{ github.sha }} \
-n staging
kubectl rollout status deployment/myapp-staging -n staging --timeout=3m
# ----------------------------------------------------------
# 6. DAST - OWASP ZAP (Dynamic Application Security Testing)
# WHAT: Active scanner attacking running app (Spider + Active Scan).
# Finds runtime issues: Auth bypass, IDOR, XSS reflected/stored,
# Security Headers missing, CSP issues.
# WHY: SAST sees code; DAST sees *behavior*. Catches config issues
# (WAF off, debug mode on) and logic flaws invisible to static analysis.
# HOW: `zaproxy/action-full-scan`. `target` is Staging URL.
# `fail_action: true` fails job on High/Medium alerts.
# Use `.zap/rules.tsv` to exclude false positives (e.g., test endpoints).
# **Critical**: Run *after* deployment, *before* Production gate.
# ----------------------------------------------------------
- name: ZAP Full Scan
uses: zaproxy/action-full-scan@v0.10.0
with:
target: 'https://staging.myapp.com'
fail_action: true
issue_severity: 'medium' # Fail on Medium+
# rules_file_name: '.zap/rules.tsv' # Custom rules
# cmd_options: '-a' # Active scan only (spider done prior)
env:
# Pass auth tokens if app requires login for deep scan
# ZAP_AUTH_TOKEN: ${{ secrets.ZAP_STAGING_TOKEN }}
# Upload ZAP Report (HTML/JSON) as Artifact for review
- name: Upload ZAP Report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report-staging
path: report.html # Default output from action
# ============================================================
# STAGE 4: PRODUCTION DEPLOYMENT (GATED)
# ============================================================
deploy-production:
name: Deploy Production
needs: deploy-staging
# Gate: Only deploy to Prod from main branch (tag/release strategy preferred)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production # Requires approval if configured in Env settings
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Configure kubectl
uses: azure/k8s-set-context@v4
with:
kubeconfig: ${{ secrets.KUBECONFIG_PRODUCTION }}
# ----------------------------------------------------------
# Production Deployment Strategy
# WHAT: Rolling update with health checks.
# WHY: Zero downtime. Rollback capability via `kubectl rollout undo`.
# HOW: `kubectl set image` triggers rolling update.
# `rollout status` waits for readiness probes.
# **Best Practice**: Use GitOps (ArgoCD) for Prod.
# This imperative step is for demonstration simplicity.
# ----------------------------------------------------------
- name: Deploy to Production
run: |
kubectl set image deployment/myapp-prod \
app=ghcr.io/${{ github.repository }}:${{ github.sha }} \
-n production
# Wait for rollout to complete (new pods Ready, old pods terminated)
kubectl rollout status deployment/myapp-prod -n production --timeout=5m
# ----------------------------------------------------------
# Post-Deployment Verification (Smoke Tests)
# WHAT: Run critical user journey tests against Prod.
# WHY: DAST scans for vulns; Smoke tests verify *business logic* works.
# HOW: Run a small Postman/Newman collection or Playwright script.
# ----------------------------------------------------------
- name: Post-Deploy Smoke Tests
run: |
echo "Running smoke tests against https://myapp.com ..."
# newman run collection.json -e production.env
# npx playwright test --project=chromium --grep @smoke
echo "Smoke tests passed."
# ----------------------------------------------------------
# Notification & Audit Trail
# ----------------------------------------------------------
- name: Notify Success (Slack/Teams/Email)
if: success()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": "✅ Production Deployment Successful",
"blocks": [
{ "type": "section", "text": { "type": "mrkdwn", "text": "*✅ Production Deployment Successful*" } },
{ "type": "section", "fields": [
{ "type": "mrkdwn", "text": "*Repo:* ${{ github.repository }}" },
{ "type": "mrkdwn", "text": "*Commit:* ${{ github.sha }}" },
{ "type": "mrkdwn", "text": "*Actor:* ${{ github.actor }}" },
{ "type": "mrkdwn", "text": "*Image:* `ghcr.io/${{ github.repository }}:${{ github.sha }}`" }
]}
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
- name: Notify Failure
if: failure()
uses: slackapi/slack-github-action@v1.24.0
with:
payload: |
{
"text": "❌ Production Deployment FAILED",
"blocks": [
{ "type": "section", "text": { "type": "mrkdwn", "text": "*❌ Production Deployment FAILED*" } },
{ "type": "section", "text": { "type": "mrkdwn", "text": "Check GitHub Actions Run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } }
]
}
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
Deep Dive: Security Shift-Left Economics
The visual below illustrates the core financial argument for this pipeline structure. It is not just "best practice"—it is risk management economics.
COST OF FIXING A VULNERABILITY (Logarithmic Scale)
│
│ $10,000+ ┌──────────────────────────────────────┐
│ │ PRODUCTION (DAST / PenTest / Breach) │ ← Incident response, legal, reputation, downtime
│ $1,000 ├──────────────────────────────────────┤
│ │ ST