Image Supply Chain Security
Secure the container image supply chain at every stage: scan for CVEs in CI, sign images with Cosign, generate SBOMs, and enforce image admission in the cluster with Kyverno or Gatekeeper. Use a private registry with pull-through cache to avoid dependency on public registries.
Image Vulnerability Scanning
Scan images in CI before they reach the registry; block critical CVEs at build time, not at runtime — fixing a CVE in a running pod is far harder than rebuilding the image.
# Trivy: scan a local or remote image
trivy image myapp:latest
trivy image --severity CRITICAL,HIGH myapp:latest # only critical and high
trivy image --exit-code 1 --severity CRITICAL myapp:latest # fail CI on critical
# Scan a running cluster for vulnerable images
trivy k8s --report summary cluster # whole cluster
trivy k8s --report summary --namespace production cluster # specific namespace
# Scan a local directory (source code, IaC)
trivy fs --security-checks vuln,config ./
trivy config --severity HIGH,CRITICAL ./terraform/ # IaC misconfigs
# Grype: alternative scanner (good for SBOM-based workflows)
grype myapp:latest
grype sbom:./sbom.json # scan from a pre-generated SBOM
# Integrate in CI (GitHub Actions example):
# - uses: aquasecurity/trivy-action@master
# with:
# image-ref: 'myapp:${{ github.sha }}'
# severity: 'CRITICAL,HIGH'
# exit-code: '1'SBOM Generation
A Software Bill of Materials (SBOM) lists every package in an image; generate one at build time and attach it to the image — it enables fast vulnerability triage when new CVEs are published.
# syft: generate SBOM from an image
syft myapp:latest -o spdx-json > sbom.spdx.json # SPDX format
syft myapp:latest -o cyclonedx-json > sbom.cdx.json # CycloneDX format
# Attach SBOM to the image in the registry (using ORAS or Cosign)
cosign attach sbom --sbom sbom.spdx.json myapp:latest
# Download and inspect SBOM from a remote image
cosign download sbom myapp:latest | jq .packages[].name | sort | head -30Image Signing with Cosign
Sign images after scanning and pushing; the signature is stored as an OCI artifact alongside the image, and can be verified in-cluster using Kyverno or Gatekeeper to prevent unsigned images from running.
# Generate a signing key pair (store private key in a secret manager, not Git)
cosign generate-key-pair
# Sign an image after pushing (uses cosign.key)
cosign sign --key cosign.key myregistry.io/myapp@sha256:<digest>
# Keyless signing with OIDC (recommended for CI — uses Sigstore Fulcio CA)
cosign sign --yes myregistry.io/myapp@sha256:<digest> # in CI with OIDC token
# Verify signature
cosign verify --key cosign.pub myregistry.io/myapp:latest
cosign verify --certificate-identity-regexp=".*github.com/myorg.*" \
--certificate-oidc-issuer=https://token.actions.githubusercontent.com \
myregistry.io/myapp:latest
# Kyverno policy to enforce signed images
kubectl apply -f - <<EOF
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-signed-images
spec:
validationFailureAction: Enforce
rules:
- name: check-image-signature
match:
any:
- resources: {kinds: [Pod]}
verifyImages:
- imageReferences: ["myregistry.io/myapp*"]
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
<paste cosign.pub contents here>
-----END PUBLIC KEY-----
EOFPrivate Registry Best Practices
Use a private registry with pull-through caching for public images; this eliminates rate-limiting, provides an audit trail, and lets you block upstream CVEs before they reach the cluster.
- Pull-through cache for Docker Hub, gcr.io, quay.io — nodes pull from your registry, never directly from the internet.
- Immutable tags — configure ECR, Artifact Registry, or Harbor to prevent tag overwrites. Always deploy by digest in production.
- Lifecycle policies — auto-delete images older than 90 days and untagged images to control storage costs.
- Network policy for egress — nodes should only be able to reach your private registry, not arbitrary image registries.