Supply Chain Security

Your system isn't just the code you write — it's every dependency you import, every base image you build on, every tool in your build pipeline, and every registry you pull from. A vulnerability in any of these is a vulnerability in your system. Supply chain security is the practice of understanding, controlling, and verifying what goes into your deployments.

Dependencies

The Problem

Every third-party dependency is a trust decision. You're importing code written by someone else, maintained on their schedule, with their security practices. Most open source maintainers are careful. Some aren't. And even careful maintainers have their accounts compromised.

The risk isn't theoretical. Dependency confusion attacks, typosquatting, and compromised maintainer accounts have all resulted in malicious code being distributed through legitimate package managers.

Practices

Pin exact versions. Use lock files (go.sum, package-lock.json, poetry.lock) and commit them. A lock file ensures that every build uses exactly the dependencies you tested — not whatever latest happens to be today.

Audit regularly. Run vulnerability scanners against your dependency tree as part of CI:

# Go
govulncheck ./...

# Node
npm audit

# Python
pip-audit

These tools check your locked versions against known vulnerability databases. They won't catch zero-days, but they'll catch the vast majority of known issues — which is where most real-world supply chain attacks land.

Minimize your dependency tree. Every dependency you add is a dependency you maintain. Before importing a library, consider whether the functionality justifies the trust relationship. A utility package that saves ten lines of code but pulls in thirty transitive dependencies is a poor trade.

Review major version upgrades deliberately. Automated dependency updates (Dependabot, Renovate) are useful for patch versions. Major version bumps deserve human review — read the changelog, understand what changed, and verify the upgrade in staging before it reaches production.

Container Images

Base Images

Every container image starts from a base. That base image is part of your supply chain — its vulnerabilities are your vulnerabilities, its packages are your attack surface.

Start minimal. Use the smallest base image that supports your application. For Go services, a scratch or distroless image contains only your binary — no shell, no package manager, no utilities an attacker could use. For services that need a runtime, use slim variants (ubuntu:24.04 over a full desktop image, python:3.12-slim over the full image).

Use custom base images for consistency. Rather than configuring each image independently, build a standard base image with your hardening applied — non-root user, CA certificates, log agent — and derive service images from it. See host-config for the provisioning patterns that apply equally to container base images.

# Build stage
FROM golang:1.23 AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 go build -o /app ./cmd/myservice

# Runtime stage — minimal attack surface
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=build /app /app
ENTRYPOINT ["/app"]

The multi-stage build ensures build tools (compilers, package managers) never appear in the final image.

Image Scanning

Scan images for known vulnerabilities before they reach production. Integrate scanning into your CI pipeline so that a vulnerable image blocks deployment:

# Scan with Trivy
trivy image --severity HIGH,CRITICAL myregistry/myservice:latest

Scanning catches known CVEs in OS packages and application dependencies baked into the image. It doesn't catch logic bugs or misconfiguration — those require code review and testing.

Image Signing and Verification

Sign your images after building them, and verify signatures before deploying. This closes the gap between "the CI pipeline built this image" and "the orchestrator is running this image" — ensuring nothing was tampered with in between.

Cosign (from the Sigstore project) is the standard tool:

# Sign after build
cosign sign --key cosign.key myregistry/myservice:latest

# Verify before deploy
cosign verify --key cosign.pub myregistry/myservice:latest

In Kubernetes, admission controllers like Kyverno or OPA Gatekeeper can enforce that only signed images are deployed — rejecting unsigned or tampered images at the API server level.

Private Registries

Host your images in a private registry rather than pulling from public registries at deploy time. This gives you control over what's available to your infrastructure, avoids rate limits, and eliminates the risk of a public image being deleted or replaced.

Mirror the upstream images you depend on into your private registry, scan them on import, and pull only from the mirror in production.

Build Pipeline Integrity

The CI/CD pipeline itself is part of the supply chain. If an attacker can modify your pipeline, they can inject code into every build.

Treat pipeline definitions as code. Store CI/CD configuration in the repository, subject to the same review process as application code. A change to a GitHub Actions workflow or a Dockerfile deserves the same scrutiny as a change to business logic.

Pin action versions by SHA, not tag. Tags are mutable — a compromised action can be silently updated:

# Fragile — tag can be reassigned
- uses: actions/checkout@v4

# Stable — pinned to an immutable commit
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1

Isolate build environments. Build steps should not have access to production credentials, deployment keys, or secrets they don't need. Use scoped tokens with minimal permissions, and rotate them regularly.

Produce a build manifest. Record what went into each build: the source commit, dependency versions, base image digest, and build tool versions. This manifest — sometimes called a Software Bill of Materials (SBOM) — is the foundation for auditing what's running in production and tracing a vulnerability back to the affected deployments.

# Generate an SBOM with Syft
syft myregistry/myservice:latest -o spdx-json > sbom.json

References