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

Unlock Full Tutorial

This chapter is paid content. Join the project to unlock over 5000 words of deep analysis, including 10+ god-tier Prompts and real Source Code examples!