TL;DR

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.

bashscanning.sh
# 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.

bashsbom.sh
# 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 -30

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

bashcosign.sh
# 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-----
EOF

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