What You'll Build
In this exercise you will audit an intentionally insecure CI/CD pipeline configuration, identify security weaknesses using the STRIDE framework, implement fixes for the three highest-priority findings, and add a minimal set of supply chain security gates to a GitHub Actions workflow. By the end, the pipeline will block builds with HIGH/CRITICAL CVEs, prevent secret commits, and generate a signed SBOM.
Prerequisites
- Lessons 1, 2, and 3 completed.
- A GitHub repository with a basic Python FastAPI project and a GitHub Actions CI workflow.
- Docker installed locally.
- Python 3.11+ with pip installed.
- GitHub CLI (gh) or access to GitHub Actions settings in the repository.
Step 1 — Audit the Insecure Pipeline
Start with the intentionally insecure workflow below. Read through it carefully and identify every security weakness. Use the STRIDE threat categories from Lesson 2 to classify each finding. You should find at least six distinct issues before comparing with the audit report in Step 2.
# .github/workflows/insecure-pipeline.yaml — FIND THE ISSUES
name: Build and Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
env:
AWS_ACCESS_KEY_ID: AKIAIOSFODNN7EXAMPLE
AWS_SECRET_ACCESS_KEY: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
DB_PASSWORD: cricketpulse_prod_pass123
steps:
- uses: actions/checkout@main # unpinned action
- name: Install dependencies
run: pip install -r requirements.txt # no lock file, no audit
- name: Build Docker image
run: docker build -t cricketpulse:latest .
# Dockerfile uses: FROM python:3.12-slim (no digest pin)
- name: Run tests
run: pytest
- name: Push to registry
run: |
docker login -u admin -p ${{ secrets.DOCKER_PASSWORD }}
docker push myregistry.io/cricketpulse:latest # mutable tag
- name: Deploy to production
run: |
aws eks update-kubeconfig --name prod-cluster
kubectl apply -f k8s/
# No image signature verification
# No approval gate
- name: Notify team
run: |
curl -X POST https://hooks.slack.com/services/T123/B456/XXXXXXXXXXX -d "{"text": "Deploy complete. DB_PASSWORD=$DB_PASSWORD"}"Step 2 — Review the Audit Findings
Compare your findings against this reference audit. Every finding has a STRIDE classification, DREAD score, and recommended fix. The six highest-priority findings are the ones you will fix in Steps 3 and 4.
# Reference audit report
| ID | Finding | STRIDE | DREAD | Fix |
|----|--------------------------------------|--------|-------|-----|
| F1 | AWS keys hardcoded in workflow env | I, E | 14 | OIDC ephemeral credentials |
| F2 | DB_PASSWORD leaked in Slack message | I | 13 | Remove from notify step |
| F3 | Unpinned action (checkout@main) | T | 12 | Pin to commit SHA |
| F4 | No dependency lock file or audit | T | 11 | pip freeze + pip-audit |
| F5 | Docker base image unpinned | T | 10 | Pin to SHA256 digest |
| F6 | Mutable image tag (:latest) | T | 10 | Tag with git SHA |
| F7 | No approval gate before production | E | 9 | Required reviewer environment |
| F8 | No image signature verification | T | 9 | cosign verify in deploy job |Step 3 — Fix the Top Three Findings
Fix F1, F2, and F3 first — the highest DREAD scores. Replace hardcoded AWS credentials with OIDC, remove the credential leak from the Slack notification, and pin the checkout action to an immutable commit SHA.
# Fix F1: Replace hardcoded AWS credentials with OIDC
# Delete env block with hardcoded credentials
# Add OIDC permissions and aws-actions/configure-aws-credentials
permissions:
id-token: write # required for OIDC
contents: read
jobs:
build:
runs-on: ubuntu-latest
# No env block with credentials
steps:
- name: Configure AWS credentials via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-deploy
aws-region: ap-south-1
# Credentials are ephemeral — expire after the job completes
# No long-lived keys in any environment variable
# Fix F2: Remove credentials from Slack notification
- name: Notify team
run: |
curl -X POST https://hooks.slack.com/services/T123/B456/XXXXXXXXXXX -d '{"text": "Deploy complete to production cluster."}'
# DB_PASSWORD is NOT included — never log credentials
# Fix F3: Pin checkout action to commit SHA (not tag)
# Find current SHA: https://github.com/actions/checkout/tags
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1Step 4 — Add Supply Chain Security Gates
Now add the supply chain gates from Lesson 3: dependency lock file with pip-audit, Docker base image digest pinning, Trivy image scanning blocking on HIGH/CRITICAL, and SBOM generation. These gates transform the pipeline from a security liability into a security asset.
# Complete secure pipeline — add these steps to your workflow
# Supply chain gate 1: Dependency audit
- name: Lock and audit dependencies
run: |
pip install pip-audit
pip-audit --requirement requirements.txt --format json --output pip-audit-report.json
# Exit non-zero on HIGH or CRITICAL CVEs (pip-audit default)
# Supply chain gate 2: Build with digest-pinned base image
# Update your Dockerfile:
# FROM python:3.12-slim@sha256:<digest>
- name: Get base image digest
run: |
docker pull python:3.12-slim
docker inspect python:3.12-slim --format '{{index .RepoDigests 0}}'
# Copy output to Dockerfile FROM line
# Supply chain gate 3: Image scan
- name: Trivy vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: cricketpulse:${{ github.sha }}
exit-code: '1'
severity: 'HIGH,CRITICAL'
format: 'sarif'
output: 'trivy-results.sarif'
# Supply chain gate 4: Generate and upload SBOM
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: cricketpulse:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Upload SBOM artifact
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
retention-days: 90
# Tag image with git SHA (not :latest)
- name: Push image with immutable tag
run: |
docker tag cricketpulse:${{ github.sha }} myregistry.io/cricketpulse:${{ github.sha }}
docker push myregistry.io/cricketpulse:${{ github.sha }}Verify Your Work
Run through this verification checklist. A passing pipeline should show zero hardcoded credentials, a blocked build on a HIGH CVE, and a generated SBOM artifact.
# Verification tests
# 1. Confirm no credentials in workflow files
grep -r "AKIA\|password\|secret" .github/workflows/ && echo "FAIL: credentials found" || echo "PASS: no credentials"
# 2. Confirm pip-audit runs and finds no HIGH/CRITICAL
pip install pip-audit
pip-audit --requirement requirements.txt
# Expect: No known vulnerabilities found (or patch before proceeding)
# 3. Test Trivy blocks on HIGH CVE — inject a known vulnerable package
echo "cryptography==41.0.0" >> requirements.txt # CVE-2023-49083 HIGH
pip install cryptography==41.0.0
trivy image --exit-code 1 --severity HIGH,CRITICAL cricketpulse:test
# Expect: exit code 1 (build blocked)
# Revert: remove the test line from requirements.txt
# 4. Confirm SBOM is generated
ls -la sbom.spdx.json
python3 -c "import json; d=json.load(open('sbom.spdx.json')); print(f'SBOM packages: {len(d["packages"])}')"
# Expect: SBOM packages: > 0
# 5. Confirm Docker image tagged with SHA not :latest
docker images | grep cricketpulse
# Expect: cricketpulse <git-sha> ... (no 'latest' tag in registry push)When running Trivy in CI, always set --exit-code 1 and --severity HIGH,CRITICAL together. Without --exit-code 1, Trivy reports vulnerabilities but does not fail the build — a common misconfiguration that gives a false sense of security while allowing vulnerable images to proceed to production.
GitHub Actions provides a Security tab (under repository Settings > Security > Code scanning) where Trivy SARIF output is automatically displayed as code scanning alerts. Uploading the trivy-results.sarif file using github/codeql-action/upload-sarif makes all vulnerabilities visible in the pull request interface, not just in CI logs.
- Fix credential exposures first — hardcoded keys and credential leaks in logs are the highest DREAD-score findings.
- Pin GitHub Actions to commit SHAs (not tags) to prevent supply chain attacks via action repo tampering.
- pip-audit + requirements.txt lock file + Trivy image scan = three complementary vulnerability detection layers.
- Tag images with git SHA instead of :latest — mutable tags make rollback and incident response harder.
- SBOM artifacts should be retained alongside every image push for future CVE auditing.