Steven's Knowledge

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.com

A kind cluster:

kind create cluster --name supply-chain

Build 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 components

Look at what's in your image:

jq -r '.packages[] | "\(.name) \(.versionInfo // .version)"' sbom.json | head -20

You'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 high

Fix 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 $DIGEST

Option 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' \
  $DIGEST

The 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 $DIGEST

The 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.yaml

Try 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=$DIGEST

You'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-namespace
apiVersion: 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 registry

What's Next

  • Patterns — keyless signing, SBOM management, provenance, dependency pinning, vulnerability triage
  • Best Practices — threat model, key management, false positives, compliance, pitfalls

On this page