Secure your software supply chain. Covers SBOM, dependency scanning, SLSA framework, signed builds, artifact verification, and protecting against supply chain attacks.
Software Supply Chain Security ensures that every component in a software release—from source code to deployed artifact—is trustworthy, verifiable, and protected against tampering from origin to execution. It matters when a dependency, build script, or container image is compromised, and the resulting binary runs in production with undetected vulnerabilities, backdoors, or supply chain attacks like the SolarWinds breach.
The foundation of software supply chain security begins with the source code repository. All changes must be traceable, authenticated, and validated before being accepted into the main branch. The primary mechanism is signed commits using GPG, with verified pull request (PR) merges and protected branches.
A PR must pass at least two checks: a CI pipeline that runs tests and linting, and a manual review with at least one approval. The CI system enforces that all commits are signed with a GPG key and that the PR is merged with a GPG-signed merge commit.
git log --format="%H %an %ae" HEAD~1..HEAD
# Output:
# a1b2c3d4e5f67890123456789012345678901234 Alice Author [email protected]
#
# Check signature:
gpg --verify HEAD.asc HEAD
# Expected output:
# gpg: Signature made Mon Apr 5 10:30:00 2024 UTC
# gpg: using RSA key 1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B
# gpg: Good signature from "Alice Author <[email protected]>"
# gpg: Primary key fingerprint: 1A2B 3C4D 5E6F 7A8B 9C0D 1E2F 3A4B 5C6D 7E8F 9A0B
If the HEAD.asc file is missing or the signature fails, the CI pipeline fails with the error:
ERROR: GPG signature verification failed for merge commit
Expected: 1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B
Got: 2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C
This failure indicates a missing or misconfigured GPG key, or a corrupted merge commit.
To enforce consistent builds, the CI system uses a build.yml file in .github/workflows/, which defines a job that runs make build on each push to main, with the Makefile including a build target that produces a dist/app.tar.gz.
build:
@echo "Building application..."
@mkdir -p dist
@go build -o dist/app app/main.go
@tar -czf dist/app.tar.gz -C dist app
@sha256sum dist/app.tar.gz > dist/app.tar.gz.sha256
@gpg --detach-sign dist/app.tar.gz
@echo "Build completed: dist/app.tar.gz"
The dist/app.tar.gz.sha256 file contains a single line:
a1b2c3d4e5f67890123456789012345678901234567890123456789012345678 dist/app.tar.gz
The CI job uploads this file, along with the GPG signature, as artifacts.
The next step is verifying that the build artifacts are produced by a trusted pipeline and are reproducible. The pipeline must sign the output with a digital signature and generate a provenance record.
The build job outputs a provenance.json file, which includes:
make, ninja, bazel, etc.){
"subject": {
"name": "app.tar.gz",
"digest": "sha256:a1b2c3d4e5f67890123456789012345678901234567890123456789012345678"
},
"builder": {
"name": "Makefile",
"version": "4.3"
},
"build": {
"command": "make build",
"startedAt": "2024-04-05T10:30:00Z",
"finishedAt": "2024-04-05T10:35:00Z",
"inputs": [
{
"name": "app/main.go",
"digest": "sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
},
{
"name": "Makefile",
"digest": "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210"
}
],
"outputs": [
{
"name": "dist/app.tar.gz",
"digest": "sha256:a1b2c3d4e5f67890123456789012345678901234567890123456789012345678"
}
]
},
"signatures": [
{
"keyId": "1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B",
"signature": "-----BEGIN PGP SIGNATURE-----\n...\n-----END PGP SIGNATURE-----"
}
]
}
The provenance.json is signed with the same GPG key used for the source commit. The signing key must be registered in a trusted keyring, and the pipeline validates that the signing key is in the set of known keys.
During deployment, the runtime environment loads the provenance.json and verifies the signature using gpg --verify provenance.json.asc provenance.json.
If the key is not trusted, the system fails with:
ERROR: GPG key not in trusted keyring
Expected: 1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B
Found: 2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B1C
This indicates a misconfigured keyring or a key not distributed to the deployment system.
Once the build artifact is verified, it is packaged into a container image and published to a registry. The registry must support content trust through Notary or a similar system.
The build pipeline pushes the image with docker build and docker push, and then signs it using cosign.
# Build and push image
docker build -t registry.example.com/app:v1.2.3 .
docker push registry.example.com/app:v1.2.3
# Sign the image with cosign
cosign sign registry.example.com/app:v1.2.3
# Prompts for OIDC login, then uploads signature to the registry
The signature is stored as a cosign artifact in the image’s manifest. The image now has a cosign field in its manifest.json:
{
"schemaVersion": 2,
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"config": {
"mediaType": "application/vnd.docker.container.image.v1+json",
"digest": "sha256:abc123def456...",
"size": 1024
},
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:xyz789...",
"size": 2048
}
],
"cosign": {
"signatures": [
{
"payload": "eyJ0aW1lIjogIjIwMjQtMDQtMDVUMTA6MzA6MDAiLCAiZGF0YSI6ICJ0aGlzIGlzIGEgYXJ0aWNsZSIsICJ2ZXJzaW9uIjogMS4yLjMiLCAiZGlzdCI6ICJhcnRpY2xlLmRhdGEifQ==",
"signature": "MEQCIBF6c1b2sK1pL0H1qK1vT4Xc3jY3zD9c9B2vE1YBnCnC",
"keyid": "1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B",
"created": "2024-04-05T10:30:00Z"
}
]
}
}
The payload is a JSON structure containing the image’s metadata, including the version, the time of signing, and the original dist/app.tar.gz digest.
When the runtime pulls the image, it must verify the signature using cosign verify.
cosign verify registry.example.com/app:v1.2.3
# Output:
# Verification for registry.example.com/app:v1.2.3
# The image signature is valid and issued by 1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B
# The payload contains:
# version: 1.2.3
# time: 2024-04-05T10:30:00Z
# dist: app.tar.gz
If the payload does not match the expected values, or the key ID is not in the trusted keyring, the system logs:
ERROR: Image signature verification failed
Expected version: 1.2.3
Got: 1.2.4
Key not in trusted keyring: 1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B
This error points to a mismatch between the build and deployment configurations, or a missing key.
Deployment systems must enforce that only verified images are run. This is achieved by a trusted deployment pipeline that pulls images and verifies their signatures before starting containers.
The deployment configuration includes a deploy.yaml file that defines the image to run and the verification policy.
image: registry.example.com/app:v1.2.3
trusted: true
verify:
- type: "cosign"
keyring: "/etc/trusted-keys.gpg"
expectedVersion: "1.2.3"
expectedDigest: "sha256:a1b2c3d4e5f67890123456789012345678901234567890123456789012345678"
The deployment system reads this file and runs:
# Verify the image
cosign verify \
--keyring /etc/trusted-keys.gpg \
--expected-version 1.2.3 \
--expected-digest sha256:a1b2c3d4e5f67890123456789012345678901234567890123456789012345678 \
registry.example.com/app:v1.2.3
If any verification step fails, the deployment aborts with a clear error:
ERROR: Deployment failed: image verification
- Signature invalid: key 1A2B3C4D5E6F7A8B9C0D1E2F3A4B5C6D7E8F9A0B not in keyring
- Expected version 1.2.3, got 1.2.4
- Expected digest sha256:a1b2c3d4e5f67890123456789012345678901234567890123456789012345678, got sha256:b2c3d4e5f6789012345678901234567890123456789012345678901234567890
The deployment system logs all verification details and provides the full cosign verify output for debugging.
After deployment, the system continuously audits the supply chain. A periodic job runs cosign verify-all on all deployed images and logs the results.
# Verify all images in a deployment manifest
cosign verify-all \
--keyring /etc/trusted-keys.gpg \
--output-format json \
--output-file /var/log/supply-chain/verify-all.json \
--manifest-file /etc/deployment/manifest.yaml
The manifest.yaml file contains a list of images and their expected versions.
- image: registry.example.com/app:v1.2.3
expectedVersion: "1.2.3"
expectedDigest: "sha256:a1b2c3d4e5f67890123456789012345678901234567890123456789012345678"
- image: registry.example.com/db:v1.1.0
expectedVersion: "1.1.0"
expectedDigest: "sha256:c3d4e5f678901234567890123456789012345678901234567890123456789012"
The verify-all.json output includes a list of verification results, with one entry per image, including the raw GPG output, the verification status, and any discrepancies.
This log is used for incident response and compliance audits. When a security team investigates a breach, they can trace back the supply chain from the running process to the original Makefile and provenance.json files.
startedAt timestamps that differ by minutes from the finishedAt time, causing provenance.json validation to fail.provenance.json: If the build step fails to generate provenance.json, the deployment system falls back to a default version, but logs a warning: WARNING: No provenance.json found in build directory
Using default version: 1.0.0
gpg format, not asc or pem. A .gpg keyring file is expected, and the system fails if the file is missing or corrupted.docker build but the provenance.json references a dist/app.tar.gz that was not included in the image layers, the digest verification fails.These subtle but critical details ensure that the software supply chain is not just a pipeline, but a trusted, auditable, and resilient system.
This page was rewritten on 17 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy