Module 4.4: Supply Chain Security
Discipline Module | Complexity:
[COMPLEX]| Time: 70-85 min
Prerequisites
Section titled “Prerequisites”Before starting this module, you should be comfortable reading CI/CD workflows, container image references, dependency lockfiles, and Kubernetes admission policy examples. This module builds directly on Module 4.3: Security in CI/CD, where you learned how pipeline permissions, secret handling, and branch protections shape the trust boundary around a delivery system.
You do not need to be a cryptographer to succeed here, but you should understand the basic idea of signing and verification. A signature proves that a trusted identity approved a specific artifact digest. It does not prove that the artifact is vulnerability-free, well-designed, or safe to run by itself. Supply chain security is the practice of combining identity, provenance, dependency evidence, and runtime enforcement so that no single control has to carry the whole risk.
You should also have basic familiarity with container registries and Kubernetes workloads. The examples use container images, SBOMs, Sigstore Cosign, SLSA provenance, GitHub Actions, and Kyverno-style admission policies. The same mental model applies to language packages, binaries, Helm charts, Terraform modules, and internal platform templates.
Learning Outcomes
Section titled “Learning Outcomes”After completing this module, you will be able to:
- Map a software supply chain from source commit to Kubernetes deployment and identify where tampering, confusion, or secret exposure can occur.
- Generate and evaluate SBOM evidence so an incident team can answer whether a vulnerable component exists in a released artifact.
- Design and verify an artifact signing flow that binds an image digest to a workload identity, not merely to a mutable tag.
- Compare and apply SLSA, lockfiles, dependency controls, admission policies, and Prevent · Contain · Detect defenses for realistic supply chain attack paths.
- Debug a failed deployment caused by missing signatures, stale provenance, or unsafe dependency resolution without weakening the control.
Why This Module Matters
Section titled “Why This Module Matters”A platform team ships a payment service after weeks of security work. The application code has passed review, the container scan is clean, and the deployment pipeline uses protected branches. On release morning, the on-call engineer notices outbound traffic from a new pod in kube-system, even though no platform component was scheduled for maintenance. The source repository looks untouched, so the first instinct is to search the application code for a backdoor.
The backdoor is not in the code the team reviewed. It entered through the build path. A CI action was referenced by a mutable tag, a publish token was available to a step that did not need it, and a package uploaded under a familiar name was accepted because the install command trusted registry defaults. The team had protected the application while leaving the path that created the application weak enough to impersonate.
Supply chain security matters because modern software is assembled, not handwritten. A production image contains operating system packages, language dependencies, generated files, base layers, CI helpers, build actions, registry metadata, and cluster policy decisions. A senior engineer does not ask only, “Is the code secure?” They ask, “Can we prove what ran, who built it, what it contains, and why the cluster accepted it?”
This module teaches that proof chain step by step. You will start by modeling the attack surface, then generate an SBOM to make components visible, then sign and verify artifacts, then connect those controls into SLSA provenance and Kubernetes admission. The goal is not to memorize tool commands. The goal is to reason about evidence and enforcement when an artifact’s origin is in doubt.
1. Model the Supply Chain Before Choosing Tools
Section titled “1. Model the Supply Chain Before Choosing Tools”Supply chain security starts with a map, not a scanner. If a team begins by installing every popular security tool, they often create noisy dashboards while leaving the dangerous trust decisions unchanged. The useful first question is, “What has to be true for this artifact to be safe enough to deploy?” That question forces the team to identify source integrity, dependency resolution, build isolation, artifact immutability, registry trust, and cluster admission as separate links.
┌──────────────────────────────────────────────────────────────────────────────┐│ SOFTWARE SUPPLY CHAIN PATH ││ ││ ┌────────────┐ ┌──────────────┐ ┌─────────────┐ ┌────────────────┐ ││ │ Source │ │ Dependencies │ │ Build │ │ Artifact │ ││ │ repository │──▶│ and base │──▶│ environment │──▶│ registry │ ││ │ commits │ │ images │ │ and CI jobs │ │ and metadata │ ││ └─────┬──────┘ └──────┬───────┘ └──────┬──────┘ └───────┬────────┘ ││ │ │ │ │ ││ ▼ ▼ ▼ ▼ ││ insider commit typosquat package secret exposure tag replacement ││ branch bypass dependency drift build injection registry tamper ││ ││ ┌────────────────┐ ┌─────────────────┐ ┌───────────────────────────┐ ││ │ Deployment │ │ Admission │ │ Runtime and incident │ ││ │ manifests │──▶│ controller │──▶│ response evidence │ ││ │ and GitOps │ │ verification │ │ │ ││ └────────────────┘ └─────────────────┘ └───────────────────────────┘ ││ │└──────────────────────────────────────────────────────────────────────────────┘The diagram shows why “scan the image” is only one part of the answer. A vulnerability scanner can report known CVEs in installed packages, but it cannot prove that the image was built from the approved commit. A signature can prove that a trusted identity signed a digest, but it cannot prove that the dependency tree was free of risky transitive packages. An admission controller can reject unsigned images, but it cannot help if the signing workflow signs whatever the attacker builds.
A useful supply chain design names the evidence each stage must produce. Source control produces commits, reviews, and branch protection events. Dependency managers produce lockfiles and resolved package URLs. Build systems produce logs, artifact digests, SBOMs, and provenance attestations. Registries preserve immutable digests and metadata. Kubernetes admission records why a workload was accepted or rejected. Incident response becomes dramatically faster when these records are already connected.
| Stage | Main Question | Evidence to Keep | Common Attack |
|---|---|---|---|
| Source | Was this change reviewed and approved? | Commit SHA, reviewer record, branch protection result | Insider commit or bypassed review |
| Dependencies | Were packages resolved from expected sources? | Lockfile, registry URL, package hash, SBOM component | Dependency confusion or typosquatting |
| Build | Was the artifact created by the trusted workflow? | Build logs, runner identity, provenance, digest | Build script injection or secret theft |
| Registry | Is this exact artifact immutable and traceable? | Image digest, signature, attestation, push event | Tag replacement or registry compromise |
| Admission | Did the cluster verify the artifact before running it? | Admission decision, policy version, verified identity | Unsigned or unapproved image deployment |
| Runtime | Can responders connect a running pod back to evidence? | Pod image digest, workload owner, SBOM, provenance | Long dwell time after compromise |
Pause and predict: If an attacker can replace
ghcr.io/acme/payments:v1.2.0in the registry but cannot change the digestsha256:..., which deployments are still vulnerable? Write down whether a manifest using the tag, the digest, or both the tag and digest would run the attacker’s image before you continue.
The safest Kubernetes manifests deploy immutable digests, because tags are names that can move. A tag is convenient for humans, but the digest is the content identity. A manifest such as image: ghcr.io/acme/payments@sha256:... says exactly which bytes should run. A manifest such as image: ghcr.io/acme/payments:v1.2.0 asks the registry what that tag means today, which creates a trust decision at pull time.
This does not mean tags are useless. Teams often publish tags for discoverability and release communication, then resolve those tags to digests during promotion. The promotion system can record, “release v1.2.0 means digest sha256:abc...,” and GitOps can deploy the digest. That pattern gives developers readable releases while giving the cluster immutable content.
IMAGE="ghcr.io/acme/payments:v1.2.0"
docker buildx imagetools inspect "$IMAGE"
# A real promotion script would capture the digest from the registry response,# store it in the release record, and update deployment manifests to use it.The next design choice is where to enforce trust. CI can fail builds that have unapproved dependencies. The registry can require signatures before promotion. Admission can reject workloads whose image identity or provenance does not match policy. Runtime detection can alert on unexpected privileged pods or new system namespace workloads. Mature platforms use all of these because attackers move laterally through whichever stage is least protected.
The table below is a compact decision matrix for choosing the first control when a team has limited time. It does not replace defense in depth, but it helps avoid random tool adoption. Start where the evidence gap causes the worst incident response failure.
| Symptom | First Control to Add | Why It Helps | What It Does Not Solve |
|---|---|---|---|
| Nobody knows whether a CVE affects production | SBOM generation and storage | Makes released components searchable | Does not prove who built the image |
| Images can be replaced under the same tag | Digest deployment and signing | Binds deployment to immutable content | Does not block risky dependencies alone |
| CI secrets are available to every step | Job-scoped permissions and secret scoping | Limits blast radius of compromised tools | Does not identify transitive packages |
| Developers use unpinned CI actions | Commit SHA pinning and dependency review | Reduces mutable third-party execution | Does not verify final runtime admission |
| Clusters run whatever manifests request | Admission verification | Moves trust enforcement to deploy time | Does not generate missing build evidence |
| Releases cannot be traced to source | SLSA provenance | Connects artifact, builder, and source | Does not replace vulnerability management |
The beginner version of supply chain security is “scan dependencies.” The senior version is “preserve and verify a chain of evidence from source to runtime.” Tools matter, but only because they produce or enforce evidence at a specific trust boundary.
2. Build SBOMs That Answer Incident Questions
Section titled “2. Build SBOMs That Answer Incident Questions”A Software Bill of Materials, or SBOM, is an inventory of the components inside an artifact. It lists packages, versions, package URLs, licenses, hashes, and relationships depending on the format and generator. The operational value is simple: when a new vulnerability is announced, responders can query released artifacts instead of asking every team to inspect every repository by hand.
┌──────────────────────────────────────────────────────────────────────────────┐│ SBOM CONTENT ││ ││ Artifact: ghcr.io/acme/payments@sha256:8a2... ││ Build: release workflow run 9021 ││ Format: CycloneDX JSON ││ ││ ┌─────────────────────────────┐ ┌────────────────────────────────────┐ ││ │ Direct application packages │ │ Operating system packages │ ││ │ flask 2.3.3 │ │ openssl 3.x │ ││ │ requests 2.31.0 │ │ ca-certificates │ ││ └──────────────┬──────────────┘ └──────────────────┬─────────────────┘ ││ │ │ ││ ▼ ▼ ││ ┌─────────────────────────────┐ ┌────────────────────────────────────┐ ││ │ Transitive dependencies │ │ Base image layer components │ ││ │ urllib3 │ │ debian or alpine packages │ ││ │ idna │ │ shell, libc, package manager data │ ││ └─────────────────────────────┘ └────────────────────────────────────┘ ││ │└──────────────────────────────────────────────────────────────────────────────┘An SBOM is not a security verdict. It is evidence. A clean SBOM can still describe a malicious package that has no known CVE. A noisy SBOM can include a vulnerable package that is not reachable in the deployed application. Treat the SBOM as the starting point for investigation, then combine it with exploitability analysis, runtime exposure, and business impact.
The two common formats you will see are SPDX and CycloneDX. SPDX has strong roots in license compliance and is widely used for legal and open source governance. CycloneDX is common in application security workflows because it models components, services, vulnerabilities, and dependency relationships in a way many security tools consume naturally. Either format is better than having no searchable inventory, and many organizations store both when tooling allows it.
| Format | Strong Fit | Typical Consumers | Trade-Off |
|---|---|---|---|
| SPDX | License compliance and legal review | Legal teams, open source program offices, artifact stores | Security relationships may require extra tooling |
| CycloneDX | Vulnerability management and dependency analysis | AppSec teams, scanners, policy engines, dashboards | License workflows may need additional fields |
| SWID | Enterprise asset inventory | Asset management and procurement systems | Less common in cloud-native build pipelines |
| in-toto statement | Attestation envelope for evidence | Provenance and policy verification systems | Carries predicates rather than being only an inventory |
A practical SBOM workflow has four steps. Generate the SBOM during the build, store it next to the artifact, attach or attest it so the digest and inventory stay linked, and index it for incident queries. If the SBOM lives only in a CI log or an engineer’s laptop, it will not help during an incident. If it is generated after deployment from a mutable tag, it may describe a different artifact than the one running in production.
mkdir -p supply-chain-democd supply-chain-demo
cat > requirements.txt <<'EOF'flask==2.3.3requests==2.31.0EOF
cat > app.py <<'EOF'from flask import Flask
app = Flask(__name__)
@app.get("/")def hello(): return {"status": "ok"}
if __name__ == "__main__": app.run(host="127.0.0.1", port=8080)EOF
cat > Dockerfile <<'EOF'FROM python:3.12-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app.py .CMD ["python", "app.py"]EOF
docker build -t supply-chain-demo:v1 .After the image exists locally, generate an SBOM from the actual image rather than only from the source directory. Source scans are useful, but they miss operating system packages and base layer contents. Image scans see what will actually ship, including dependencies introduced by the base image or package manager.
syft supply-chain-demo:v1 -o cyclonedx-json > sbom.cdx.json
jq '.components[] | {name: .name, version: .version, type: .type}' sbom.cdx.json | headIf your team uses Trivy instead of Syft, the same pattern applies. The important design point is not the brand of generator. The important point is that the SBOM is created from a specific artifact digest in the same release path that creates the deployable image.
trivy image --format cyclonedx --output sbom.cdx.json supply-chain-demo:v1
trivy sbom sbom.cdx.jsonStop and think: Your scanner reports a critical CVE in a package from the base image, but developers say the application never imports that package. What evidence would you need before deciding whether to block release, patch immediately, or accept temporary risk?
The answer depends on exposure and policy. A package in a base image can be reachable through a system utility, an interpreter, a shell, or a library loaded by another package. The SBOM tells you the package exists. A vulnerability scanner tells you a known issue may apply. Runtime configuration, reachable code paths, exploit prerequisites, and compensating controls determine urgency. Senior teams document that decision instead of treating every scanner row as equal.
A strong SBOM program also records relationships. During a Log4Shell-style event, the key question is often not “Does any repository import this package directly?” but “Which released artifacts include it transitively?” Relationship data lets you trace from application framework to logging library to vulnerable component. Without that chain, teams waste time searching source code and miss packaged dependencies.
grype sbom:./sbom.cdx.json
# For a targeted incident query, filter scanner output for the vulnerability ID.# Replace the example ID with the incident identifier your security team is tracking.grype sbom:./sbom.cdx.json --only-fixed| SBOM Failure Mode | What Happens During an Incident | Better Practice |
|---|---|---|
| SBOM generated only from source | Base image and OS packages are missing | Generate from the final image digest |
| SBOM stored only as a CI artifact | Evidence expires or is hard to find | Store with release metadata and artifact registry |
| SBOM not tied to digest | Inventory may describe the wrong image | Attach or attest SBOM against the digest |
| SBOM generated after release | The artifact may have changed | Generate during the build path |
| SBOM ignored after creation | Vulnerability response stays manual | Index SBOMs for search and alerting |
| SBOM treated as a pass/fail gate only | Teams lose context and over-block | Combine inventory with risk analysis |
SBOMs become more valuable when they are boring. Every build produces one, every release stores one, every incident query uses the same location, and every exception records why the component was accepted. The team should not be inventing the SBOM process during a zero-day response.
3. Sign Artifacts and Verify Identity
Section titled “3. Sign Artifacts and Verify Identity”Image signing answers a different question than vulnerability scanning. Scanning asks, “What known weaknesses are inside this artifact?” Signing asks, “Did a trusted identity approve this exact artifact digest?” You need both questions because an attacker can create a backdoored image with no known CVEs, and a legitimate image can contain vulnerable packages.
┌──────────────────────────────────────────────────────────────────────────────┐│ SIGNING TRUST MODEL ││ ││ Build workflow ││ identity: repo acme/payments, release workflow ││ │ ││ │ builds image ││ ▼ ││ ghcr.io/acme/payments@sha256:8a2... ││ │ ││ │ signs digest, not mutable tag ││ ▼ ││ Signature + certificate + transparency log entry ││ │ ││ │ verified by admission policy ││ ▼ ││ Kubernetes accepts only images signed by the expected workflow identity ││ │└──────────────────────────────────────────────────────────────────────────────┘Sigstore is popular in cloud-native environments because it supports keyless signing. Traditional signing often requires teams to create, store, rotate, and protect long-lived private keys. Keyless signing uses workload identity through OIDC, short-lived certificates, and a transparency log. The result is still a cryptographic signature, but the human or workflow identity becomes part of the verification story.
Cosign is the command-line tool most teams use with Sigstore. Fulcio issues short-lived signing certificates based on OIDC identity. Rekor records transparency log entries so signatures can be audited. Policy controllers and admission systems can verify that an image digest was signed by a specific identity, such as a GitHub Actions workflow in a specific repository.
| Component | Role in the Signing Flow | Operational Question It Answers |
|---|---|---|
| Cosign | Signs and verifies artifacts | Can this digest be linked to a trusted signer? |
| Fulcio | Issues short-lived certificates | Which OIDC identity performed the signing? |
| Rekor | Records transparency log entries | Is there an auditable record of the signature event? |
| OIDC issuer | Provides workload or user identity | Was the signer a trusted workflow or account? |
| Admission policy | Enforces verification before runtime | Should this cluster accept the workload? |
The most important habit is signing the digest. A tag can point to different content over time, so signing only a tag-shaped reference can hide ambiguity in conversations and runbooks. When a build pushes an image, capture the pushed digest and sign that digest. The digest is the immutable subject of the trust decision.
IMAGE="ghcr.io/acme/payments"TAG="v1.2.0"
docker build -t "$IMAGE:$TAG" .docker push "$IMAGE:$TAG"
DIGEST="$(docker buildx imagetools inspect "$IMAGE:$TAG" --format '{{json .Manifest.Digest}}' | tr -d '"')"echo "$IMAGE@$DIGEST"In GitHub Actions, keyless signing requires id-token: write because the workflow must request an OIDC token. This permission should be granted only to the job that signs or generates attestations. The build job should have only the permissions it needs, and the publish job should not expose package registry tokens to unrelated scanners or test steps.
name: build-sign-release
on: push: tags: - 'v*'
jobs: build: runs-on: ubuntu-24.04 permissions: contents: read packages: write outputs: image: ${{ steps.meta.outputs.image }} digest: ${{ steps.build.outputs.digest }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Set image name id: meta run: echo "image=ghcr.io/${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT"
- name: Log in to registry run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin
- name: Build and push id: build run: | docker build -t "${{ steps.meta.outputs.image }}:${GITHUB_REF_NAME}" . docker push "${{ steps.meta.outputs.image }}:${GITHUB_REF_NAME}" DIGEST="$(docker buildx imagetools inspect "${{ steps.meta.outputs.image }}:${GITHUB_REF_NAME}" --format '{{json .Manifest.Digest}}' | tr -d '"')" echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
sign: needs: build runs-on: ubuntu-24.04 permissions: contents: read id-token: write packages: write steps: - uses: sigstore/cosign-installer@398d4b0eeef1380460a10c8013a76f728fb906ac # v3.9.1
- name: Sign image digest run: cosign sign --yes "${{ needs.build.outputs.image }}@${{ needs.build.outputs.digest }}"The workflow pins actions to commit SHAs rather than broad tags. That choice matters because CI actions are executable dependencies. A mutable action tag can change what runs inside your trusted pipeline. Pinning does not make third-party code harmless, but it prevents silent drift and makes updates reviewable.
Decision point: A developer proposes signing images from their laptop after local testing because it is faster than waiting for CI. Would you allow that for production images? Decide what identity, environment, and review evidence would be lost or preserved.
For production images, signing from a laptop usually weakens the trust story. The signer identity proves that a person signed something, but it does not prove that the artifact came from the reviewed source, a clean build environment, or the approved release workflow. A better pattern is to let developers sign development artifacts for testing, while production policy accepts only signatures from the release workflow identity.
Verification should be as specific as practical. “Signed by someone” is weak. “Signed by the release workflow in acme/payments using the GitHub OIDC issuer” is much stronger. Good verification names the expected issuer and subject, then binds that identity to the image digest being deployed.
cosign verify "ghcr.io/acme/payments@sha256:REPLACE_WITH_DIGEST" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ --certificate-identity-regexp "https://github.com/acme/payments/.github/workflows/build-sign-release.yml@refs/tags/v.*"Kubernetes admission control moves verification from a human checklist into the deployment path. A Kyverno policy can require signatures for images from a registry namespace, and the policy can specify the expected keyless identity. The cluster then rejects workloads that do not match, even if someone bypasses a manual release checklist.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-signed-payments-imagesspec: validationFailureAction: Enforce background: false rules: - name: verify-payments-image-signature match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "ghcr.io/acme/payments*" attestors: - entries: - keyless: issuer: "https://token.actions.githubusercontent.com" subject: "https://github.com/acme/payments/.github/workflows/build-sign-release.yml@refs/tags/v*"Admission policies should start in audit mode for existing clusters, then move to enforcement after teams understand what will break. This is not because enforcement is optional. It is because a poorly scoped policy can cause an outage by rejecting system workloads, vendor controllers, or emergency rollback images. Roll out verification by namespace, registry prefix, or application group, and document how a break-glass exception is approved and expired.
4. Worked Example: Trace, Sign, Attest, and Enforce One Image
Section titled “4. Worked Example: Trace, Sign, Attest, and Enforce One Image”This worked example connects the pieces into one coherent flow. The scenario is a platform team releasing ghcr.io/acme/orders:v2.0.0 to a Kubernetes 1.35 cluster. The team wants evidence that the image came from the approved repository, contains a searchable SBOM, has provenance from the release workflow, and cannot run unless admission verifies the release identity.
┌──────────────────────────────────────────────────────────────────────────────┐│ WORKED EXAMPLE CONTROL FLOW ││ ││ 1. Build image from reviewed source ││ │ ││ ▼ ││ 2. Push image and capture immutable digest ││ │ ││ ▼ ││ 3. Generate SBOM from that exact digest ││ │ ││ ▼ ││ 4. Sign digest and attach SBOM/provenance attestations ││ │ ││ ▼ ││ 5. Deploy by digest, then admission verifies signer identity ││ │ ││ ▼ ││ 6. Incident team can query digest, SBOM, signer, and source commit ││ │└──────────────────────────────────────────────────────────────────────────────┘Step one is to define the artifact identity. The team refuses to promote a tag alone. The release record contains the image repository, tag, digest, source commit, workflow run, SBOM location, and signature verification command. This release record is boring metadata until an incident occurs, then it becomes the difference between a five-minute query and a multi-day search.
| Release Field | Example Value | Why It Matters |
|---|---|---|
| Source repository | github.com/acme/orders | Defines the expected source of truth |
| Source commit | 9b6c... | Ties the artifact to reviewed code |
| Workflow identity | build-sign-release.yml | Defines the trusted builder |
| Image digest | sha256:8a2... | Identifies immutable content |
| SBOM artifact | orders-v2.0.0.cdx.json | Supports incident component queries |
| Signature identity | GitHub OIDC subject | Supports admission verification |
| Provenance predicate | SLSA provenance | Links builder, source, and artifact |
Step two is to build and push the image. The exact commands vary by build system, but the behavior should not. The build produces an image, the push returns or allows lookup of a digest, and every later control uses that digest as the subject. If a later step receives only orders:v2.0.0, the workflow has already lost precision.
IMAGE="ghcr.io/acme/orders"TAG="v2.0.0"
docker build -t "$IMAGE:$TAG" .docker push "$IMAGE:$TAG"
DIGEST="$(docker buildx imagetools inspect "$IMAGE:$TAG" --format '{{json .Manifest.Digest}}' | tr -d '"')"IMAGE_REF="$IMAGE@$DIGEST"
printf '%s\n' "$IMAGE_REF"Step three is to generate the SBOM from the immutable image reference. The team stores the SBOM as a build artifact and attaches it as an attestation. Storing the file supports search and dashboards. Attaching the predicate supports verification that the SBOM belongs to the digest being deployed.
syft "$IMAGE_REF" -o cyclonedx-json > orders-v2.0.0.cdx.json
cosign attest --yes \ --predicate orders-v2.0.0.cdx.json \ --type cyclonedx \ "$IMAGE_REF"Step four is to sign the digest with the release workflow identity. If the image is rebuilt, the digest changes and the old signature does not automatically apply. That is the desired behavior. A signature says, “this exact content was approved by this identity,” not “anything with this tag is acceptable.”
cosign sign --yes "$IMAGE_REF"
cosign verify "$IMAGE_REF" \ --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ --certificate-identity-regexp "https://github.com/acme/orders/.github/workflows/build-sign-release.yml@refs/tags/v.*"Step five is to deploy by digest and let admission enforce the same identity requirement. Notice that the manifest does not ask Kubernetes to resolve a moving tag. The digest is explicit, which means the admission controller, kubelet, registry, and incident record all talk about the same artifact.
apiVersion: apps/v1kind: Deploymentmetadata: name: orders namespace: paymentsspec: replicas: 3 selector: matchLabels: app: orders template: metadata: labels: app: orders spec: containers: - name: orders image: ghcr.io/acme/orders@sha256:REPLACE_WITH_REAL_DIGEST ports: - containerPort: 8080Step six is to test the negative case. A control that has never rejected anything is not yet proven. The team attempts to deploy an unsigned image in a staging namespace where the policy is enforced. The expected result is a rejected admission request with a message that names signature verification or attestor mismatch.
kubectl apply -f deployment.yaml
kubectl get events -n payments --sort-by='.lastTimestamp' | tail -n 20If the deployment fails even though the image was signed, debug the evidence chain in order. First confirm the manifest uses the digest that was signed. Then verify the certificate issuer and subject match the policy. Then confirm the policy image pattern matches the image reference. Finally, check whether the attestation type or signature is stored in a registry location the verifier can access.
| Failure Symptom | Likely Cause | First Debug Command |
|---|---|---|
| Verification says no signatures found | Signed tag or wrong digest | cosign verify "$IMAGE_REF" ... |
| Admission says subject mismatch | Policy expects different workflow identity | Inspect certificate identity from cosign verify output |
| Admission does not run | Policy match selector misses the workload | kubectl get clusterpolicy and review match block |
| SBOM query finds nothing | SBOM stored as file but not indexed | Check artifact store and attestation upload |
| Rollback image is rejected | Old release lacks signature | Sign historical digest or define approved exception |
| Vendor image is rejected | Policy scope too broad | Limit policy to owned registry prefixes first |
This worked example is intentionally narrow. It secures one image path rather than pretending to solve all supply chain risk at once. Once the team can do this reliably for one service, they can template it into platform pipelines, GitOps promotion, and admission policy libraries.
5. Raise Maturity with SLSA and Dependency Controls
Section titled “5. Raise Maturity with SLSA and Dependency Controls”SLSA, pronounced “salsa,” is a framework for improving software supply chain integrity. It is useful because it turns vague maturity goals into concrete build and provenance requirements. A team can say, “We want SLSA Build Level 2 for internal services this quarter,” and that statement implies hosted builds, authenticated provenance, and repeatable evidence instead of a generic promise to be more secure.
┌──────────────────────────────────────────────────────────────────────────────┐│ SLSA BUILD LEVELS ││ ││ Level 0 ││ No meaningful supply chain guarantees. Builds may be manual, local, or ││ undocumented, and released artifacts cannot be reliably traced. ││ ││ Level 1 ││ Provenance exists. The build process is scripted, and the artifact has ││ basic information about how it was produced. ││ ││ Level 2 ││ Hosted build service and authenticated provenance. The build runs on a ││ trusted platform, and provenance is signed or otherwise authenticated. ││ ││ Level 3 ││ Hardened build platform. Builds are isolated, stronger tamper resistance ││ exists, and provenance is difficult for project maintainers to falsify. ││ │└──────────────────────────────────────────────────────────────────────────────┘SLSA is not a badge you earn once and forget. It is a way to reason about how much confidence you can place in the relationship between source, builder, and artifact. A manual build from a developer laptop might be acceptable for a prototype, but it is weak evidence for production. A hosted build with authenticated provenance is better. A hardened build platform with strong isolation and non-falsifiable provenance is stronger still.
| Capability | Level 1 | Level 2 | Level 3 |
|---|---|---|---|
| Scripted build process | Yes | Yes | Yes |
| Provenance generated | Yes | Yes | Yes |
| Hosted build service | No | Yes | Yes |
| Authenticated provenance | No | Yes | Yes |
| Strong build isolation | No | Partial | Yes |
| Tamper-resistant provenance | Basic | Better | Strong |
| Suitable default for production platforms | Limited | Common target | High assurance target |
GitHub Actions can produce provenance through artifact attestations or SLSA generator workflows. The exact implementation changes over time, but the principle stays stable: provenance must describe the subject artifact, the build type, the source material, and the builder identity. A policy engine can then verify whether the artifact came from the expected source and build path.
name: provenance
on: push: tags: - 'v*'
jobs: build: runs-on: ubuntu-24.04 permissions: contents: read packages: write id-token: write attestations: write steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Build artifact run: | mkdir -p dist printf 'release artifact for %s\n' "$GITHUB_SHA" > dist/app.txt
- name: Generate artifact attestation uses: actions/attest-build-provenance@1c608d11d69870c2092266b3f9a6f3abbf17002c # v1.4.3 with: subject-path: dist/app.txtProvenance protects against a different class of confusion than SBOMs. An SBOM can tell you an artifact contains openssl. Provenance can tell you the artifact was built by github.com/acme/orders at a specific commit using a specific workflow. If an attacker uploads a lookalike artifact to the registry, provenance verification should fail because the trusted builder relationship is missing.
Dependency controls are the other side of the maturity model. Many supply chain incidents begin before the build produces anything: package names are confused, transitive dependencies drift, action tags move, or a maintainer account is compromised. Lockfiles, registry scoping, hash verification, and update automation reduce the chance that a build silently consumes something unexpected.
┌──────────────────────────────────────────────────────────────────────────────┐│ DEPENDENCY CONFUSION PATTERN ││ ││ Internal package expected: @acme/internal-metrics 1.3.0 ││ Private registry expected: https://npm.acme.example ││ ││ Risky configuration: package name without enforced scope ││ │ ││ ▼ ││ Public registry contains: internal-metrics 99.0.0 ││ │ ││ ▼ ││ Installer chooses public package because version or registry precedence wins ││ ││ Safer configuration: scoped package + private registry mapping ││ │└──────────────────────────────────────────────────────────────────────────────┘For npm, scoped packages and registry mapping are essential. The scope tells the package manager that @acme/* packages belong to the organization. The .npmrc mapping tells the installer where those packages may be resolved. The lockfile records the resolved URL and integrity hash so CI can install the same dependency graph that was reviewed.
{ "dependencies": { "@acme/internal-metrics": "1.3.0", "express": "4.18.3" }}@acme:registry=https://npm.acme.exampleregistry=https://registry.npmjs.orgalways-auth=truenpm cinpm audit signaturesFor Python, hash-checked requirements reduce silent package replacement. This approach is stricter than a plain requirements.txt, because each package version must match an expected hash. It takes more maintenance, but it gives high-value services a stronger guarantee that the package downloaded in CI is the package that was reviewed.
flask==2.3.3 \ --hash=sha256:REPLACE_WITH_HASH_FROM_LOCK_TOOLrequests==2.31.0 \ --hash=sha256:REPLACE_WITH_HASH_FROM_LOCK_TOOLpip install --require-hashes -r requirements.txtUse an appropriate lock tool to generate real hashes rather than typing placeholders by hand. For Python teams, pip-tools, Poetry, or uv can produce reproducible lock data depending on the standard your organization has adopted. The control is not “make requirements hard to edit.” The control is “make dependency resolution explicit enough that a changed package source or hash creates a reviewable diff.”
| Ecosystem | Resolution Control | CI Command | Risk Reduced |
|---|---|---|---|
| npm | package-lock.json and scoped .npmrc | npm ci | Registry drift and dependency confusion |
| Yarn | yarn.lock with immutable install | yarn install --immutable | Silent dependency updates |
| pnpm | pnpm-lock.yaml | pnpm install --frozen-lockfile | Transitive dependency drift |
| pip | Hash-checked requirements or lock file | pip install --require-hashes -r requirements.txt | Package replacement and unreviewed versions |
| Poetry | poetry.lock | poetry install --sync | Environment drift |
| Go | go.sum and module verification | go mod verify | Module checksum mismatch |
| Containers | Digest-pinned base images | docker build with pinned FROM digest | Mutable base image changes |
Automation helps when it creates small, reviewable changes. Dependabot or Renovate should open pull requests that update lockfiles, run tests, regenerate SBOMs, and show vulnerability context. Automatic merge can be reasonable for low-risk patch updates in well-tested services, but major updates and security-sensitive packages need human review. The point is not to freeze dependencies forever. The point is to make change deliberate.
{ "extends": ["config:recommended"], "vulnerabilityAlerts": { "enabled": true, "labels": ["security"] }, "packageRules": [ { "matchUpdateTypes": ["patch", "minor"], "automerge": true, "requiredStatusChecks": ["test", "sbom", "image-scan"] }, { "matchUpdateTypes": ["major"], "labels": ["major-update", "needs-review"] } ]}Finally, SLSA and dependency controls should feed platform policy. If a service is tier one, the platform might require digest deployment, signed images, SBOM attestation, SLSA provenance, lockfile enforcement, and admission verification. If a service is experimental, the platform might require fewer controls but still prevent known dangerous patterns such as latest tags and unscoped internal package names. Senior platform teams design risk tiers instead of pretending every workload has identical assurance needs.
6. The 2026 npm-worm wave
Section titled “6. The 2026 npm-worm wave”The 2026 npm-worm wave changed the shape of the supply-chain lesson. Older dependency attacks often looked like a bad package version, a typosquat, or a vulnerable transitive library. Those still matter, but the newer pattern is more aggressive: the package manager becomes an execution surface, the CI runner becomes a credential source, the publishing workflow becomes a replication path, and the provenance record can become misleading if the trusted workflow itself was abused. The right question is no longer only, “Did this dependency come from the registry?” The sharper question is, “If this dependency executes during install, can it reach anything that lets it publish, deploy, or spread?”
Historical backdoor case studies live in the KCSA supply-chain module as the canonical thread; this section focuses only on npm worm behavior and the controls that stop it from becoming a platform incident.
Here is the mental model. A normal dependency install should be boring: resolve packages, verify integrity, unpack files, and leave a reviewable lockfile trail. A worm tries to turn that boring path into a launch point. It hides code where installers naturally run code, steals credentials from the environment or host, uses those credentials to modify repositories or publish new packages, and then waits for the next downstream install to repeat the cycle. That is why supply chain security cannot rely on a single trust badge. A valid package name, a signed publish event, or an attestation can still be dangerous when the execution environment behind that evidence has been compromised.
┌──────────────────────────────────────────────────────────────────────────────┐│ NPM WORM CONTROL MODEL ││ ││ Dependency install ││ │ ││ ▼ ││ PREVENT: block or review install-time execution ││ │ ││ ▼ ││ CONTAIN: keep publish, cloud, SSH, and deploy credentials unreachable ││ │ ││ ▼ ││ DETECT: flag lockfile drift, native-build execution, and provenance mismatch ││ ││ The goal is not "we can never be reached." The goal is "a reached payload ││ cannot execute freely, cannot find useful credentials, and cannot spread ││ without creating evidence." │└──────────────────────────────────────────────────────────────────────────────┘On 2026-06-01, Microsoft Security Blog and StepSecurity documented the Miasma npm worm : 32 @redhat-cloud-services npm packages across 96 versions were trojanized through the upstream RedHatInsights CI/CD pipeline, abusing its legitimate GitHub Actions OIDC publishing workflow. The primary payload executed through an npm preinstall hook during npm install, including direct and transitive installs, and the obfuscated payload was about 4.3 MB. A “Phantom Gyp” variant used a 157-byte binding.gyp file with gyp command substitution, shaped like "sources": ["<!(node index.js ... && echo stub.c)"], so node-gyp rebuild ran the payload during install without any package.json lifecycle script. The malware harvested GitHub, npm, cloud metadata credentials for AWS IMDS, Azure IMDS, GCP service accounts, SSH material, Kubernetes service-account tokens, and GitHub Actions Runner.Worker process memory. It self-replicated by exchanging stolen npm OIDC tokens for publish rights, committing .github/setup.js through the Git Data API, and forging Sigstore/SLSA provenance.
On 2025-09-15, Unit42, CISA, and Microsoft described the Shai-Hulud self-replicating worm as the first npm worm to self-replicate, affecting 500+ packages including @ctrl/tinycolor. Entry came through a phishing email mimicking an npm security alert. The payload ran TruffleHog to find secrets, and when it found a GitHub token it created a public repository named Shai-Hulud to dump the secrets and pushed a GitHub Actions workflow to every accessible repository. Later, Mini Shai-Hulud on 2026-05-11 became the first campaign to span npm and PyPI.
Those two incidents teach the same platform lesson from different angles. The September 2025 wave showed that stolen maintainer and repository credentials can turn package publishing into automated propagation. The June 2026 wave showed that trusted publishing and provenance can be abused when the legitimate CI path is the thing the attacker controls. A platform that only asks “is this package signed?” misses the second lesson. A platform that only asks “does this package declare a preinstall script?” misses the Phantom Gyp lesson. A platform that only rotates one token after a hit misses the worm lesson, because the payload searched across developer, cloud, CI, SSH, npm, and Kubernetes credential surfaces.
The useful defensive frame is Prevent · Contain · Detect. Prevent means reducing the chance that dependency code runs at install time. Contain means assuming some code may still run and making sure it cannot reach credentials that let it spread. Detect means treating small supply-chain evidence changes as security signals rather than routine dependency churn. The order matters for teaching, but the controls work as a mesh; none of them is enough alone.
Prevent: treat install-time code as untrusted
Section titled “Prevent: treat install-time code as untrusted”The first prevention control for npm is explicit and boring: set ignore-scripts=true in .npmrc. That setting prevents npm lifecycle scripts such as preinstall, install, and postinstall from executing during npm install and npm ci. For a worm whose primary launch point is a lifecycle hook, this is a major reduction in risk because a reached package can still be downloaded but its declared installer code does not automatically run.
ignore-scripts=trueThis control is especially valuable in CI because CI environments often contain more useful credentials than a developer realizes. Even a read-only repository token can reveal private source context, and a job that can request a publish or deploy identity can become a replication path. Blocking lifecycle scripts turns a dependency update from “download and execute unreviewed code” into “download files and let the next gate inspect them.” That is not perfect safety, but it changes the attacker’s timing and creates room for review.
The subtle lesson from Phantom Gyp is that ignore-scripts=true is not a complete model of install-time execution. Native build tooling can execute commands through files that are not visible as package.json lifecycle scripts. A tool that only searches for preinstall and postinstall can therefore report “nothing to see” while a native build path still runs code during installation. The platform response is not to abandon ignore-scripts; it is to pair it with review of native-build files, lifecycle-script allow-lists, and lockfile integrity checks that make a newly introduced build path visible.
{ "targets": [ { "target_name": "review-this-native-build", "sources": ["<!(node index.js ... && echo stub.c)"] } ]}That snippet is not a lab command; it is a pattern to recognize during review. The danger is the <!( command-substitution shape inside binding.gyp, because the package can ask the native build system to run a command as part of preparing source files. A real review should ask why the package needs a native build, who maintains it, whether the lockfile just introduced or changed the native-build metadata, and whether the project has an explicit allow-list for dependencies that are permitted to run installation code.
| Prevent Control | What It Blocks | What It Misses | Pair It With |
|---|---|---|---|
ignore-scripts=true | Declared npm lifecycle hooks during install | Native build execution paths that do not appear as lifecycle scripts | Native-build file review and install-script inventory |
| Install-script allow-list | Surprise preinstall, install, or postinstall declarations | A compromised package already on the allow-list | Expected-hook checks and maintainer review |
| Frozen lockfile install | Silent transitive dependency movement | A malicious version already recorded in the lockfile | Registry signature and provenance verification |
| Scoped registry mapping | Dependency confusion for internal packages | Compromise of the legitimate upstream publisher | Token isolation and package provenance checks |
| Digest-pinned base images | Mutable base image drift | Malicious language package execution | SBOM, signing, and admission enforcement |
Hypothetical scenario: a pull request updates only package-lock.json, and the diff adds a transitive dependency with a native build file. Nobody changed package.json, and tests pass. A listicle answer says, “The build is green, merge it.” A supply-chain answer says, “This is installer-affecting metadata without a manifest change, so review the resolved URL, integrity, dependency edge, install-script flag, and native-build files before trusting the result.” The second answer is slower for one PR and much faster during an incident.
Prevention also means being selective about where package installation is allowed. A build job that installs dependencies should not also be the job that publishes packages, signs production artifacts, deploys workloads, or requests privileged cloud credentials. The more capabilities you place next to npm ci, the more valuable install-time execution becomes. When teams complain that this separation is inconvenient, remind them what the attacker wants: one process that both runs untrusted dependency code and holds a credential powerful enough to spread.
Contain: keep credentials out of the blast radius
Section titled “Contain: keep credentials out of the blast radius”Containment assumes prevention may fail. A developer may override scripts locally to rebuild an audited native dependency. A CI workflow may install a package before a new detector exists. A dependency may execute later through normal application code rather than during installation. Containment asks what the payload can reach when that happens. If the answer is “a long-lived publish token, cloud credentials, repository write access, and deployment identity,” the platform has created a worm-friendly environment.
The safest shape is short-lived, scoped credentials with separate build and release identities. Short-lived means the credential expires quickly enough that theft has a narrow window. Scoped means the credential can do only the job it was minted for. Separate identities mean the job that installs dependencies cannot also mint the identity used to publish or deploy. This is the supply-chain version of least privilege: do not put spread-capable credentials in the process most likely to execute third-party code.
jobs: build: permissions: contents: read steps: - run: npm ci - run: npm test - run: npm run build
publish: needs: build permissions: contents: read id-token: write steps: - run: echo "Publish a reviewed artifact with a short-lived identity"This example is intentionally generic. The important boundary is that the build job can resolve dependencies and produce an artifact, but it cannot request the publishing or deployment identity. The publish job can request a short-lived OIDC identity, but it should not run fresh dependency installation from the public registry. If a dependency payload executes during the build job, there is no publish identity nearby. If the publish job is compromised, there should be no reason for it to resolve arbitrary new packages.
Containment also changes token rotation from a panic ritual into a prepared playbook. If an install-time detector fires, rotate credentials that were reachable from the machines and runners that performed the install. That includes package registry tokens, GitHub tokens, cloud credentials, SSH keys, and Kubernetes service-account tokens when they were present in the environment. Do not rotate only the credential named in the first alert. The worm pattern is credential discovery across many stores, so the rotation boundary should be based on exposure, not on the first string a scanner found.
The most common containment mistake is treating OIDC as magic. OIDC-based publishing is better than storing a long-lived token in a repository secret, but it still has a trust boundary: the job that can request the OIDC token becomes the sensitive environment. If untrusted dependency code runs in that same job, the attacker may not need a stored secret. They may only need the ability to ask the trusted workflow for a fresh identity while the job is alive. Build/deploy isolation is therefore not ceremony; it is what keeps a legitimate trusted publisher from becoming a worm’s publish engine.
| Containment Decision | Weak Version | Stronger Version |
|---|---|---|
| Package publishing | Long-lived token available to all CI jobs | Short-lived publish identity only in the publish job |
| Cloud access | Build job inherits broad cloud credentials | Build job has no cloud write path; deploy job receives scoped identity |
| Repository writes | Default token can push workflow changes | Install and test jobs use read-only repository access |
| Kubernetes access | Runner has a reusable cluster token | Deployment identity is scoped and minted only after artifact verification |
| Developer machines | Broad personal tokens stay logged in forever | Developers use scoped sessions and rotate after suspicious installs |
Containment is the part learners often skip because it feels less exciting than detection. It is also the part that turns a reached package into a non-event. If the payload cannot execute, prevention wins. If it executes but cannot find a useful credential, containment wins. If it executes and reaches something, detection and response have to move fast enough to stop propagation.
Detect: make suspicious dependency evidence loud
Section titled “Detect: make suspicious dependency evidence loud”Detection for npm worms should focus on evidence that changes before, during, or immediately after installation. A lockfile change without a matching manifest change is one of the highest-signal review prompts because npm ci trusts the lockfile. The change may be legitimate, but it deserves human attention because it can redirect a transitive dependency to a different tarball, integrity hash, dependency edge, binary entry, operating-system constraint, or install-script flag without an obvious top-level dependency change.
package.json changed? nopackage-lock.json changed? yesinstaller-affecting field? yes
Result: review as a supply-chain event, not as routine formatting.Signature and provenance verification are also detection controls, but the June 2026 lesson is that they must be interpreted carefully. npm audit signatures, registry attestations, and provenance verification can catch unsigned packages, invalid signatures, or artifacts that do not match expected registry metadata. They are necessary gates. They are not sufficient if the attacker abused the legitimate workflow that creates the provenance. In that case, “valid provenance exists” is weaker than “the expected workflow identity, source, dependency state, and release intent all match what we reviewed.”
The strongest provenance review therefore asks three questions. First, is the package or artifact signed or attested by the expected identity, not merely by any valid identity? Second, did the trusted workflow run in a context where untrusted install-time code could reach publish credentials or OIDC token exchange? Third, does the dependency diff make sense for the change being reviewed? A forged or misleading provenance story often relies on defenders stopping after the first green check.
Native-build file review is the Phantom Gyp-specific detection lesson. Reviewers and tools should not only enumerate lifecycle scripts. They should also inspect files that can trigger native build behavior, especially when those files appear in a package that did not previously need native compilation. The suspicious pattern is not “native code exists”; many legitimate packages compile native extensions. The suspicious pattern is a new or changed native-build path that executes a JavaScript payload, shell command, downloader, or opaque bootstrapper during install.
| Detect Signal | Why It Matters | First Review Question | Response Direction |
|---|---|---|---|
Lockfile changes without package.json changes | The install graph can change without a visible dependency request | Which installer-affecting fields changed? | Review resolved URL, integrity, dependency edges, script flags, and native metadata |
| New lifecycle script in a dependency | Install-time code may execute before tests run | Is this package explicitly allow-listed for install scripts? | Block, review maintainer context, or add a narrow reviewed exception |
New or changed binding.gyp command substitution | Native build tooling can run code without lifecycle scripts | Why does this dependency need command execution during build? | Treat as suspicious unless the native build path is expected and reviewed |
| Signature or attestation failure | Registry evidence does not verify cleanly | Is the package unsigned, mismatched, or affected by a tooling outage? | Block merge until understood; do not silence the gate casually |
| Valid provenance from an unexpected workflow | The artifact may be signed by the wrong identity | Which workflow, source ref, and builder identity produced it? | Require expected identity, not any valid signature |
| Unexpected repository or workflow changes after install | Stolen credentials may have been used to persist or spread | Which token could write those files? | Rotate exposed credentials and audit accessible repositories |
Hypothetical scenario: a registry attestation verifies, but the package was published by a workflow that also ran dependency installation with a publish-capable identity in the same job. That is not the same assurance as a release workflow where the install job cannot mint publish credentials and the publish job only handles a reviewed artifact. The attestation tells you which identity signed; it does not erase the need to inspect what that identity was allowed to do while untrusted code could execute.
The final detection habit is to rehearse the negative case. Create a safe branch that changes lockfile installer metadata without changing the manifest and confirm the tripwire fails. Add a harmless package fixture with an install script and confirm the lifecycle inventory reports it. Add a review-only binding.gyp fixture and confirm reviewers know where it would surface. Run provenance verification against an artifact signed by the wrong identity and confirm policy rejects it. A detector that has never failed in a controlled exercise is usually a dashboard, not an operational control.
The 2026 npm-worm wave should leave you with a practical instinct: install-time execution is a privileged event, not a package-manager detail. Treat it the way you treat deployment access. Keep it small, scoped, reviewed, and observable. When a dependency can execute before your code even starts, the supply chain is already part of your runtime security boundary.
Did You Know?
Section titled “Did You Know?”-
The SolarWinds compromise showed that a trusted software update mechanism can become the delivery path for malicious code when the build process itself is compromised. For the full case study, see CI/CD Pipelines.
-
SBOMs are most useful when they are generated for released artifacts, because source-only inventories can miss base image packages and build-time additions.
-
Keyless signing does not mean unsigned signing; it means the signing key is short-lived and tied to an identity provider instead of a long-lived private key managed by the team.
-
SLSA focuses on artifact integrity and build provenance, so it complements vulnerability scanning rather than replacing scanners, code review, or runtime detection.
Common Mistakes
Section titled “Common Mistakes”| Mistake | Why It Fails | Better Practice |
|---|---|---|
| Deploying mutable tags in production | A tag can be repointed after review, which makes incident evidence ambiguous | Promote and deploy immutable image digests |
| Generating SBOMs only from source directories | Source scans miss operating system packages and base image contents | Generate SBOMs from final image digests |
| Signing images from developer laptops for production | The signature proves a person signed something, not that CI built reviewed source | Accept production signatures only from trusted release workflows |
| Granting publish tokens to every CI step | A compromised scanner or test tool can steal release credentials | Scope secrets and permissions to the job that needs them |
| Trusting broad signature checks | ”Signed by anyone” does not prove the artifact came from the right repository | Verify expected issuer, subject, repository, workflow, and digest |
| Enforcing admission policies without audit rollout | Legitimate workloads may be blocked because image patterns or vendor exceptions were missed | Start with audit, review events, then enforce by namespace or registry scope |
| Treating SBOM findings as automatic release blockers | Some findings are unreachable or mitigated, while others are urgent | Combine SBOM data with exploitability, exposure, and policy |
| Updating dependencies without lockfile review | Transitive changes can enter production without meaningful inspection | Require lockfile diffs, tests, SBOM regeneration, and review |
Question 1
Section titled “Question 1”Your team deploys ghcr.io/acme/api:v3.1.0 in production. During an incident, the registry shows that the tag now points to a digest different from the one recorded in last week’s release notes. The image has a valid vulnerability scan report with no critical findings. What should you check first, and what long-term control would prevent this ambiguity?
Show Answer
Check whether the running Pods are using a tag or an immutable digest. If the manifest uses only v3.1.0, Kubernetes may pull whichever digest the registry currently associates with that tag, depending on pull policy and node cache behavior. The vulnerability scan does not prove the image is the reviewed release; it only describes known issues in the scanned artifact.
The long-term control is to promote and deploy by digest, then sign that digest and verify it at admission. Release metadata should record the tag, digest, source commit, SBOM, signature identity, and provenance. Tags may still exist for human readability, but the cluster should run the immutable digest.
Question 2
Section titled “Question 2”A new CVE affects a transitive Java logging library. Developers search the repository and say the service does not import that library directly. Your SBOM index shows the vulnerable package inside the production image. How should you investigate before deciding whether to block the next release?
Show Answer
First, trace the dependency relationship from the SBOM or build tool to identify which direct dependency introduced the library. Then check whether the vulnerable component is present in the final runtime image, whether the affected code path is reachable, and whether the deployed configuration exposes the exploit prerequisite. The fact that developers do not import the package directly is not enough, because transitive dependencies can still be packaged and reachable.
A reasonable response is to update the direct dependency that brings the vulnerable library, override or exclude the vulnerable version if the ecosystem supports it, rebuild the image, regenerate the SBOM, and verify that the fixed version appears in the released artifact. If exploitability is unclear, document the temporary risk decision and compensating controls instead of ignoring the SBOM finding.
Question 3
Section titled “Question 3”A platform team adds Cosign signing to CI, but admission rejects the image with a subject mismatch. The image was signed successfully, and cosign verify shows a GitHub OIDC certificate. What should you compare, and why is weakening the policy to “any valid signature” the wrong fix?
Show Answer
Compare the certificate issuer and subject from cosign verify with the issuer and subject pattern in the admission policy. The workflow file path, repository name, branch or tag reference, and OIDC issuer must match what the policy expects. A common cause is signing from a different workflow, branch, repository fork, or manual environment than the policy was designed to trust.
Weakening the policy to accept any valid signature proves only that someone signed the image. It no longer proves that the trusted release workflow for the expected repository produced the artifact. The correct fix is to align the policy with the intended release identity or change the workflow so it signs from the expected identity.
Question 4
Section titled “Question 4”A service uses an internal package named internal-utils. An attacker publishes internal-utils with a much higher version number to a public package registry. A CI build unexpectedly installs the public package. Which controls would you add, and which evidence would show the fix is working?
Show Answer
Use scoped package names such as @acme/internal-utils, configure the package manager so the @acme scope resolves only from the private registry, and enforce lockfile-based installation in CI. For npm, that means .npmrc registry mapping and npm ci. For other ecosystems, use the equivalent private registry and lockfile or hash verification controls.
Evidence of the fix includes a lockfile entry whose resolved URL points to the private registry, CI logs showing immutable lockfile installation, dependency review that flags unexpected public packages, and an SBOM showing the expected package coordinates. You can also test the negative case by attempting to install the unscoped public package in a controlled branch and confirming CI fails.
Question 5
Section titled “Question 5”A team claims they have reached SLSA Level 3 because all builds run in GitHub Actions and generate an SBOM. You are reviewing the claim for a production platform. What questions would you ask before accepting or rejecting it?
Show Answer
Ask whether the build produces authenticated provenance that names the artifact digest, source repository, source commit, build workflow, and builder identity. Then ask whether the build environment has the isolation and tamper-resistance expected for the claimed level, and whether provenance is difficult for maintainers or compromised repository workflows to falsify. An SBOM alone is not SLSA provenance; it describes contents, not necessarily the trustworthy relationship between builder, source, and artifact.
You should also ask whether policy verifies the provenance before deployment. A maturity claim is weak if provenance is generated but never checked. The likely conclusion is that hosted CI plus SBOM generation may be useful, but it does not automatically prove SLSA Level 3.
Question 6
Section titled “Question 6”A cluster admission policy begins enforcing signed images for every Pod in every namespace. Several vendor controllers and emergency rollback workloads fail to start. The security team suggests disabling the policy globally to restore service. What safer response would you recommend?
Show Answer
First, restore service with a narrow, time-bound exception rather than disabling verification globally. Scope the exception to the affected namespace, image registry prefix, service account, or specific digest where possible. Record the exception owner and expiration so it does not become permanent bypass policy.
Then review audit data to refine the policy rollout. Owned application namespaces can enforce signatures from internal release workflows, while vendor images may require separate trusted identities, mirrored registries, or approved digest lists. The lesson is not that admission verification is too strict; the lesson is that enforcement should be staged and scoped with operational evidence.
Question 7
Section titled “Question 7”A scanner step in CI needs read-only access to the repository, but the workflow exposes the package publishing token to all jobs through a global environment variable. The scanner action is pinned to a broad version tag. What attack path does this create, and how would you redesign the workflow?
Show Answer
A compromised scanner action could read the publishing token and upload a malicious package or image under the project’s trusted name. The broad version tag adds risk because the action code can change without a reviewed commit update. The scanner becomes a supply chain dependency with access to release credentials.
Redesign the workflow so the scanner job has only read permissions and no publish token. Put publishing in a separate job that runs after tests and scans pass, grant the token only to that job, and pin third-party actions to commit SHAs. If the platform supports OIDC-based publishing or trusted publishing, prefer that over long-lived tokens.
Question 8
Section titled “Question 8”An npm dependency update has no preinstall, install, or postinstall script in package.json, but the diff introduces a native-build file that uses command substitution to run a JavaScript file during installation. Your .npmrc already sets ignore-scripts=true. What should you conclude, and which controls should you pair with ignore-scripts?
Show Answer
Conclude that lifecycle-script blocking is necessary but not sufficient. ignore-scripts=true prevents declared npm lifecycle hooks, but native build tooling can still create install-time execution paths that do not appear as package.json scripts. A review that only searches for preinstall and postinstall can miss the risk.
Pair ignore-scripts with lockfile-integrity review, install-script inventory, native-build file review, registry signature or attestation verification, and build/deploy identity isolation. The important design goal is that a reached package cannot execute unnoticed, cannot reach publish or deploy credentials, and cannot produce misleading provenance without another signal changing.
Hands-On Exercise: Build a Verifiable Release Path
Section titled “Hands-On Exercise: Build a Verifiable Release Path”In this exercise, you will create a small containerized application, generate an SBOM from the built image, scan the SBOM, sign the image digest, attach the SBOM as an attestation, and write an admission policy that would reject unsigned images. You can complete the artifact steps locally with Docker, Syft, Grype, and Cosign. The admission step requires a Kubernetes 1.35 cluster with Kyverno or a compatible image verification controller.
Part 1: Create the Demo Application
Section titled “Part 1: Create the Demo Application”Create a clean working directory so the release evidence is easy to inspect. The application is intentionally small because the lesson is the release path, not the web framework.
mkdir -p supply-chain-democd supply-chain-demo
cat > requirements.txt <<'EOF'flask==2.3.3requests==2.31.0EOF
cat > app.py <<'EOF'from flask import Flask
app = Flask(__name__)
@app.get("/")def hello(): return {"service": "supply-chain-demo", "status": "ok"}
if __name__ == "__main__": app.run(host="127.0.0.1", port=8080)EOF
cat > Dockerfile <<'EOF'FROM python:3.12-slimWORKDIR /appCOPY requirements.txt .RUN pip install --no-cache-dir -r requirements.txtCOPY app.py .CMD ["python", "app.py"]EOF
docker build -t supply-chain-demo:v1 .Part 2: Generate and Inspect the SBOM
Section titled “Part 2: Generate and Inspect the SBOM”Generate the SBOM from the final image, not just from the source directory. Then inspect a few components so you can confirm the inventory includes application dependencies.
syft supply-chain-demo:v1 -o cyclonedx-json > sbom.cdx.json
jq '.components[] | select(.name == "flask" or .name == "requests") | {name, version, type}' sbom.cdx.json
grype sbom:./sbom.cdx.jsonIf Grype reports vulnerabilities, do not treat the output as a rote pass/fail result. Pick one finding and identify the package, version, severity, fixed version if available, and whether the vulnerable package came from requirements.txt or the base image.
Part 3: Push by Tag, Resolve the Digest, and Sign the Digest
Section titled “Part 3: Push by Tag, Resolve the Digest, and Sign the Digest”Set REGISTRY_IMAGE to a registry path you control. Docker Hub, GHCR, or an internal registry can work as long as Cosign can write signatures to it.
export REGISTRY_IMAGE="ghcr.io/YOUR_ORG/supply-chain-demo"export TAG="v1"
docker tag supply-chain-demo:v1 "$REGISTRY_IMAGE:$TAG"docker push "$REGISTRY_IMAGE:$TAG"
export DIGEST="$(docker buildx imagetools inspect "$REGISTRY_IMAGE:$TAG" --format '{{json .Manifest.Digest}}' | tr -d '"')"export IMAGE_REF="$REGISTRY_IMAGE@$DIGEST"
printf 'Immutable image reference: %s\n' "$IMAGE_REF"
cosign sign --yes "$IMAGE_REF"Part 4: Attach and Verify the SBOM Attestation
Section titled “Part 4: Attach and Verify the SBOM Attestation”Attach the SBOM to the same immutable digest. Then verify that the attestation exists. If you used keyless signing, match the certificate identity and issuer to the account or workflow you used.
cosign attest --yes \ --predicate sbom.cdx.json \ --type cyclonedx \ "$IMAGE_REF"
cosign verify-attestation "$IMAGE_REF" \ --type cyclonedx \ --certificate-identity "YOUR_IDENTITY_HERE" \ --certificate-oidc-issuer "YOUR_OIDC_ISSUER_HERE"Part 5: Write an Admission Policy
Section titled “Part 5: Write an Admission Policy”Create a policy that accepts only images from your registry path when they are signed by the expected identity. Apply this first in a non-production cluster or namespace. If you do not have Kyverno installed, still write the policy and explain which fields you would customize for your platform.
apiVersion: kyverno.io/v1kind: ClusterPolicymetadata: name: require-signed-supply-chain-demospec: validationFailureAction: Audit background: false rules: - name: verify-demo-image match: any: - resources: kinds: - Pod verifyImages: - imageReferences: - "ghcr.io/YOUR_ORG/supply-chain-demo*" attestors: - entries: - keyless: issuer: "YOUR_OIDC_ISSUER_HERE" subject: "YOUR_SIGNER_SUBJECT_HERE"Apply the policy in audit mode first, deploy a signed image by digest, and then test an unsigned image or mismatched registry path. The expected learning outcome is not merely “the policy applied.” The outcome is that you can explain why the signed digest is accepted and why the negative case is rejected or audited.
Success Criteria
Section titled “Success Criteria”- You built a demo image and recorded the immutable digest that identifies the pushed artifact.
- You generated an SBOM from the final image and inspected at least two components from the inventory.
- You scanned the SBOM and explained one finding in terms of package source, severity, and remediation path.
- You signed the image digest, not only the mutable tag.
- You attached the SBOM as an attestation to the same digest.
- You wrote an admission policy that verifies an expected signer identity for your registry path.
- You tested or described a negative case where an unsigned or mismatched image would be rejected.
- You can trace the release from source, to digest, to SBOM, to signature, to admission decision.
Reflection Prompts
Section titled “Reflection Prompts”After the lab, answer these questions in your own notes. Which evidence would your incident team query first if a new CVE affected requests? Which identity should production admission trust for this image, and which identities should it reject? Which step in your workflow would be most dangerous if a third-party action or tool were compromised?
Your answers should reveal whether the control design is coherent. If you cannot explain who is trusted to sign, where the SBOM is stored, or how the cluster verifies the digest, the release path is not yet ready for production.
Learner check
Section titled “Learner check”Supply chain security starts with a map, not a scanner.
Use that sentence as a final check on your understanding. Pick one artifact path from this module and name the source evidence, dependency evidence, build identity, registry evidence, admission decision, and runtime evidence you would need during an incident. If your answer stops at “the package was signed” or “the scanner was clean,” revisit the 2026 npm-worm section and identify which Prevent, Contain, and Detect layer you skipped.
Next Module
Section titled “Next Module”Continue to Module 4.5: Runtime Security, where you will extend supply chain assurance into running workloads by detecting suspicious behavior, constraining privilege, and responding when prevention is not enough.
Sources
Section titled “Sources”- Sigstore Cosign Quickstart — Best compact primary walkthrough for keyless signing, verification, and transparency-log concepts used throughout this module.
- GitHub Actions Attest — Shows the current GitHub-native path for signed attestations and SLSA build provenance in CI.
- CISA/NIST: Defending Against Software Supply Chain Attacks — Provides authoritative incident-driven guidance that connects supply-chain controls to real attack patterns and mitigations.
- Kubernetes: Images — Upstream reference for image tags, digests, pull behavior, and why deployment by immutable digest matters.
- CycloneDX specification — Primary specification repository for the SBOM format discussed in the inventory and incident-response sections.
- SPDX specification — Primary specification repository for the SPDX SBOM format and its license/compliance-oriented metadata model.
- SLSA requirements — Official requirements reference for the build-level concepts used in the SLSA maturity section.
- Microsoft Security Blog, 2026-06-02 — Source for the June 2026 npm trusted-publishing compromise, install-hook execution, OIDC abuse, and provenance-forgery lesson.
- StepSecurity, 2026-06-03 — Source for the native-build execution path and
binding.gypreview guidance in the 2026 npm-worm section. - Unit 42 npm supply-chain analysis — Source for the September 2025 npm self-replication pattern, public repository exposure, and credential-harvesting behavior.
- CISA npm ecosystem alert, 2025-09-23 — Federal alert source for npm ecosystem credential-theft response guidance and organizational triage.
- Microsoft Security Blog, 2025-12-09 — Source for later campaign evolution, npm/PyPI scope, and defensive analysis.