Follow a container image from source checks through signing, staged deployment, approval, and rollback on AWS EKS. Select a job to inspect its work.
Select a job to see its steps, tools, and dependencies.
Use the trigger, dependency, and timeout columns to see when each job runs and what must finish first.
| # | 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 |
Match each security check to the pipeline stage that runs it, then see whether a finding blocks delivery or records evidence.
| 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 |
These choices reduce artifact substitution, credential exposure, and deployment risk. Their protection still depends on the referenced policies, tools, and environment settings.
The image is built once in build and tagged by github.sha. All
subsequent jobs reference the immutable sha256:β¦ digest instead of a mutable tag. Staging and
production therefore request the same image manifest.
GitHub Actions requests an OIDC token and exchanges it for temporary AWS role credentials. This removes stored IAM access keys from the workflow. Each job uses a role scoped to its task, such as pushing to ECR or deploying to one EKS environment.
The deployment jobs run cosign verify against the selected digest before using it. Whether this
blocks an unwanted image depends on the verification identity and issuer constraints configured in the
workflow, so treat those values as part of the policy.
Both environments use an Argo Rollouts canary so traffic can move in stages. The workflow waits for the
rollout result and runs argo rollouts undo after a failed production deployment. The configured
analysis and health checks determine what the rollout can detect.
The concurrency group identifies runs for the same ref, and cancel-in-progress: true cancels an
older matching run when a newer one starts. Review this behavior carefully for deployment jobs that should
finish rather than be interrupted.
The sample leaves application tests for you to add to code-quality. Configure CodeQL,
Semgrep, the linter, and the test command for the languages and build system in the repository; their
defaults do not make the workflow language-independent by themselves.
Configure these repository values, AWS roles, and GitHub environments before the workflow can authenticate or deploy.
| 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.
Review the permissions, action versions, role ARNs, environments, and application commands before adapting these workflow files.
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