Shift-left security for containerised apps on AWS EKS. Click a job to inspect it.
π Click any job card to see its steps, tools, and details here.
All jobs defined in secopspipeline.yaml with triggers and timeouts.
| # | Job | Trigger | Timeout | Needs | Purpose |
|---|---|---|---|---|---|
| 0 | dependency-review |
PR only | β | β | Blocks merge on HIGH/CRITICAL vulnerable dependencies |
| 1 | code-quality |
push | 20 min | β | Gitleaks + CodeQL + Semgrep + Super-Linter |
| 2 | build |
push | 20 min | code-quality |
Build & push Docker image to ECR with SBOM and provenance |
| 3 | scan-sign |
push | 15 min | build |
Trivy CVE scan, SARIF upload, Cosign sign |
| 4 | deploy-staging |
push | 30 min | scan-sign |
Helm + Argo Rollouts canary to staging EKS |
| 5 | dast-staging |
push | 20 min | deploy-staging |
OWASP ZAP baseline against live staging URL |
| 6 | promote |
push | 60 min | dast-staging |
Manual approval gate on production environment |
| 7 | deploy-prod |
push | 45 min | promote, scan-sign |
Deploy exact same digest to production EKS |
| 8 | rollback-prod |
on failure | 20 min | deploy-prod |
Auto-rollback via argo rollouts undo |
| Event | Branches / Patterns | Jobs Fired |
|---|---|---|
| push | main |
code-quality β build β scan-sign β deploy-staging β dast-staging β promote β deploy-prod |
| push | v*.*.* tags |
Same as push to main |
| pull_request | targeting main |
dependency-review only |
Every control, the tool that enforces it, and whether it blocks the pipeline.
| Control | Tool | Stage | Blocks? |
|---|---|---|---|
| Secrets in git history | Gitleaks | code-quality |
β Yes |
| SAST (source-level) | CodeQL | code-quality |
β Yes |
| SAST (OWASP Top-10) | Semgrep | code-quality |
β Yes |
| Code linting (50+ langs) | Super-Linter | code-quality |
β Yes |
| SCA (dependency vulns) | dependency-review-action | dependency-review |
β Yes (PRs) |
| Container CVE scan | Trivy | scan-sign |
β Yes (HIGH/CRIT) |
| CVE visibility / reporting | Trivy SARIF β GitHub Security tab | scan-sign |
β Report only |
| Supply chain signing | Cosign (Sigstore OIDC) | scan-sign |
β Yes (sign) |
| Deploy-time verification | Cosign verify | deploy-staging, deploy-prod |
β Yes |
| SBOM & provenance | docker/build-push-action | build |
β Attestation |
| DAST (runtime attacks) | OWASP ZAP | dast-staging |
β Yes |
| Manual promotion gate | GitHub Environment approval | promote |
β Yes |
Architectural choices that make this pipeline supply-chain safe and operationally robust.
The image is built once in build and tagged by github.sha. All
subsequent jobs reference the immutable sha256:β¦ digest β never a mutable tag. This guarantees
staging and production receive byte-for-byte identical images.
AWS credentials are federated via GitHub Actions OIDC (id-token: write). No
AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY secrets are stored anywhere. Each job
assumes a least-privilege IAM role scoped to its action (ECR push, EKS staging, EKS prod).
Even if ECR were compromised post-sign, cosign verify at deploy time rejects unsigned or
tampered images. Verification uses Sigstore's public Fulcio/Rekor transparency log.
Both staging and production use Argo Rollouts with a canary strategy. Traffic shifts
incrementally. If the rollout health check fails, the rollback-prod job issues
argo rollouts undo automatically.
cancel-in-progress: true ensures only one run per ref is active at a time. A fast follow-up push
cancels the previous run rather than queuing a stale deployment.
CodeQL auto-detects the language. Semgrep rules apply universally. The only app-specific step is
unit/integration testing β add it in code-quality for Python (pytest), Node
(npm test), Go (go test), or Java (mvn test).
Secrets, IAM roles, and GitHub environments needed before running this pipeline.
| Secret | Used In |
|---|---|
AWS_ACCOUNT_ID |
All configure-aws-credentials steps |
| Role | Job |
|---|---|
github-actions-ecr |
build, scan-sign |
github-actions-eks-staging |
deploy-staging |
github-actions-eks-prod |
deploy-prod, rollback-prod |
| Environment | Protection Rule |
|---|---|
staging |
None (auto-deploy) |
production |
Required reviewers (manual) |
| Field | Location |
|---|---|
| DAST target URL | dast-staging β target: |
| EKS cluster name (staging) | deploy-staging step |
| EKS cluster name (prod) | deploy-prod step |
IMAGE_NAME |
top-level env: |
AWS_REGION |
top-level env: |
vars.STAGING_URL) instead of hardcoding it in the YAML to avoid manual edits on every deploy.
.github/workflows/setup-tools.yml. It installs Cosign, kubectl,
Helm, and Argo Rollouts CLI and is called via
uses: ./.github/workflows/setup-tools.yml in deploy jobs.
Raw workflow files β copy and place in .github/workflows/.
name: DevSecOps Pipeline v3 (Progressive + Promotion)
on:
push:
branches: [ main ]
tags: [ "v*.*.*" ]
pull_request:
branches: [ main ]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
id-token: write
contents: read
security-events: write
pull-requests: read
env:
AWS_REGION: us-east-1
ECR_REGISTRY: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.us-east-1.amazonaws.com
IMAGE_NAME: my-app
jobs:
# βββ 0. DEPENDENCY REVIEW (PRs only) βββββββββββββββββββββββββββββββββββββββ
dependency-review:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- uses: actions/checkout@v4
- uses: actions/dependency-review-action@v4
with:
fail-on-severity: high
# βββ 1. CODE QUALITY GATE βββββββββββββββββββββββββββββββββββββββββββββββββββ
code-quality:
runs-on: ubuntu-latest
if: github.event_name == 'push'
timeout-minutes: 20
permissions:
security-events: write
actions: read
contents: read
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- uses: github/codeql-action/init@v3
with:
languages: auto
- uses: github/codeql-action/autobuild@v3
- uses: github/codeql-action/analyze@v3
with:
category: codeql-source
- uses: returntocorp/semgrep-action@v1
with:
config: >
p/owasp-top-ten p/secrets p/ci
- uses: super-linter/super-linter@v7
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
DEFAULT_BRANCH: main
VALIDATE_ALL_CODEBASE: false
# βββ 2. BUILD ONCE βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
build:
runs-on: ubuntu-latest
needs: code-quality
if: github.event_name == 'push'
timeout-minutes: 20
outputs:
digest: ${{ steps.build.outputs.digest }}
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-ecr
aws-region: ${{ env.AWS_REGION }}
- uses: aws-actions/amazon-ecr-login@v2
- name: Build and Push Image
id: build
uses: docker/build-push-action@v5
with:
context: .
push: true
provenance: true
sbom: true
tags: ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
# βββ 3. SECURITY SCAN + SIGN ββββββββββββββββββββββββββββββββββββββββββββββββ
scan-sign:
runs-on: ubuntu-latest
needs: build
timeout-minutes: 15
outputs:
image_uri: ${{ steps.uri.outputs.image_uri }}
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-ecr
aws-region: ${{ env.AWS_REGION }}
- uses: aws-actions/amazon-ecr-login@v2
- uses: aquasecurity/trivy-action@0.24.0
with:
image-ref: ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
severity: CRITICAL,HIGH
exit-code: "1"
- uses: aquasecurity/trivy-action@0.24.0
if: always()
with:
image-ref: ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}
format: sarif
output: trivy-results.sarif
- uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-results.sarif
category: trivy-container
- name: Compute Image URI (digest-based)
id: uri
run: |
echo "image_uri=${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build.outputs.digest }}" >> $GITHUB_OUTPUT
- name: Sign Image
run: cosign sign --yes ${{ steps.uri.outputs.image_uri }}
# βββ 4. DEPLOY TO STAGING (CANARY) βββββββββββββββββββββββββββββββββββββββββ
deploy-staging:
runs-on: ubuntu-latest
needs: scan-sign
timeout-minutes: 30
environment:
name: staging
url: https://staging.my-app.example.com
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: azure/setup-kubectl@v4
- uses: azure/setup-helm@v4
- name: Install Argo Rollouts CLI
run: |
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-eks-staging
aws-region: ${{ env.AWS_REGION }}
- run: aws eks update-kubeconfig --name my-eks-staging
- name: Verify Image Signature
run: |
cosign verify \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
${{ needs.scan-sign.outputs.image_uri }}
- name: Deploy via Helm (Argo Rollout)
run: |
helm upgrade --install my-app ./helm/my-app \
--namespace staging --create-namespace \
--set image.digest=${{ needs.build.outputs.digest }}
- name: Monitor Canary Rollout
run: kubectl argo rollouts status my-app -n staging --timeout 10m
# βββ 5. DAST β STAGING ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
dast-staging:
runs-on: ubuntu-latest
needs: deploy-staging
timeout-minutes: 20
steps:
- uses: zaproxy/action-baseline@v0.12.0
with:
target: https://staging.my-app.example.com # β update this
fail_action: true
issue_title: "ZAP Baseline Scan β Staging"
# βββ 6. PROMOTION GATE (MANUAL APPROVAL) ββββββββββββββββββββββββββββββββββββ
promote:
runs-on: ubuntu-latest
needs: dast-staging
timeout-minutes: 60
environment:
name: production
steps:
- run: echo "Approved for production deployment"
# βββ 7. DEPLOY TO PRODUCTION (CANARY) βββββββββββββββββββββββββββββββββββββββ
deploy-prod:
runs-on: ubuntu-latest
needs: [ promote, scan-sign ]
timeout-minutes: 45
environment:
name: production
url: https://my-app.example.com
steps:
- uses: actions/checkout@v4
- uses: sigstore/cosign-installer@v3
- uses: azure/setup-kubectl@v4
- uses: azure/setup-helm@v4
- name: Install Argo Rollouts CLI
run: |
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-eks-prod
aws-region: ${{ env.AWS_REGION }}
- run: aws eks update-kubeconfig --name my-eks-prod
- name: Verify Image Signature
run: |
cosign verify \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
${{ needs.scan-sign.outputs.image_uri }}
- name: Deploy via Helm (same digest)
run: |
helm upgrade --install my-app ./helm/my-app \
--namespace production --create-namespace \
--set image.digest=${{ needs.build.outputs.digest }}
- name: Monitor Canary Rollout
run: kubectl argo rollouts status my-app -n production --timeout 15m
# βββ 8. ROLLBACK ON PRODUCTION FAILURE ββββββββββββββββββββββββββββββββββββββ
rollback-prod:
runs-on: ubuntu-latest
needs: deploy-prod
if: failure()
timeout-minutes: 20
environment:
name: production
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/github-actions-eks-prod
aws-region: ${{ env.AWS_REGION }}
- run: aws eks update-kubeconfig --name my-eks-prod
- name: Install Argo Rollouts CLI
run: |
curl -LO https://github.com/argoproj/argo-rollouts/releases/latest/download/kubectl-argo-rollouts-linux-amd64
chmod +x kubectl-argo-rollouts-linux-amd64
sudo mv kubectl-argo-rollouts-linux-amd64 /usr/local/bin/kubectl-argo-rollouts
- name: Rollback Argo Rollout
run: |
kubectl argo rollouts undo my-app -n production
kubectl argo rollouts status my-app -n production --timeout 10m