What You'll Build
In this exercise you will integrate all Module 2 security tools into a single hardened GitHub Actions pipeline for the CricketPulse API. The finished pipeline will: scan dependencies with pip-audit, perform SAST with Semgrep and Bandit, scan the container image with Trivy, generate and sign an SBOM, and use OIDC for all cloud authentication — replacing every static credential with ephemeral tokens.
Prerequisites
- Lessons 5, 6, and 7 completed.
- A GitHub repository with the CricketPulse FastAPI app from Module 1.
- AWS account with an IAM OIDC identity provider configured for GitHub Actions.
- Docker installed and a container registry (GHCR or ECR) available.
- GitHub Advanced Security enabled (for SARIF upload to Security tab).
Step 1 — Configure OIDC Authentication
Replace all static AWS credentials with an OIDC identity provider. Create an IAM role that GitHub Actions can assume via OIDC, scoped to your specific repository and branch. This eliminates long-lived credentials from your pipeline entirely.
# 1. Create the OIDC provider in AWS (one-time setup)
aws iam create-open-id-connect-provider --url https://token.actions.githubusercontent.com --client-id-list sts.amazonaws.com --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1
# 2. Create IAM role trust policy — restrict to your repo
cat > trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::ACCOUNT_ID:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:myorg/cricketpulse:*"
}
}
}]
}
EOF
aws iam create-role --role-name github-actions-cricketpulse --assume-role-policy-document file://trust-policy.json
# 3. Attach least-privilege policy (ECR push + EKS deploy only)
aws iam attach-role-policy --role-name github-actions-cricketpulse --policy-arn arn:aws:iam::ACCOUNT_ID:policy/cricketpulse-deploy-policyStep 2 — Build the Hardened Pipeline
Assemble all the security gates into a single workflow with parallel jobs for SAST, dependency scanning, and image scanning. The deploy job depends on all security gates passing and uses OIDC credentials scoped to the production deployment role.
# .github/workflows/secure-pipeline.yaml
name: Secure Build and Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
permissions:
contents: read
id-token: write # required for OIDC
security-events: write # required for SARIF upload
packages: write # required for GHCR push
jobs:
# ── Security Gates (run in parallel) ────────────────────────────────
dependency-scan:
name: Dependency Audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: actions/setup-python@82c7e631bb3cdc910f68e0081d67478d79c6982d # v5.1.0
with:
python-version: "3.12"
- name: pip-audit
run: |
pip install pip-audit
pip-audit --requirement requirements.txt --format json --output pip-audit.json
python3 -c "
import json, sys
r = json.load(open('pip-audit.json'))
vulns = r.get('dependencies', [])
highs = [v for d in vulns for v in d.get('vulns',[]) if v.get('fix_versions')]
if highs:
print(f'FAIL: {len(highs)} patchable vulnerabilities found')
sys.exit(1)
print('PASS: no patchable vulnerabilities')
"
sast:
name: SAST Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/python
p/secrets
- name: Bandit
run: |
pip install bandit
bandit -r src/ -f sarif -o bandit.sarif -ll || true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: bandit.sarif
image-scan:
name: Image Scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Build image
run: docker build -t cricketpulse:${{ github.sha }} .
- name: Hadolint
uses: hadolint/hadolint-action@v3.1.0
with:
dockerfile: Dockerfile
failure-threshold: error
- name: Trivy scan
uses: aquasecurity/trivy-action@master
with:
image-ref: cricketpulse:${{ github.sha }}
format: sarif
output: trivy.sarif
exit-code: '1'
severity: HIGH,CRITICAL
ignore-unfixed: true
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: trivy.sarif
# ── Deploy (depends on all gates) ───────────────────────────────────
deploy:
name: Deploy to Production
needs: [dependency-scan, sast, image-scan]
runs-on: ubuntu-latest
environment:
name: production
url: https://cricketpulse.example.com
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Configure AWS via OIDC
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789:role/github-actions-cricketpulse
aws-region: ap-south-1
- name: Login to ECR
id: ecr-login
uses: aws-actions/amazon-ecr-login@v2
- name: Push image
run: |
docker tag cricketpulse:${{ github.sha }} ${{ steps.ecr-login.outputs.registry }}/cricketpulse:${{ github.sha }}
docker push ${{ steps.ecr-login.outputs.registry }}/cricketpulse:${{ github.sha }}
- name: Generate and upload SBOM
uses: anchore/sbom-action@v0
with:
image: cricketpulse:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
retention-days: 365
- name: Deploy to EKS
run: |
aws eks update-kubeconfig --name prod-cluster --region ap-south-1
kubectl set image deployment/cricketpulse-api api=${{ steps.ecr-login.outputs.registry }}/cricketpulse:${{ github.sha }}
kubectl rollout status deployment/cricketpulse-api --timeout=120sStep 3 — Add the Required Reviewer Gate
Configure a GitHub environment with required reviewers so that the deploy job cannot proceed without a named approver even if all automated gates pass. This implements separation of duties — the engineer who writes the code cannot also approve their own deployment to production.
# GitHub UI: Settings > Environments > production
# Enable: Required reviewers
# Add: your team lead or ops team as required reviewers
# With required reviewer, the pipeline pauses after all automated gates
# and sends a notification to the reviewer:
# "Deploy job waiting for approval — all security gates passed"
# Reviewer clicks Approve or Reject with a comment
# Verify in workflow: the environment block handles this automatically
deploy:
environment:
name: production # triggers required reviewer check
url: https://cricketpulse.example.com
# To test: push a commit and observe the pipeline pause at the deploy job
# In GitHub Actions UI: a yellow "Waiting for approval" status appears
# Reviewer approves: job proceeds
# Reviewer rejects: job fails with reviewer's commentVerify Your Work
Run these verification checks to confirm all five security layers are active and functional before marking the exercise complete.
# Verification checklist
# 1. Confirm no static credentials in workflows
grep -r "AWS_ACCESS_KEY_ID\|AWS_SECRET_ACCESS_KEY" .github/ && echo "FAIL: static credentials found" || echo "PASS: no static credentials"
# 2. Confirm all Actions are pinned to commit SHAs
grep "uses:" .github/workflows/*.yaml | grep "@" | grep -v "@[a-f0-9]\{40\}" && echo "WARN: some actions not pinned to SHA" || echo "PASS: all actions pinned to SHA"
# 3. Trigger pipeline and confirm parallel gate execution
# Push a commit and verify all three gates run simultaneously in GitHub Actions
# Expected: dependency-scan, sast, image-scan all show "In progress" at the same time
# 4. Test that a vulnerable dependency blocks the build
echo "cryptography==41.0.0" >> requirements.txt
git add requirements.txt && git commit -m "test: inject vulnerable dep"
git push
# Expected: dependency-scan job fails; deploy job does not run
# Revert
git revert HEAD && git push
# 5. Verify SBOM is uploaded as artifact
# After a successful deploy: Actions > your run > Artifacts
# Expect: sbom-<sha>.zip with sbom.spdx.json inside
# 6. Verify required reviewer gate
# Push to main and observe deploy job status
# Expected: "Waiting for review" before deploy proceedsThe required reviewer gate only provides value if reviewers actually read the deployment context before approving. Include the commit SHA, PR link, and a summary of what changed in the deployment notification. A reviewer who approves without reading provides no additional security — they just add latency.
GitHub's environment protection rules also support 'Wait timer' (delay before deploy) and 'Deployment branches' (only specific branches can deploy to this environment). Combine required reviewers + deployment branches (main only) + wait timer (15 minutes) for a layered production protection that resists both mistakes and rushed deployments.
- OIDC authentication eliminates long-lived AWS credentials from the pipeline — credentials are ephemeral per job.
- Parallel security gates (dependency scan, SAST, image scan) reduce total pipeline time vs sequential execution.
- All GitHub Actions must be pinned to commit SHAs to prevent supply chain attacks via action repo tampering.
- GitHub environment required reviewers implements separation of duties for production deployments.
- SBOM artifacts should be retained for 365 days alongside the image they describe for future CVE auditing.