⬡ CLOUD ENGAGEMENT — PHASE 2 Cloud Security II — Network, Containers & IaC

⬡ Engagement — Operation: OPEN BUCKET, Phase 2

Cloud Security II
Network, Containers & IaC

Six weeks after your Phase 1 remediation plan, Meridian Retail Group's SOC 2 auditor came back with a follow-up letter: IAM, storage, and logging passed review — but the audit scope is expanding. Meridian just containerized their checkout service onto Kubernetes, their platform team has been hand-editing Terraform in production for two years with no review process, and nobody has ever looked at whether their security groups actually do what their names say. This is the second half of the job: network, containers, and the pipeline that ships infrastructure changes in the first place.

Engagement : Meridian Retail Group — Phase 2: Network, Containers & IaC
Trigger : Auditor follow-up — expanded scope after Phase 1 passed
New surface : Kubernetes (checkout service) · unreviewed Terraform · VPC security groups
Your mandate : Network → Containers/K8s → IaC Scanning → CI/CD Pipeline Security
Status : Engagement OPEN — practice environment only, see safety rules below
Prerequisite: this workbook continues directly from Cloud Security: Hands-On Workbook (Modules 0–5). It assumes you already have a free-tier AWS account, the AWS CLI configured under a non-root IAM user, and are comfortable with Modules 1–4 of that workbook (IAM, storage, logging, Prowler). If any of that is unfamiliar, complete Phase 1 first.
Difficulty:Intermediate–Advanced
Est. time:10–14 hours
Modules:5
Labs:15

⚠ Read This Before Lab 0.1 — Same Rules, One New One

Everything from Phase 1's safety rules still applies — own infrastructure or named practice targets only, billing alarms, destroy-after-use, never root. Phase 2 adds one more, because this module's tooling lives in CI/CD pipelines and supply chains, not just cloud consoles.

This is general training guidance, not legal or financial advice. Cloud provider pricing and free-tier terms change — always check current limits before deploying a lab, especially EKS/AKS/GKE.

📑 Table of Contents

Module 0 – Orientation & Setup
Module 1 – Network Security
Module 2 – Container & Kubernetes Security
Module 3 – Infrastructure-as-Code Scanning
Module 4 – CI/CD & Supply Chain Security
🧭

Module 0 – Orientation & Phase 2 Setup

3 labs · What's new in Phase 2, the tool stack, and the Trivy supply-chain incident as a live case study
0.1
What Phase 2 Covers and WhyThe three layers Phase 1 left untouched
ObjectiveUnderstand the three attack surfaces Phase 1 didn't cover and why each one matters for Meridian's expanded audit scope.
SurfaceWhy Phase 1 Skipped ItWhy Phase 2 Can't
Network (VPCs, Security Groups)IAM misconfig is the #1 breach vector — that came firstAn over-permissive security group exposes every service behind it regardless of IAM correctness
Containers & KubernetesMeridian wasn't running K8s yetCheckout service just containerized — default K8s settings are notoriously insecure
Infrastructure-as-CodeCan't scan what doesn't exist yet as codeTwo years of hand-edited production Terraform, never peer-reviewed for security

Phase 1 and Phase 2 together form the complete picture: you can have perfect IAM and still have a publicly accessible EC2 instance because a security group says 0.0.0.0/0 on port 22. Both layers have to be correct at the same time.

📋
There's also a fourth area — CI/CD and supply-chain security — that neither Phase covers in isolation. Module 4 adds it specifically because the pipeline that *delivers* infrastructure changes is itself a security boundary.
0.2
Install the Phase 2 Tool StackCheckov · kube-bench · kube-hunter · kubectl · kind
ObjectiveInstall every tool Phase 2 uses, with exact verification steps confirming each one works before you need it.CLIK8sIaC
  1. Install Checkov

    The IaC scanner you'll use in Module 3 — Palo Alto Networks, actively maintained, unaffected by the Trivy supply-chain incident.

  2. Install kubectl

    The Kubernetes CLI.

  3. Install kind

    Kubernetes-in-Docker — spins up a local cluster instantly with no cloud cost, used for Modules 2 and 3's K8s labs.

  4. Install kube-bench

    CIS Kubernetes Benchmark checker — as a direct binary download, not via a GitHub Action (see Lab 0.3).

  5. Install kube-hunter

    K8s penetration testing tool — same, binary install only.

  6. Verify each tool responds

    Run the version check for each one before moving on.

# Checkov
pip install checkov
checkov --version

# kubectl (Linux)
curl -LO "https://dl.k8s.io/release/$(curl -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x kubectl && sudo mv kubectl /usr/local/bin/
kubectl version --client

# kind (Kubernetes-in-Docker) — binary install
curl -Lo ./kind https://kind.sigs.k8s.io/dl/v0.24.0/kind-linux-amd64
chmod +x kind && sudo mv kind /usr/local/bin/
kind version

# kube-bench — binary download, NOT via GitHub Action
wget https://github.com/aquasecurity/kube-bench/releases/latest/download/kube-bench_linux_amd64.tar.gz
tar -xzf kube-bench_linux_amd64.tar.gz && sudo mv kube-bench /usr/local/bin/
kube-bench version

# kube-hunter — pip install
pip install kube-hunter
kube-hunter --help
ToolVerify CommandExpected
checkovcheckov --versionCheckov 3.x.x
kubectlkubectl version --clientClient version output, no server error
kindkind versionkind vX.X.X go...
kube-benchkube-bench versionVersion string
kube-hunterkube-hunter --helpHelp text with --remote, --cidr, --pod options
0.3
Case Study — The Trivy Supply-Chain CompromiseMarch 2026 · What happened, why it matters, and how your tool choices reflect it
ObjectiveUnderstand a real, recent supply-chain attack against a widely-used security scanner — and learn exactly what you would have done differently before Module 4 explains the general defenses.
Real Incident — March 19, 2026

⚡ The Trivy GitHub Actions Supply-Chain Compromise

On March 19, 2026, a threat actor known as TeamPCP used credentials stolen three weeks earlier to force-push 76 of 77 version tags in the aquasecurity/trivy-action GitHub repository — the official GitHub Action used to run Trivy in CI/CD pipelines. Every pipeline referencing a tag like aquasecurity/trivy-action@v0.24.0 automatically pulled and executed the attacker's code. The malicious payload ran silently before the legitimate Trivy scan, so affected pipelines appeared to complete normally while harvesting AWS/Azure/GCP credentials, SSH keys, Kubernetes service account tokens, and other secrets.

This was the second compromise in under a month — the first (February 28) extracted a privileged access token via a misconfigured pull_request_target workflow. Incomplete credential rotation during the first incident gave the attacker a foothold to launch the tag-poisoning attack three weeks later. The exposure window was roughly 3–6 hours before Aqua Security identified and removed the malicious artifacts.

Why it's in this workbook: Trivy was the "recommended" tool for most of what Module 3 covers just four months ago. This incident is the reason this workbook uses Checkov for IaC scanning and installs kube-bench as a direct binary rather than via a GitHub Action — and Module 4 turns this specific attack into the lesson plan for supply-chain security.

Unsafe Pattern (Used Before the Incident)Safe Pattern (What You'll Do in This Workbook)
uses: aquasecurity/trivy-action@v0.24.0Pin to a full 40-char commit SHA, not a mutable version tag
Install kube-bench via its GitHub ActionDownload the binary directly from GitHub Releases (the binary itself was safe — only the Action was compromised)
Trivy as sole IaC/container scanner in CI/CDCheckov for IaC scanning; evaluate any tool's incident history before putting it in your pipeline
No monitoring of outbound CI/CD network callsAlert on unexpected outbound connections from runner environments
⚠️
Trivy's scanning capabilities themselves are unaffected and the tool is widely used — the incident was in its GitHub Action distribution mechanism, not in its detection engine. The lesson is about how you install and reference CI/CD tools, not about avoiding Trivy entirely.
🌐

Module 1 – Network Security

3 labs · VPC architecture, security group auditing, and traffic visibility with VPC Flow Logs
1.1
VPC Architecture ReviewPublic vs private subnets · routing · what "private" actually means
ObjectiveUnderstand Meridian's VPC layout and audit whether their "private" subnets are actually isolated from the internet.AWS
  1. List all VPCs in your account

    Note any that are still using the Default VPC — this should be deleted in production accounts.

  2. Examine route tables for each subnet

    A subnet whose route table has a 0.0.0.0/0 → internet gateway route is public, regardless of what it's called.

  3. Check NAT gateway vs internet gateway

    Private subnets route outbound traffic through a NAT gateway — they can reach out but can't be reached in.

  4. Confirm whether any production instances are in public subnets

    Databases and internal services should never be directly internet-routable, even if a security group restricts port access.

# list all VPCs in your account
aws ec2 describe-vpcs --query 'Vpcs[*].{ID:VpcId,CIDR:CidrBlock,Default:IsDefault}'

# inspect route tables — look for 0.0.0.0/0 with gateway destination
aws ec2 describe-route-tables --query 'RouteTables[*].{ID:RouteTableId,Routes:Routes[*].{Dest:DestinationCidrBlock,GW:GatewayId}}'
FindingRisk
Default VPC still existsPermissive default security group + internet gateway attached — a blast radius waiting to happen
0.0.0.0/0 → igw-xxx in a "private" subnetThe subnet is public regardless of its name — anything in it is internet-reachable
Database instance in a public subnetNetwork layer doesn't protect it — only security group rules do, and those can be misconfigured
💡
The Default VPC is a known bad practice and should be deleted from every production account — it comes with an internet gateway and a permissive default security group that new engineers will use "just temporarily" and then leave forever.
1.2
Security Group AuditingFinding overly permissive rules at scale · 0.0.0.0/0 on sensitive ports
ObjectiveSystematically find every security group rule that exposes a sensitive port to the whole internet — the equivalent of Lab 1.2's flAWS walkthrough, but at the network layer.AWSCLI
  1. List all security groups

    And their inbound rules.

  2. Filter for 0.0.0.0/0 or ::/0 ingress

    Any rule allowing traffic from the whole internet.

  3. Cross-reference sensitive ports

    22 (SSH), 3389 (RDP), 3306 (MySQL), 5432 (Postgres), 27017 (MongoDB), 6379 (Redis) — none should be reachable from 0.0.0.0/0.

  4. Identify intent vs reality

    Is the group named "private-db" but has port 3306 open to 0.0.0.0/0? That's your finding.

  5. Document for the remediation plan

    Each finding: group ID, port, intended vs actual access, fix recommendation.

# find security groups with inbound 0.0.0.0/0 or ::/0 rules
aws ec2 describe-security-groups \
  --query "SecurityGroups[?IpPermissions[?IpRanges[?CidrIp=='0.0.0.0/0']]].{Name:GroupName,ID:GroupId,Rules:IpPermissions}" \
  --output table

# alternatively, use Prowler's EC2 security group checks from Phase 1
prowler aws --service ec2 --checks ec2_securitygroup_allow_ingress_from_internet_to_any_port
Port Open to 0.0.0.0/0SeverityCorrect Pattern
22 (SSH)HighRestrict to your office IP or use AWS Systems Manager Session Manager — no SSH at all
3389 (RDP)HighSame — restrict by IP or use a bastion/SSM
3306 / 5432 / 27017CriticalAllow only from the application server's security group ID, never from 0.0.0.0/0
80 / 443Expected for public-facing load balancers onlyVerify the instance behind it is intentionally public
⚠️
Removing a security group rule from a running instance takes effect immediately — confirm with the team which instances are affected before making changes in a real account. In the lab context, document the finding rather than fixing it mid-exercise.
1.3
VPC Flow Logs for Network VisibilityEnabling traffic logging · reading what's actually flowing
ObjectiveEnable VPC Flow Logs and read real traffic records — the network-layer equivalent of Phase 1's CloudTrail work.AWS
  1. Check whether Flow Logs are already enabled

    On Meridian's VPCs.

  2. Enable Flow Logs on your lab VPC

    Sending records to an S3 bucket or CloudWatch Logs.

  3. Generate some traffic

    Make a few API calls, curl a public endpoint from an EC2 instance — something that produces records.

  4. Read the flow log records

    Identify source IP, destination IP, port, action (ACCEPT/REJECT), and bytes transferred.

  5. Find a REJECT record

    Something that tried to connect and was blocked — confirm it matches a security group rule.

# check for existing flow logs on your VPC
aws ec2 describe-flow-logs

# enable flow logs to S3 (replace bucket-name with your own)
aws ec2 create-flow-logs \
  --resource-type VPC \
  --resource-ids vpc-XXXXXXXX \
  --traffic-type ALL \
  --log-destination-type s3 \
  --log-destination arn:aws:s3:::your-flow-logs-bucket
Flow Log FieldWhat It Tells You
action: ACCEPTTraffic reached the destination — the security group allowed it
action: REJECTTraffic was blocked by a security group or NACL rule
Unexpected ACCEPT on a sensitive portA security group is more permissive than intended
High-volume REJECT from a single IPPossible port scan or credential stuffing attempt against a blocked service
📋
Flow Logs and CloudTrail answer different questions: CloudTrail tells you who made which AWS API call; Flow Logs tell you what network traffic actually flowed. Together they cover both the control plane and the data plane — you need both for a complete picture.

Module 2 – Container & Kubernetes Security

3 labs · Image hygiene, CIS Benchmark hardening, and RBAC — Meridian's checkout service made defensible
2.1
Container Image SecurityNon-root users · read-only filesystems · image scanning
ObjectiveReview and harden a Dockerfile for Meridian's checkout service — the four Dockerfile mistakes that matter most before a container ever runs.Container
  1. Review the insecure baseline Dockerfile

    Copy the example below into a file — it deliberately contains the four most common container security mistakes.

  2. Identify each issue

    Running as root, no USER instruction, latest tag, no health check, unnecessary packages.

  3. Write the hardened version

    Fix each issue: pin a specific image tag, add a non-root USER, pin dependencies, minimize the attack surface.

  4. Build both locally with Docker

    And confirm the hardened one runs correctly.

  5. Scan with Checkov's Dockerfile support

    Note how many of the issues Checkov catches automatically.

# ── insecure baseline Dockerfile ──────────────────────────
FROM node:latest               # unpinned tag — version drift is a supply-chain risk
RUN apt-get install -y curl    # unnecessary tool increases attack surface
COPY . .
RUN npm install
CMD ["node", "app.js"]         # runs as root — no USER instruction

# ── hardened version ──────────────────────────────────────
FROM node:20.18-alpine3.20    # pinned digest; alpine reduces surface area
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev          # no dev dependencies in production image
COPY --chown=node:node . .
USER node                      # non-root; node user exists in the official image
EXPOSE 3000
CMD ["node", "app.js"]

# scan your Dockerfile with Checkov
checkov -f Dockerfile
IssueRiskFix
FROM node:latestUnpredictable image content across builds; supply-chain riskPin to a specific version + digest
No USER instructionProcess runs as root inside container — privilege escalation pathAdd USER nonroot before CMD
npm install (not npm ci)Installs dev deps + resolves floating versionsUse npm ci --omit=dev
Unnecessary tools (curl)Extra attack surface; used by post-exploit payloads to exfiltrate dataRemove everything not needed at runtime
2.2
Kubernetes CIS Benchmark with kube-benchSpin up a local cluster · audit it · read the failures
ObjectiveCreate a local kind cluster, run kube-bench against it, and understand the CIS findings Meridian's checkout cluster will show on day one.K8skube-bench
  1. Create a kind cluster

    Your local, cost-free Kubernetes environment.

  2. Run kube-bench

    Against the cluster — note that kind hides some control-plane files, so some checks will WARN rather than FAIL.

  3. Read the output sections

    Control Plane, Worker Nodes, Policies — find at least three FAIL items.

  4. Pick one FAIL and apply its remediation

    kube-bench prints the exact fix for each finding. Apply one.

  5. Re-run and confirm the FAIL became PASS

    Then destroy the cluster.

# create a local cluster with kind
kind create cluster --name meridian-checkout

# run kube-bench as a job inside the cluster
kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl wait --for=condition=complete job/kube-bench --timeout=120s
kubectl logs job/kube-bench

# when finished — always destroy immediately
kind delete cluster --name meridian-checkout
kube-bench SectionCommon FAILs on a Default Kind Cluster
1.x Control PlaneAPI server anonymous auth, audit logging not configured
3.x Control Plane Configetcd data directory permissions, peer TLS settings
5.x PoliciesDefault namespace has no NetworkPolicy, Pod Security Admission not configured
📋
kube-bench on a kind cluster will produce some WAININGs for checks it can't evaluate because kind hides control-plane processes inside Docker. That's expected — a real EKS/AKS/GKE cluster run will produce different (often fewer control-plane) findings since the provider manages those components.
2.3
Kubernetes RBAC & Pod SecurityLeast-privilege ServiceAccounts · kube-hunter active scan
ObjectiveAudit RBAC for over-permissive roles and ServiceAccounts, then run kube-hunter to see the cluster the way an attacker would.K8skube-hunter
  1. Create a new kind cluster

    For this lab.

  2. List all ClusterRoleBindings

    Flag any that bind the cluster-admin role to a ServiceAccount — that ServiceAccount can do anything in the cluster.

  3. Create a minimal ServiceAccount

    With only the permissions the checkout service actually needs — read-only access to ConfigMaps in one namespace.

  4. Run kube-hunter in passive mode

    From outside the cluster — it maps the attack surface without attempting exploitation.

  5. Review what kube-hunter found

    Note which findings overlap with kube-bench's policy section.

  6. Destroy the cluster

    Immediately after.

# list ClusterRoleBindings to find over-privileged ServiceAccounts
kubectl get clusterrolebindings -o json | \
  jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects'

# create a minimal ServiceAccount with read-only ConfigMap access
kubectl create serviceaccount checkout-sa -n default
kubectl create role checkout-reader \
  --verb=get,list --resource=configmaps -n default
kubectl create rolebinding checkout-rb \
  --role=checkout-reader --serviceaccount=default:checkout-sa -n default

# run kube-hunter in passive mode (safe — no exploitation attempts)
kube-hunter --remote $(kubectl cluster-info | grep -oP 'https://[^ ]+' | head -1)

# destroy when done
kind delete cluster --name meridian-checkout
RBAC IssueImpactFix
ServiceAccount bound to cluster-adminAny pod using this SA can do anything cluster-wideCreate a minimal Role scoped to exactly what the pod needs
Default ServiceAccount auto-mountedEvery pod in the namespace gets a token even if it doesn't need oneSet automountServiceAccountToken: false on Pods that don't use the API
Wildcard verbs in a Role"* on *" in a namespace is effectively admin for that namespaceList only the specific verbs (get, list, watch) actually needed
📋

Module 3 – Infrastructure-as-Code Scanning

3 labs · Scanning Meridian's Terraform before it deploys, not after it's already live
3.1
Running Checkov Against TerraformFirst scan, reading output, understanding severity levels
ObjectiveWrite a deliberately misconfigured Terraform module representing Meridian's hand-edited infrastructure, scan it with Checkov, and read the output like a triage queue.CheckovTerraform
  1. Create the insecure Terraform example

    Copy the file below — it contains five real misconfigurations Meridian's platform team would plausibly have committed.

  2. Run a Checkov scan

    Against the directory.

  3. Read the output

    Note which checks pass, which fail, the check IDs, and the file/line references.

  4. Fix two findings

    Directly in the Terraform file.

  5. Re-run and confirm fewer failures

    Track the delta — this is exactly what a PR security gate would show.

# main.tf — deliberately misconfigured S3 + security group
resource "aws_s3_bucket" "meridian_data" {
  bucket = "meridian-data-2024"
  # ISSUE 1: no server-side encryption block
  # ISSUE 2: no versioning block
}

resource "aws_s3_bucket_public_access_block" "meridian_data" {
  bucket = aws_s3_bucket.meridian_data.id
  block_public_acls   = false   # ISSUE 3: public access not blocked
  block_public_policy = false
}

resource "aws_security_group" "checkout" {
  name = "checkout-sg"
  ingress {
    from_port   = 22
    to_port     = 22
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]  # ISSUE 4: SSH open to internet
  }
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]  # ISSUE 5: unrestricted egress
  }
}

# scan — no plan file needed, Checkov reads HCL directly
checkov -d .
Checkov Check IDFindingSeverity
CKV_AWS_19S3 bucket not encryptedHigh
CKV_AWS_21S3 versioning not enabledMedium
CKV_AWS_53/54/55/56Block Public Access settings falseHigh
CKV_AWS_25Security group allows SSH from 0.0.0.0/0High
CKV_AWS_277Security group unrestricted egressMedium
💡
Every Checkov finding links to a documentation page explaining why the check exists and exactly what to add to fix it. That link is in the output — use it.
3.2
Scanning Kubernetes ManifestsCheckov covers YAML too — the same tool, a different format
ObjectiveScan Kubernetes YAML manifests for the same class of misconfigurations kube-bench found at the cluster level — but here before they're deployed.CheckovK8s
  1. Write the insecure checkout deployment manifest

    Copy the example below.

  2. Run Checkov against it

    Same command, different file.

  3. Compare findings to Module 2's kube-bench output

    Note the overlap — the same misconfiguration shows up in both the cluster audit and the manifest scan.

  4. Fix the Pod Security Context

    Add runAsNonRoot: true, readOnlyRootFilesystem: true, and drop all capabilities.

  5. Re-scan and confirm the Pod-level findings clear
# checkout-deployment.yaml — deliberately insecure manifest
apiVersion: apps/v1
kind: Deployment
metadata:
  name: checkout
spec:
  replicas: 1
  selector:
    matchLabels:
      app: checkout
  template:
    metadata:
      labels:
        app: checkout
    spec:
      containers:
      - name: checkout
        image: meridian/checkout:latest   # unpinned tag
        # MISSING: securityContext block entirely
        # MISSING: resources.limits (no CPU/memory caps)
        # MISSING: readinessProbe

# scan the manifest
checkov -f checkout-deployment.yaml --framework kubernetes
Checkov Check IDKubernetes Finding
CKV_K8S_14Container not running as non-root
CKV_K8S_20Containers should not run with allowPrivilegeEscalation
CKV_K8S_28No resource limits defined
CKV_K8S_8No liveness probe defined
CKV_K8S_15Image tag is "latest" (mutable, unpinned)
📋
The same finding (container running as root) appears in kube-bench's policy section, in kube-hunter's report, and here in Checkov's manifest scan. That overlap isn't redundant — it means you can catch it at three different points in the lifecycle: before deploy (Checkov), at cluster audit time (kube-bench), and during a penetration test (kube-hunter).
3.3
IaC Capstone — Meridian's Full Terraform AuditScan, triage, remediate, and produce a finding report
ObjectiveCombine Labs 3.1 and 3.2 into one full Terraform + Kubernetes audit, triaged and reported the way a real engagement closes out.
  1. Scan the entire working directory

    All Terraform and Kubernetes files together — Checkov auto-detects framework per file.

  2. Export to JSON

    For structured processing.

  3. Triage to the top 5 critical/high findings

    Using the same severity-first approach as Phase 1's Module 4.3 remediation plan.

  4. Write one-line fixes for each

    Exact HCL or YAML additions, not vague recommendations.

  5. Produce a two-page audit summary

    Findings, business risk, exact fix, owner, target date — the deliverable Meridian's engineering team can act on.

# scan everything at once — Checkov detects Terraform + Kubernetes + Dockerfiles
checkov -d . --output json --output-file-path ./checkov-results

# filter for critical and high findings only
cat ./checkov-results/results_json.json | \
  jq '[.results.failed_checks[] | select(.severity=="HIGH" or .severity=="CRITICAL")]'
🎯
Ready for Module 4 when: you can explain the difference between a Checkov finding on a manifest versus a kube-bench finding on a running cluster, and why you need both even though they sometimes flag the same issue.
🔗

Module 4 – CI/CD & Supply Chain Security

3 labs · The Trivy incident as a lesson plan · hardening GitHub Actions · secrets in pipelines
4.1
Hardening GitHub Actions WorkflowsSHA pinning · least-privilege GITHUB_TOKEN · pull_request_target risks
ObjectiveAudit a GitHub Actions workflow for the exact vulnerabilities that enabled the Trivy supply-chain compromise — and apply each fix to Meridian's pipeline.CI/CD
  1. Review the insecure workflow below

    Identify the three issues the Trivy incident exploited or enabled.

  2. Pin every Action to a commit SHA

    Not a version tag. A SHA cannot be rewritten; a tag can. This is the single most impactful change.

  3. Restrict GITHUB_TOKEN permissions

    Declare only the permissions the workflow actually needs.

  4. Audit any pull_request_target usage

    If it checks out PR code and has token permissions, it's a critical vulnerability waiting to be exploited.

  5. Run Checkov against the workflow file

    Checkov covers GitHub Actions YAML — same tool, different framework.

# ── INSECURE workflow ─────────────────────────────────────
on: [push, pull_request_target]     # RISK: pull_request_target + checkout = credential theft
jobs:
  scan:
    runs-on: ubuntu-latest
    # RISK: no permissions block = default wide-open GITHUB_TOKEN
    steps:
      - uses: actions/checkout@v4   # RISK: mutable version tag
      - uses: aquasecurity/trivy-action@v0.24.0  # RISK: tag was rewritten in March 2026

# ── HARDENED workflow ─────────────────────────────────────
on: [push]                          # pull_request_target removed
permissions:
  contents: read                    # explicit least-privilege
jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683  # SHA pinned
      - name: Run Checkov IaC scan
        run: pip install checkov && checkov -d .  # Checkov via pip — no GitHub Action needed

# scan your own workflow file with Checkov
checkov -f .github/workflows/scan.yml --framework github_actions
VulnerabilityHow It Was Exploited in March 2026Fix
Mutable version tagsTags rewritten to point to malicious commits — pipelines pulled attacker code automaticallyPin all Actions to full 40-char commit SHA
pull_request_target + checkoutLets PR code run with the base repo's secrets — the initial credential theft vectorSeparate trusted workflow from untrusted PR code; avoid the combination
Over-permissive GITHUB_TOKENDefault token had write access that amplified the blast radiusDeclare permissions: contents: read at minimum
4.2
Secrets in Pipelines — Detection and PreventionWhat shouldn't be in environment variables · short-lived credentials · OIDC
ObjectiveUnderstand why static, long-lived credentials in pipelines are the primary target of supply-chain attacks — and how short-lived OIDC tokens close that attack surface.CI/CDAWS
PatternRisk
AWS_ACCESS_KEY_ID in GitHub Secrets, rotated annuallyStatic key — if the pipeline is compromised, the attacker has a valid key that lasts until next rotation
AWS OIDC — no stored key at allGitHub requests a short-lived token per job; attacker capturing it gets something valid for minutes, not months
  1. Scan your repo for leaked secrets with gitleaks

    From Module 7 of the Bug Bounty workbook — secrets accidentally committed to Terraform or workflow files are extremely common.

  2. Configure AWS OIDC in your lab account

    Create an IAM OIDC provider for GitHub Actions and an IAM Role it can assume — no stored key required.

  3. Update the hardened workflow to use OIDC

    Replace aws-actions/configure-aws-credentials using stored keys with the OIDC token approach.

  4. Verify the pipeline can access AWS

    Without any long-lived key stored in GitHub Secrets.

# scan for accidentally committed secrets
gitleaks detect --source . --log-opts="--all"

# hardened workflow using OIDC (no stored AWS keys)
permissions:
  id-token: write   # required for OIDC
  contents: read

steps:
  - name: Configure AWS Credentials via OIDC
    uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502  # SHA pinned
    with:
      role-to-assume: arn:aws:iam::ACCOUNT_ID:role/GitHubActionsRole
      aws-region: us-east-1
      # no access-key-id or secret-access-key — OIDC exchanges the GitHub token for temporary AWS creds
🎯
Short-lived OIDC credentials are what made the Trivy attack's credential-theft payload less damaging for organizations that had already adopted this pattern — stolen credentials expired within minutes. This is the structural fix, not just rotation.
4.3
Phase 2 Capstone — Meridian's Audit-Ready PipelineThe full loop, closed
ObjectiveClose the Phase 2 engagement by connecting every module into a single hardened pipeline and producing Meridian's final audit-ready summary.
GateToolCatches
Pre-commit (developer machine)Checkov + gitleaksIaC misconfigs and secrets before the commit lands
PR gate (GitHub Actions)Checkov, SHA-pinned, OIDC credentialsSame checks on every PR — blocks merge on High/Critical
Post-deploy (runtime)kube-bench CronJob · VPC Flow Logs · CloudTrailDrift from the hardened baseline after deployment
Periodic full-account auditProwler (from Phase 1)Everything Phase 1 covers — IAM, storage, logging — on a schedule
  1. Write the full hardened workflow

    Combining Labs 4.1 (SHA pinning, OIDC, least-privilege) and 4.2 (OIDC), with a Checkov scan gate on every PR.

  2. Write the closing audit summary

    Both phases together: what Meridian's posture was on day one, what changed in Phase 1, and what Phase 2 adds.

  3. Identify the one remaining gap

    What does the pipeline above still not catch? (Runtime threats inside a running container — the answer is a runtime security tool like Falco, which is out of scope here but the honest next step.)

🎯
Phase 2 engagement complete when: you can walk Meridian's CTO through the full pipeline above in five minutes — what each gate catches, why each tool was chosen over the alternatives, and what the one remaining gap is. That conversation is the job.

🎉 Engagement Complete — Operation: OPEN BUCKET, Phase 2

Meridian's expanded audit scope is closed out. You've hardened the network layer, secured the Kubernetes checkout service, put IaC scanning in front of every Terraform change, and locked down the pipeline that ships it all — the complete loop a mid-level cloud security engineer owns end to end.