Software Supply-Chain Security, SBOMs, Signatures, Provenance, Trusted Agents, and Dependency Controls: Guided Hands-On Workflow and Core Operations
Create the chapter’s first end-to-end evidence chain on a disposable agent, then prove that tampering breaks verification.
Learning objectives
- Create a deterministic synthetic artifact and bind it to an exact source SHA.
- Generate and inspect a CycloneDX SBOM with Syft.
- Hash and sign the artifact with a disposable Cosign key, then verify it.
- Create a minimal SLSA-shaped provenance teaching statement that names the artifact digest and builder/source identity.
- Tamper with a copy and preserve the failing verification as evidence.
1. Preflight: disposable resources only
Use a synthetic repository and a dedicated agent label such as supply-chain-lab. Do not run this on the built-in node. The private key below is a throw-away lab key; production signing should use a dedicated signing service, KMS/HSM or a carefully reviewed keyless identity flow.
| Resource | Lab identity | Guard |
|---|---|---|
| Job | labs/supply-chain-demo | Delete only this job if you created it for the lab. |
| Agent | supply-chain-lab-01 | No production credentials or Docker socket. |
| Signing key | cosign-lab.key | Disposable; never archive it. |
| Artifact | dist/hello- | Unique to this build. |
| Evidence | evidence/* | Archive reports/bundles, not private keys. |
2. Local tool check
Install Syft and Cosign by your organization’s verified package/binary process. Do not pipe an unaudited remote installer into a privileged shell. Record versions and, where practical, the distribution checksum/signature used to install them.
set -euo pipefail
syft version
cosign version
sha256sum --version | head -1
git --version
jq --version3. Pipeline workflow
The Pipeline below deliberately keeps build, SBOM, signing and verification as separate stages so their evidence cannot be mistaken for one another. The signing key is created only for the disposable lab. A real system should not generate a long-lived production signing key inside a general-purpose workspace.
pipeline {
agent { label 'supply-chain-lab' }
options { timestamps(); disableConcurrentBuilds() }
environment { ARTIFACT = "dist/hello-${BUILD_NUMBER}.txt" }
stages {
stage('Record identity') {
steps {
sh '''set -euo pipefail
mkdir -p dist evidence
git rev-parse HEAD | tee evidence/source-sha.txt
syft version > evidence/syft-version.txt
cosign version > evidence/cosign-version.txt
uname -a > evidence/agent-platform.txt
'''
}
}
stage('Build exact bytes') {
steps {
sh '''set -euo pipefail
printf 'hello from source %s build %s\n' "$(cat evidence/source-sha.txt)" "$BUILD_NUMBER" > "$ARTIFACT"
sha256sum "$ARTIFACT" | tee evidence/artifact.sha256
'''
}
}
stage('Generate SBOM') {
steps {
sh '''set -euo pipefail
syft dir:. -o cyclonedx-json=evidence/sbom.cdx.json
sha256sum evidence/sbom.cdx.json > evidence/sbom.cdx.json.sha256
'''
}
}
stage('Sign + verify lab artifact') {
steps {
withCredentials([string(credentialsId: 'lab-cosign-passphrase', variable: 'COSIGN_PASSWORD')]) {
sh '''set -euo pipefail
umask 077
cosign generate-key-pair
cosign sign-blob --yes --key cosign.key --bundle evidence/artifact.sigstore.json "$ARTIFACT"
cosign verify-blob --key cosign.pub --bundle evidence/artifact.sigstore.json "$ARTIFACT" | tee evidence/signature-verification.txt
rm -f cosign.key
'''
}
}
}
}
post {
always {
archiveArtifacts artifacts: 'dist/**,evidence/**,cosign.pub', fingerprint: true, allowEmptyArchive: true
}
}
}COSIGN_PASSWORD is a Jenkins Secret Text credential containing a fake lab-only passphrase. Jenkins masking reduces accidental log exposure but is not a data-loss-prevention boundary; the Pipeline never echoes the variable or archives the private key.
4. Expected state after a successful run
| Layer | Expected evidence |
|---|---|
| Build | Exact job/build number and source SHA retained. |
| Agent | Label, node name, workspace and platform visible in Jenkins/log evidence. |
| Artifact | One unique file with recorded SHA-256. |
| SBOM | CycloneDX JSON plus its own checksum. |
| Signature | Cosign bundle plus public key; private key removed before archive. |
| Verification | A successful verify-blob transcript bound to the artifact bytes. |
A successful signing stage does not prove the SBOM is complete; a successful SBOM stage does not prove the artifact was signed; and a successful archive step only proves Jenkins retained files matching the pattern.
5. Add a minimal provenance teaching statement
For pedagogy, create a small in-toto Statement/SLSA provenance-shaped JSON file. It is intentionally labelled a teaching statement: real provenance should be emitted by a build platform component whose identity and isolation are independently trustworthy.
set -euo pipefail
ARTIFACT="dist/hello-${BUILD_NUMBER}.txt"
DIGEST="$(sha256sum "$ARTIFACT" | awk '{print $1}')"
SOURCE_SHA="$(cat evidence/source-sha.txt)"
jq -n \
--arg name "$(basename "$ARTIFACT")" \
--arg digest "$DIGEST" \
--arg source "$SOURCE_SHA" \
--arg build "$BUILD_TAG" \
'{
"_type": "https://in-toto.io/Statement/v1",
"subject": [{"name": $name, "digest": {"sha256": $digest}}],
"predicateType": "https://slsa.dev/provenance/v1",
"predicate": {
"buildDefinition": {
"buildType": "https://devops-academy.example/jenkins-lab/v1",
"externalParameters": {"sourceSha": $source},
"internalParameters": {},
"resolvedDependencies": []
},
"runDetails": {
"builder": {"id": "https://devops-academy.example/builder/jenkins-supply-chain-lab"},
"metadata": {"invocationId": $build}
}
}
}' > evidence/provenance.json
jq '.subject, .predicate.runDetails.builder' evidence/provenance.jsonNotice the explicit subject digest. Without it, provenance cannot establish which artifact bytes the statement describes.
6. Intentional failure: tamper with a copy
Preserve the successful evidence first. Then copy the artifact, mutate the copy and verify it with the original bundle.
set -euo pipefail
cp "$ARTIFACT" evidence/tampered.txt
printf 'tampered\n' >> evidence/tampered.txt
sha256sum "$ARTIFACT" evidence/tampered.txt | tee evidence/tamper-digests.txt
set +e
cosign verify-blob --key cosign.pub --bundle evidence/artifact.sigstore.json evidence/tampered.txt \
> evidence/tampered-verification.txt 2>&1
rc=$?
set -e
printf 'tampered_verify_exit=%s\n' "$rc" | tee -a evidence/tampered-verification.txt
test "$rc" -ne 0The correct outcome is a non-zero verification result. Do not “fix” this by signing the tampered copy; that would create a new signed artifact, not prove equivalence to the original artifact.
7. Optional keyless path
Sigstore keyless signing uses an OIDC identity and short-lived certificate. It is attractive because it avoids distributing long-lived private keys, but a verifier must check the expected certificate identity and OIDC issuer. Interactive public keyless flows may be inappropriate for a headless local lab unless the CI identity integration is explicitly designed. Treat this as an optional architecture exercise rather than requiring a real external account.
8. Small challenge
A team wants pull requests from forks to generate SBOMs but never sign release artifacts. Where should the control live?
Answer target: allow SBOM generation on an unprivileged disposable test agent, but route signing to a separate trusted agent/service whose scheduling and credentials are unavailable to untrusted fork code. The distinction is an agent/identity/trust control, not merely a different shell command.
Knowledge check
Answer before revealing the explanation.
1. Why archive the Cosign public key and bundle but not the private key?
Verification material is evidence; the private signing key is authority. Archiving the private key would hand signing authority to anyone who can read the artifact archive.
2. Why does the tampered-copy test use the original signature bundle?
It tests whether verification remains bound to the original bytes. Re-signing the tampered file would create a different claim.
3. What does Jenkins fingerprinting add here?
It adds Jenkins-side tracking of archived file usage/identity, but it does not replace a cryptographic signature, SBOM, provenance or external repository digest.
4. Why is keyless not automatically safer in every Jenkins deployment?
It removes long-lived private-key distribution but shifts trust to workload/OIDC identity, issuer configuration, certificate identity matching and CI integration. Those controls still need review.
Official references and version notes
Use primary documentation because supply-chain formats, signing defaults and Jenkins security guidance evolve.
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0Send only Ethereum/ERC-20 compatible assets to this address.