Getting Started
Sign a container with Cosign, generate an SBOM with Syft, scan with Grype, verify on admission with Kyverno
Getting Started
This page walks the full supply chain loop on a local container: build → sign → SBOM → scan → verify on admission.
Prerequisites
brew install cosign syft grype # or download from sigstore.dev, anchore.comA kind cluster:
kind create cluster --name supply-chainBuild a Sample Image
# Dockerfile
FROM golang:1.22-alpine AS build
WORKDIR /src
COPY <<EOF main.go
package main
import "net/http"
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("hello\n")) })
http.ListenAndServe(":8080", nil)
}
EOF
RUN go mod init hello && go build -o /hello
FROM alpine:3.19
COPY --from=build /hello /hello
ENTRYPOINT ["/hello"]Build and push to a local registry:
docker run -d -p 5001:5000 --name registry registry:2
docker build -t localhost:5001/hello:v1 .
docker push localhost:5001/hello:v1
# Capture the digest — we want to sign by digest, not tag
DIGEST=$(docker inspect localhost:5001/hello:v1 --format='{{index .RepoDigests 0}}')
echo "$DIGEST"Generate an SBOM
syft localhost:5001/hello:v1 -o spdx-json > sbom.json
wc -l sbom.json
jq '.packages | length' sbom.json # count componentsLook at what's in your image:
jq -r '.packages[] | "\(.name) \(.versionInfo // .version)"' sbom.json | head -20You'll see Go runtime, Alpine packages, and your code. This list is your inventory.
Scan for Vulnerabilities
grype localhost:5001/hello:v1 -o table
# Or scan the SBOM
grype sbom:sbom.json -o table
# CI-friendly: fail if any "high" or "critical"
grype sbom:sbom.json --fail-on highFix what's findable (use a newer base image, upgrade a vulnerable dependency); accept what isn't (no fix yet, known-low-impact).
Sign the Image with Cosign
Option A: Key-based (simple but key management overhead)
cosign generate-key-pair
# Two files: cosign.key (PRIVATE), cosign.pub (public)
cosign sign --key cosign.key $DIGEST
cosign verify --key cosign.pub $DIGESTOption B: Keyless (no key to manage)
This requires a public OIDC issuer that Sigstore trusts (GitHub Actions, Google, Microsoft, etc.) — works out of the box in CI:
# In CI (GitHub Actions, GitLab CI):
COSIGN_EXPERIMENTAL=1 cosign sign $DIGEST
# Browser opens for OIDC auth (or uses workflow identity in CI)
# Verify
COSIGN_EXPERIMENTAL=1 cosign verify \
--certificate-identity 'engineer@company.com' \
--certificate-oidc-issuer 'https://accounts.google.com' \
$DIGESTThe signature, certificate, and a Rekor transparency log entry are produced. Anyone can later verify the signature against the public Sigstore infrastructure.
Attach the SBOM as a Signed Attestation
cosign attest --key cosign.key --predicate sbom.json --type spdx $DIGEST
# Anyone can now fetch
cosign download attestation $DIGEST | jq -r '.payload' | base64 -d | jq
# Verify the attestation chain
cosign verify-attestation --key cosign.pub --type spdx $DIGESTThe SBOM is now cryptographically tied to the image. Tampering breaks the chain.
Enforce on the Cluster: Kyverno
You only deploy images that meet supply chain requirements:
# policy-verify-images.yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-image-signatures
spec:
validationFailureAction: Enforce
rules:
- name: verify-cosign
match:
any:
- resources: { kinds: [Pod] }
verifyImages:
- imageReferences: ['localhost:5001/*']
attestors:
- count: 1
entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
$(cat cosign.pub)
-----END PUBLIC KEY-----kubectl apply -f policy-verify-images.yamlTry to run unsigned and signed images:
# Build an UNSIGNED image
docker build -t localhost:5001/unsigned:v1 .
docker push localhost:5001/unsigned:v1
# Try to deploy it — fails!
kubectl run unsigned --image=localhost:5001/unsigned:v1
# Error: ... image verification failed
# Signed one: succeeds
kubectl run hello --image=$DIGESTYou've closed the loop. Only signed images run; any tampering is rejected at admission.
Equivalent with Sigstore Policy Controller
For Sigstore-native enforcement (more granular than Kyverno's verifyImages):
helm install policy-controller \
sigstore/policy-controller -n cosign-system --create-namespaceapiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
name: require-signed
spec:
images:
- glob: 'localhost:5001/*'
authorities:
- key:
data: |
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----SLSA Provenance (Bonus)
GitHub Actions with the SLSA generator produces verifiable build provenance automatically:
# .github/workflows/build.yml
permissions:
id-token: write
contents: read
packages: write
jobs:
build:
uses: slsa-framework/slsa-github-generator/.github/workflows/builder_container.yml@v2.0.0
with:
image: ghcr.io/${{ github.repository_owner }}/hello
digest: ${{ needs.build.outputs.digest }}
registry-username: ${{ github.actor }}The output is a signed predicateType: "https://slsa.dev/provenance/v1" attestation. Verifiers can confirm: source repo, commit SHA, builder, GitHub Actions workflow path — all signed.
Cleanup
kind delete cluster --name supply-chain
docker rm -f registryWhat's Next
- Patterns — keyless signing, SBOM management, provenance, dependency pinning, vulnerability triage
- Best Practices — threat model, key management, false positives, compliance, pitfalls