Excerpt: Supply chain attacks targeting CI/CD pipelines have become one of the most dangerous vectors in modern software security. This guide covers dependency pinning, SBOM generation, Sigstore/Cosign pipeline signing, artifact verification, runner hardening, and Kyverno policy enforcement to build a hardened software delivery pipeline.
Introduction
The compromise of a CI/CD pipeline can be catastrophic. Unlike a single application vulnerability, a poisoned build pipeline can propagate malicious code to every system that consumes its output. The SolarWinds attack demonstrated that even well-resourced organizations can fall victim when the software supply chain is compromised. In 2025 and beyond, securing the pipeline itself has become as critical as securing the application it builds.
This article walks through a layered approach to supply chain security: from pinning dependencies and generating Software Bills of Materials (SBOMs) to cryptographically signing artifacts with Sigstore and enforcing policy at the Kubernetes admission layer with Kyverno.
Understanding the Supply Chain Attack Surface
A modern CI/CD pipeline touches many trust boundaries. Source code repositories, package registries, build runners, container registries, and deployment targets all represent potential entry points for an attacker. Supply chain attacks typically exploit one of three vectors:
- Dependency confusion — uploading a malicious package to a public registry with the same name as a private internal package, exploiting registry resolution order
- Compromised maintainer accounts — taking over a popular open-source package maintainer account and injecting malicious code into a legitimate package update
- CI runner compromise — gaining execution on a shared build runner to exfiltrate secrets or tamper with build artifacts before signing
Each vector requires a different defensive control, but they share a common theme: trust must be explicitly established and cryptographically verifiable at every step.
Dependency Pinning and Lock Files
The first line of defense is preventing unexpected dependency updates from entering your build. Floating version specifiers like ^1.2.0 or ~4.5 allow patch and minor updates to be pulled in automatically — updates that may include compromised code.
Pin all dependencies to exact versions and commit lock files to version control:
# package.json — pin exact versions
{
"dependencies": {
"express": "4.18.2",
"lodash": "4.17.21"
}
}
# Always commit package-lock.json, yarn.lock, or pnpm-lock.yaml
# In CI, use --frozen-lockfile (yarn/pnpm) or --ci (npm)
npm ci # Fails if package-lock.json is out of sync
For container base images, pin to digest rather than tag. Tags are mutable — a tag can be reassigned to a different image without notice:
# Avoid: mutable tag
FROM node:20-alpine
# Prefer: immutable digest
FROM node:20-alpine@sha256:a1b2c3d4e5f6...
# Use tools like crane to resolve digests
crane digest node:20-alpine
For Python projects, use pip-compile from pip-tools to generate a fully-pinned requirements.txt from a requirements.in file. For Go, the go.sum file provides cryptographic verification of all module versions.
Software Bill of Materials (SBOM) Generation
An SBOM is a machine-readable inventory of all components in a software artifact — analogous to an ingredients list. When a new vulnerability is disclosed (e.g., a new Log4Shell-class CVE), an accurate SBOM lets you instantly determine which of your applications are affected without manual source code analysis.
Generate SBOMs as part of every build using tools like Syft:
# Install Syft
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh
# Generate SBOM for a container image in SPDX format
syft example-corp.com/myapp:1.2.3 -o spdx-json > sbom.spdx.json
# Generate SBOM in CycloneDX format (preferred for toolchain integration)
syft dir:. -o cyclonedx-json > sbom.cyclonedx.json
# Scan SBOM for known vulnerabilities with Grype
grype sbom:sbom.cyclonedx.json --fail-on high
Attach the SBOM to the container image using OCI annotations so it travels with the artifact:
# Attach SBOM to image using ORAS
oras attach example-corp.com/myapp:1.2.3 \
--artifact-type application/vnd.cyclonedx+json \
sbom.cyclonedx.json:application/vnd.cyclonedx+json
Pipeline Signing with Sigstore and Cosign
Sigstore is an open-source project that provides a transparent, auditable certificate authority and signing infrastructure for software artifacts. Cosign is the primary tool for signing and verifying container images using Sigstore’s Fulcio CA and Rekor transparency log.
The keyless signing workflow uses short-lived certificates tied to OIDC identities (e.g., a GitHub Actions workflow), eliminating the need to manage long-lived signing keys:
# In GitHub Actions — keyless signing via OIDC
- name: Sign container image
env:
COSIGN_EXPERIMENTAL: "true"
run: |
cosign sign \
--rekor-url https://rekor.sigstore.dev \
example-corp.com/myapp:${{ github.sha }}
# The signing identity is tied to the workflow OIDC token:
# Subject: https://github.com/example-corp/myapp/.github/workflows/build.yml@refs/heads/main
# Issuer: https://token.actions.githubusercontent.com
To verify an image before deployment:
# Verify image was signed by the expected workflow
cosign verify \
--certificate-identity "https://github.com/example-corp/myapp/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
example-corp.com/myapp:latest
# Verify and extract the SBOM attestation
cosign verify-attestation \
--type cyclonedx \
--certificate-identity "..." \
--certificate-oidc-issuer "..." \
example-corp.com/myapp:latest | jq .payload | base64 -d | jq .
Build Runner Hardening
Build runners are high-value targets because they have access to source code, secrets, and artifact registries. Shared runners (where multiple repositories share the same runner instance) carry particularly high risk — a malicious job in one repository can potentially read secrets or tamper with artifacts of concurrent jobs.
Key runner hardening practices:
- Use ephemeral runners — each job gets a fresh VM or container that is destroyed after the job completes. GitHub Actions self-hosted runners support JIT (just-in-time) registration for this purpose
- Network isolation — runners should only reach the registries and services they need. Use egress filtering to block exfiltration to unknown destinations
- Secret management — never store secrets in environment variables that persist across steps. Use OIDC-based short-lived credentials (AWS IAM roles, GCP Workload Identity) instead of long-lived service account keys
- Read-only filesystem — mount the runner workspace as read-only except for explicitly designated writable paths
# GitLab CI — ephemeral Docker runner with network restrictions
job:
image: node:20-alpine@sha256:a1b2c3...
variables:
DOCKER_DRIVER: overlay2
before_script:
- npm ci --frozen-lockfile
script:
- npm run build
- npm run test
tags:
- ephemeral # Routes to ephemeral runner pool
Artifact Verification at Deploy Time
Signing artifacts is only valuable if verification is enforced at deployment time. Without enforcement, a compromised delivery mechanism could substitute an unsigned or differently-signed image without detection.
In a Kubernetes environment, admission controllers provide the enforcement point. The policy should reject any image that cannot be verified against the expected signing identity:
# Using Sigstore Policy Controller (admission webhook)
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signed-images
spec:
images:
- glob: "example-corp.com/**"
authorities:
- keyless:
url: https://fulcio.sigstore.dev
identities:
- issuer: https://token.actions.githubusercontent.com
subjectRegExp: "https://github.com/example-corp/.*"
Kyverno Policy Enforcement
Kyverno is a Kubernetes-native policy engine that can enforce a wide range of supply chain security requirements beyond image signing. It operates as an admission webhook and can validate, mutate, and generate Kubernetes resources.
# Require all images to have a valid digest (not just a tag)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Enforce
rules:
- name: check-image-digest
match:
any:
- resources:
kinds: [Pod]
validate:
message: "Images must be referenced by digest, not tag."
foreach:
- list: "request.object.spec.containers"
deny:
conditions:
any:
- key: "{{ element.image }}"
operator: NotContains
value: "@sha256:"
---
# Require SBOM attestation to be present
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-sbom-attestation
spec:
validationFailureAction: Enforce
rules:
- name: check-sbom
match:
any:
- resources:
kinds: [Pod]
verifyImages:
- imageReferences:
- "example-corp.com/*"
attestations:
- predicateType: https://cyclonedx.org/bom
Monitoring and Alerting on Policy Violations
Policy enforcement prevents bad artifacts from deploying, but you also need visibility into attempted violations. Configure Kyverno to emit events to your SIEM for every policy violation and alert when unexpected signing identities appear:
# Kyverno generates Kubernetes events for all violations
# Forward with Fluent Bit or Vector to your SIEM
# Key alert conditions:
# - PolicyViolation events in production namespaces
# - Cosign verification failures in admission logs
# - Images signed by unexpected identities
# Example Wazuh rule for Kyverno policy violations
<rule id="100200" level="12">
<if_sid>100100</if_sid>
<field name="kubernetes.reason">PolicyViolation</field>
<description>Kyverno policy violation in production</description>
<group>supply_chain,policy_violation</group>
</rule>
A Reference Pipeline Architecture
A complete supply chain-hardened pipeline integrates all of these controls into a coherent workflow:
- Pre-commit — secret scanning (GitLeaks), dependency audit (npm audit, pip-audit)
- Build — pinned dependencies, locked base images, ephemeral runners
- Test — SAST scan, dependency vulnerability scan (Grype)
- Package — SBOM generation (Syft), SBOM vulnerability scan
- Sign — Cosign keyless signing, SBOM attestation attachment to image
- Publish — push to private registry with immutable digest references
- Deploy — Kyverno admission policy enforces signing and SBOM requirements
- Runtime — Falco runtime anomaly detection on deployed containers
Conclusion
Securing the software supply chain is not a single control but a layered system of trust. Each layer — pinned dependencies, SBOMs, cryptographic signing, hardened runners, and enforced admission policies — addresses a distinct attack vector. The key insight is that trust must be established at build time and cryptographically verifiable at deploy time, with no gaps in the chain.
Start with dependency pinning and SBOM generation as foundational hygiene, layer in Cosign signing tied to your CI OIDC identity, and enforce verification with Kyverno policies. Instrument everything so that policy violations surface immediately in your security operations workflow. A pipeline that an attacker cannot silently tamper with is your strongest defense against supply chain compromise.
