Software Supply Chain and Third-Party Exposure: Defending Against Upstream Compromise
Problem
The most efficient way to compromise 10,000 organisations is to compromise one library they all depend on.
Supply chain attacks bypass perimeter defences entirely because the malicious code arrives through trusted channels: a dependency update your build system pulls automatically, a managed service provider with VPN access to your network, or a container base image that ships with a backdoor. Your security posture is irrelevant if the attack arrives inside a component you have already decided to trust.
The attack surface is expanding:
- Open-source dependencies. A typical production application has 200-500 transitive dependencies. Each dependency is maintained by individuals or small teams with varying security practices. A single compromised maintainer account can push malicious code to every downstream consumer.
- Managed service providers. SaaS tools with API access to your infrastructure (CI/CD platforms, monitoring, identity providers, cloud services) are trusted third parties with standing access. A breach at the provider is a breach of every customer.
- Container base images.
ubuntu:latest,node:20,python:3.12are pulled millions of times per day. A compromised base image affects every container built on top of it. - Build infrastructure. CI/CD runners execute arbitrary code from repository configuration. A malicious PR that modifies the CI configuration runs attacker code on your build infrastructure with access to secrets and deployment credentials.
Three high-profile examples demonstrate the pattern: SolarWinds (2020, compromised build system injected backdoor into a software update affecting 18,000 organisations), Log4Shell (2021, vulnerability in a ubiquitous logging library affected virtually every Java application), and the xz utils backdoor (2024, a compromised maintainer inserted a backdoor into a compression library used by most Linux distributions).
Threat Model
- Adversary: Sophisticated attacker targeting the supply chain for maximum downstream impact. Methods include: compromising open-source maintainer accounts, submitting malicious PRs to popular libraries, infiltrating vendor organisations, and poisoning container registries.
- Access level: The attacker gains code execution inside your build or production environment through a trusted dependency or vendor connection. The code runs with the same permissions as the compromised component.
- Objective: Mass compromise. A single supply chain attack can affect thousands of downstream organisations simultaneously. Specific objectives include: credential theft, persistent backdoor installation, data exfiltration, and cryptomining.
- Blast radius: Proportional to the popularity and privilege level of the compromised component. A compromised npm package with 1 million weekly downloads affects 1 million build pipelines. A compromised identity provider affects every SSO-integrated application.
Configuration
1. Pin Every Dependency with Hash Verification
Never trust a version tag alone. Tags can be force-pushed. Hashes cannot be forged.
Go:
# go.sum already contains cryptographic hashes for every dependency.
# Verify hashes on every build:
go mod verify
# If verification fails, the build stops. A modified dependency
# (even with the same version tag) will not match the hash.
Node.js:
# Use npm ci (not npm install) in CI.
# npm ci installs from package-lock.json with exact versions and integrity checks.
# If a dependency hash doesn't match, the install fails.
npm ci
# Verify lockfile integrity
npm audit signatures
Python:
# Pin dependencies with hashes in requirements.txt
pip install --require-hashes -r requirements.txt
# requirements.txt with hashes
# Generate with: pip-compile --generate-hashes requirements.in
cryptography==43.0.3 \
--hash=sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890
requests==2.32.3 \
--hash=sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
Container images:
# Pin by digest, not by tag. Tags are mutable; digests are not.
FROM golang:1.23@sha256:a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2 AS builder
# Get the current digest for an image
docker inspect --format='{{index .RepoDigests 0}}' golang:1.23
2. Verify Provenance with SLSA and Sigstore
Pinning ensures you get the same artefact every time. Provenance verification ensures the artefact was built from the expected source, by the expected build system, with no tampering.
Verify container image signatures with Cosign:
# Install cosign
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# Verify a signed image
cosign verify \
--certificate-identity "https://github.com/your-org/your-repo/.github/workflows/build.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
registry.example.com/app:latest
# If verification fails, the image was not built by your CI pipeline.
Sign your own images in CI:
# .github/workflows/build-sign.yml
name: Build and Sign
on:
push:
branches: [main]
permissions:
contents: read
id-token: write # Required for Sigstore keyless signing
packages: write
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Build image
run: docker build -t registry.example.com/app:${{ github.sha }} .
- name: Push image
run: docker push registry.example.com/app:${{ github.sha }}
- name: Sign image with Cosign (keyless)
uses: sigstore/cosign-installer@v3
- run: cosign sign registry.example.com/app:${{ github.sha }}
env:
COSIGN_EXPERIMENTAL: 1
- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v2.0.0
with:
image: registry.example.com/app
digest: ${{ steps.build.outputs.digest }}
Enforce signature verification at admission:
# kyverno-verify-images.yaml
# Block any container image that is not signed by your CI pipeline.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign-signature
match:
any:
- resources:
kinds:
- Pod
verifyImages:
- imageReferences:
- "registry.example.com/*"
attestors:
- entries:
- keyless:
subject: "https://github.com/your-org/*"
issuer: "https://token.actions.githubusercontent.com"
3. Isolate CI/CD Runners from Production
CI/CD runners execute code from repository configuration. A malicious PR modifies the workflow to exfiltrate secrets or deploy a backdoor.
Harden GitHub Actions runners:
# .github/workflows/secure-build.yml
name: Secure Build
on:
pull_request:
# Minimal permissions - only what the build needs
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
# Pin ALL actions by commit SHA, not by tag
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88eb36e1752f9e5c0
with:
go-version: '1.23'
- name: Build
run: go build ./...
- name: Test
run: go test ./...
# No access to deployment secrets in PR builds.
# Deployment secrets are only available in main branch workflows.
Restrict secrets access by branch:
# Only main branch builds can access deployment secrets.
# PR builds from forks cannot access any secrets.
jobs:
deploy:
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
runs-on: ubuntu-latest
environment: production # Requires manual approval for production deployments
steps:
- name: Deploy
env:
DEPLOY_TOKEN: ${{ secrets.DEPLOY_TOKEN }}
run: ./deploy.sh
4. Monitor Third-Party Vendor Access
Managed service providers with API access to your infrastructure are third-party risk vectors.
# vendor-access-audit.yaml
# Track and alert on third-party vendor access patterns.
groups:
- name: vendor-access
interval: 5m
rules:
# Alert when a vendor service account performs unusual actions
- alert: VendorUnusualActivity
expr: >
count by (vendor, action) (
rate(api_requests_total{user_type="service_account", vendor!=""}[1h])
)
> 3 * avg_over_time(
count by (vendor, action) (
rate(api_requests_total{user_type="service_account", vendor!=""}[1h])
)[7d:1h]
)
for: 15m
labels:
severity: warning
annotations:
summary: "Vendor {{ $labels.vendor }} performing 3x more {{ $labels.action }} than baseline"
# Alert on new API endpoint accessed by vendor
- alert: VendorNewEndpoint
expr: >
api_requests_total{user_type="service_account", vendor!=""}
unless on (vendor, endpoint)
api_requests_total{user_type="service_account", vendor!=""} offset 30d
labels:
severity: info
annotations:
summary: "Vendor {{ $labels.vendor }} accessed new endpoint: {{ $labels.endpoint }}"
5. SBOM-Based Vulnerability Response
When a supply chain vulnerability is disclosed (the next Log4Shell), you need to know within minutes whether you are affected. SBOMs provide the inventory.
# .github/workflows/sbom-continuous.yml
name: SBOM Generation
on:
push:
branches: [main]
jobs:
sbom:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11
- name: Build image
run: docker build -t app:${{ github.sha }} .
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: app:${{ github.sha }}
format: spdx-json
output-file: sbom.spdx.json
- name: Scan for vulnerabilities
uses: anchore/scan-action@v4
with:
sbom: sbom.spdx.json
fail-build: true
severity-cutoff: high
- name: Store SBOM for future queries
uses: actions/upload-artifact@v4
with:
name: sbom-${{ github.sha }}
path: sbom.spdx.json
retention-days: 365
#!/bin/bash
# check-dependency.sh
# When a new CVE is disclosed, check all SBOMs for the affected package.
PACKAGE_NAME="${1}"
PACKAGE_VERSION="${2}"
echo "=== Checking for ${PACKAGE_NAME} ${PACKAGE_VERSION} across all SBOMs ==="
for sbom in /var/lib/sbom-store/*.spdx.json; do
if jq -e ".packages[] | select(.name == \"${PACKAGE_NAME}\" and .versionInfo == \"${PACKAGE_VERSION}\")" "${sbom}" > /dev/null 2>&1; then
SERVICE=$(basename "${sbom}" .spdx.json)
echo "AFFECTED: ${SERVICE} contains ${PACKAGE_NAME}@${PACKAGE_VERSION}"
fi
done
Expected Behaviour
- Dependency integrity: Every build verifies dependency hashes. A modified dependency (same version, different content) fails the build immediately.
- Image provenance: Every production container image is signed by your CI pipeline. Unsigned images are rejected at admission by Kyverno.
- CI/CD isolation: PR builds have no access to deployment secrets. Only main branch builds can deploy. All actions are pinned by commit SHA.
- Vendor monitoring: Third-party service account access patterns are baselined. Unusual activity volume or new endpoint access generates alerts.
- SBOM inventory: Every production image has a corresponding SBOM. When a supply chain CVE is disclosed, you can determine affected services within minutes.
Trade-offs
| Control | Impact | Risk | Mitigation |
|---|---|---|---|
| Digest pinning for container images | Must manually update digests when upgrading base images | Stale base images if digests are not updated | Automate digest updates with Renovate or Dependabot. |
| Hash-verified dependencies | Build fails if any upstream package changes content at the same version | Legitimate package re-publication (yanked and re-released) breaks builds | Review and update hashes when a package is re-published. This is the correct behaviour; content changes at the same version are exactly what you want to detect. |
| CI/CD secret isolation | PR builds cannot test deployment paths | Deployment-related tests fail in PR builds | Use mock credentials for deployment tests. Real deployments only from main branch. |
| Image admission enforcement (Kyverno) | Unsigned images are blocked, including during incidents | Emergency deployment of an unsigned image is rejected | Break-glass namespace with relaxed admission policy. All break-glass deployments reviewed within 24 hours. |
| SBOM storage (365-day retention) | Storage cost for SBOMs across all builds | Large volume of SBOM artefacts | SBOMs are small (100KB-1MB). 365 days of daily builds for 50 services: under 20GB total. |
Failure Modes
| Failure | Symptom | Detection | Recovery |
|---|---|---|---|
| Compromised dependency passes hash check | Attacker compromises the build system and updates both the package and the hash | Build succeeds with malicious dependency | SLSA provenance verification catches this: the build provenance will show it was built from a different source. Multi-layer verification (hash + signature + provenance) is required. |
| Kyverno blocks legitimate deployment | Signed image rejected because certificate identity does not match policy | Deployment fails with admission webhook error | Check certificate identity in Cosign signature. Update Kyverno policy if the CI workflow changed. |
| Vendor compromise detected too late | Vendor service account exfiltrates data before baseline alert fires | Data breach from vendor access | Reduce vendor access to minimum required. Implement real-time alerting (not baseline-based) for vendor access to sensitive resources. |
| SBOM missing for production image | Cannot determine if a disclosed CVE affects a running service | SBOM query returns no results for a known production service | Ensure SBOM generation is a required CI step (fail the build if SBOM generation fails). Backfill SBOMs for any running images without one. |
When to Consider a Managed Alternative
- Snyk: Dependency vulnerability scanning with reachability analysis. Determines whether a vulnerable function is actually called by your code (not just present in the dependency tree). Automated fix PRs.
- Chainguard: Hardened, minimal container base images with SBOM and SLSA provenance built in. Eliminates the need to harden
ubuntu:latestyourself by providing images built specifically for production security. - Sonatype: Dependency firewall that blocks known-malicious packages before they enter your build. Repository proxy that enforces policy on which packages can be downloaded.
Premium content pack: Supply chain defence templates. Kyverno policies for image signature verification. GitHub Actions workflows with SLSA provenance generation. SBOM query scripts for vulnerability response. Vendor access monitoring configurations.
Related Articles
- Dependency Pinning and Lockfile Integrity: Preventing Supply Chain Attacks in CI
- SLSA Provenance for Container Images: From Build to Admission Control
- Software Bill of Materials (SBOM) Generation and Consumption in CI/CD
- AI Supply Chain Attack Surface: Models, Datasets, and Inference Dependencies
- Artifact Integrity Verification: Checksums, Signatures, and Transparency Logs