K8s Security Pro
kubernetes security supply-chain container-scanning cosign slsa

Kubernetes Supply Chain Security: From Image Scanning to SLSA

Secure your Kubernetes supply chain with image scanning, Cosign signing, admission control, SBOM generation, and SLSA framework compliance.

K8s Security Pro Team | | 13 min read

Kubernetes Supply Chain Security: From Image Scanning to SLSA

Supply chain attacks are among the most devastating threats to Kubernetes environments. When an attacker compromises a container image, a base layer, or a CI/CD pipeline, they gain code execution inside your cluster without ever exploiting a vulnerability in your application. The SolarWinds attack, the Codecov breach, and the event-stream npm compromise all demonstrated that attackers increasingly target the supply chain rather than the application itself.

In Kubernetes, the supply chain spans everything from the base images you pull from public registries to the Helm charts you deploy from third-party repositories. Securing it requires controls at every stage: build, store, deploy, and runtime.

Introduction: Supply Chain Attacks in Kubernetes

A supply chain attack in Kubernetes typically follows one of these patterns:

  1. Compromised base image — An attacker injects malware into a popular base image on Docker Hub. Every image built on top of it inherits the compromise.
  2. Typosquatting — An attacker publishes ngingx (note the typo) on a public registry. Developers who mistype the image name pull the malicious version.
  3. Tag mutability — An attacker overwrites the v1.0 tag on a registry with a compromised image. Pods pulling image:v1.0 get the malicious version without the tag changing.
  4. CI/CD pipeline compromise — An attacker gains access to the build pipeline and injects malicious code during the build process.
  5. Dependency confusion — An attacker publishes a public package with the same name as an internal one, and the build system pulls the public (malicious) version.

The fundamental problem: Kubernetes trusts whatever image you tell it to pull. There is no built-in verification that the image was built by you, hasn’t been tampered with, or is free of known vulnerabilities.

Image Scanning with Trivy

Trivy (by Aqua Security) is the most widely used open-source vulnerability scanner for container images. It scans for known CVEs in OS packages and application dependencies.

CLI Scanning

# Scan an image for vulnerabilities
trivy image nginx:1.25

# Fail on critical vulnerabilities (for CI/CD)
trivy image --exit-code 1 --severity CRITICAL my-registry/my-app:v1.2.3

# Scan with both vulnerability and misconfiguration checks
trivy image --scanners vuln,misconfig my-registry/my-app:v1.2.3

# Output as JSON for processing
trivy image --format json --output scan-results.json my-registry/my-app:v1.2.3

CI/CD Integration

Add Trivy to your pipeline to block images with critical vulnerabilities:

GitHub Actions:

- name: Scan container image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: '${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}'
    format: 'sarif'
    output: 'trivy-results.sarif'
    severity: 'CRITICAL,HIGH'
    exit-code: '1'

GitLab CI:

container_scanning:
  stage: test
  image:
    name: aquasec/trivy:latest
    entrypoint: [""]
  script:
    - trivy image --exit-code 1 --severity CRITICAL ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHA}

Runtime Scanning with Trivy Operator

For continuous scanning of running workloads (catching newly disclosed CVEs):

helm install trivy-operator aquasecurity/trivy-operator \
  --namespace trivy-system --create-namespace \
  --set operator.scanJobsConcurrentLimit=3

# Check results
kubectl get vulnerabilityreports -A
kubectl get vulnerabilityreports -n production -o json | jq '.items[] | {name: .metadata.name, critical: .report.summary.criticalCount, high: .report.summary.highCount}'

Image Signing with Cosign and Sigstore

Scanning tells you if an image has known vulnerabilities. Signing tells you if the image was built by you and hasn’t been tampered with. Both are essential.

Cosign (part of the Sigstore project) provides keyless and key-based container image signing.

Signing Images

# Generate a key pair
cosign generate-key-pair

# Sign an image
cosign sign --key cosign.key my-registry/my-app:v1.2.3

# Sign with keyless mode (uses OIDC identity, no keys to manage)
cosign sign my-registry/my-app:v1.2.3

Verifying Signatures

# Verify with a public key
cosign verify --key cosign.pub my-registry/my-app:v1.2.3

# Verify keyless signature (checks against Sigstore transparency log)
cosign verify my-registry/my-app:v1.2.3

CI/CD Integration

Sign images as part of your build pipeline:

# GitHub Actions
- name: Sign container image
  run: cosign sign --key env://COSIGN_PRIVATE_KEY ${REGISTRY}/${IMAGE_NAME}:${GITHUB_SHA}
  env:
    COSIGN_PRIVATE_KEY: ${{ secrets.COSIGN_PRIVATE_KEY }}
    COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}

Admission Control: Blocking Untrusted Images

Scanning and signing are only useful if you enforce them. Admission controllers act as the final gate, rejecting any image that doesn’t meet your standards.

Disallow Latest Tag

The :latest tag is mutable — it can point to different images at different times. This breaks reproducibility and makes it impossible to audit which image is actually running.

Using Kyverno:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: disallow-latest-tag
spec:
  validationFailureAction: enforce
  rules:
    - name: validate-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Using ':latest' tag is not allowed. Specify a version."
        pattern:
          spec:
            containers:
              - image: "!*:latest"
    - name: validate-init-container-image-tag
      match:
        any:
          - resources:
              kinds:
                - Pod
      validate:
        message: "Using ':latest' tag is not allowed on initContainers."
        pattern:
          spec:
            =(initContainers):
              - image: "!*:latest"

This checks regular containers, init containers, and ephemeral containers. Any pod using :latest is rejected at admission time.

Require Image Digest

For even stronger guarantees, require SHA256 digests instead of tags:

# Instead of: nginx:1.25
# Use: nginx@sha256:abc123...

# Get the digest for a tag
crane digest nginx:1.25

Verify Image Signatures at Admission

Kyverno can verify Cosign signatures before allowing an image to run:

apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: verify-image-signatures
spec:
  validationFailureAction: enforce
  rules:
    - name: verify-cosign-signature
      match:
        any:
          - resources:
              kinds:
                - Pod
      verifyImages:
        - imageReferences:
            - "my-registry/*"
          attestors:
            - entries:
                - keys:
                    publicKeys: |-
                      -----BEGIN PUBLIC KEY-----
                      MFkwEwYHKoZIzj0CAQY...
                      -----END PUBLIC KEY-----

Any image from my-registry/ that isn’t signed with the corresponding private key is blocked.

OPA/Gatekeeper Policies for Supply Chain

OPA/Gatekeeper provides equivalent controls using Rego policies.

Require Resource Limits

A compromised container without resource limits can consume all CPU for crypto mining or fill all memory to cause OOM cascades:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8srequireresourcelimits
spec:
  crd:
    spec:
      names:
        kind: K8sRequireResourceLimits
      validation:
        openAPIV3Schema:
          type: object
          properties:
            resources:
              type: array
              items:
                type: string
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8srequireresourcelimits

        violation[{"msg": msg}] {
          container := input_containers[_]
          required := input.parameters.resources[_]
          not has_resource_limit(container, required)
          msg := sprintf(
            "Container '%v' does not have a resource limit for '%v'.",
            [container.name, required]
          )
        }

        has_resource_limit(container, resource) {
          container.resources.limits[resource]
        }

        input_containers[c] { c := input.review.object.spec.containers[_] }
        input_containers[c] { c := input.review.object.spec.initContainers[_] }

Disallow Privileged Containers

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sdisallowprivilegedcontainers
spec:
  crd:
    spec:
      names:
        kind: K8sDisallowPrivilegedContainers
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sdisallowprivilegedcontainers

        violation[{"msg": msg}] {
          container := input_containers[_]
          container.securityContext.privileged == true
          msg := sprintf(
            "Container '%v' has privileged: true. Remove it.",
            [container.name]
          )
        }

        input_containers[c] { c := input.review.object.spec.containers[_] }
        input_containers[c] { c := input.review.object.spec.initContainers[_] }

Kyverno Policy Bundle for Supply Chain

Kyverno provides a more accessible alternative to OPA/Gatekeeper for teams that prefer YAML over Rego. A comprehensive policy bundle covers:

  1. require-resource-limits — Block pods without CPU/memory limits
  2. require-run-as-nonroot — Block pods running as root
  3. require-read-only-root-filesystem — Audit pods with writable root FS
  4. disallow-host-namespaces — Block pods sharing host PID/IPC/network
  5. require-labels — Audit pods without standard labels
  6. disallow-privilege-escalation — Block privileged containers
  7. add-default-security-context — Mutate pods to add security defaults

The mutate policy is particularly valuable for supply chain security — it acts as a safety net by automatically injecting runAsNonRoot: true and seccompProfile: RuntimeDefault on pods that don’t already define a security context.

SBOM Generation and Management

A Software Bill of Materials (SBOM) lists every component in your container image — OS packages, application dependencies, and their versions. SBOMs are increasingly required by regulations (US Executive Order 14028) and enterprise customers.

Generate SBOMs with Syft

# Generate SBOM for a container image
syft my-registry/my-app:v1.2.3 -o spdx-json > sbom.spdx.json

# Generate in CycloneDX format
syft my-registry/my-app:v1.2.3 -o cyclonedx-json > sbom.cdx.json

# Attach SBOM to image as an OCI artifact (using Cosign)
cosign attach sbom --sbom sbom.spdx.json my-registry/my-app:v1.2.3

Scan SBOMs for Vulnerabilities

# Scan an existing SBOM with Grype
grype sbom:sbom.spdx.json

# Or scan with Trivy
trivy sbom sbom.spdx.json

SBOMs enable rapid response when a new CVE is disclosed — instead of scanning every image, you search your SBOM database for the affected package.

CI/CD Integration: Shift-Left Security

The earlier you catch supply chain issues, the cheaper they are to fix. Integrate security at every stage:

Pre-Commit

# Scan Kubernetes manifests for misconfigurations
kubescape scan *.yaml

# Validate Kyverno policies locally
kyverno apply policies/ --resource manifests/

Build Stage

# 1. Build image
# 2. Scan for vulnerabilities (fail on CRITICAL)
# 3. Sign the image
# 4. Generate SBOM
# 5. Attach SBOM to image
# 6. Push to registry

Deploy Stage

# Admission controllers verify:
# 1. Image is signed
# 2. Image has no critical CVEs
# 3. Image uses a specific tag (not :latest)
# 4. Pod has security context
# 5. Pod has resource limits

Runtime

# Trivy Operator: continuous vulnerability scanning
# Falco: runtime behavior monitoring
# kube-bench: periodic CIS Benchmark checks

SLSA Framework and Supply Chain Levels

SLSA (Supply Chain Levels for Software Artifacts, pronounced “salsa”) is a framework by Google for progressively securing the software supply chain. It defines four levels:

LevelNameRequirementsProtects Against
SLSA 1Build existsAutomated build process, provenance generatedAd-hoc builds, no audit trail
SLSA 2Hosted buildBuild runs on a hosted service, signed provenanceCompromised developer machines
SLSA 3Hardened buildIsolated build environment, non-falsifiable provenanceCompromised build infrastructure
SLSA 4Two-party reviewAll code changes reviewed by two independent parties, hermetic buildInsider threats

Implementing SLSA in Kubernetes

SLSA 1: Use GitHub Actions, GitLab CI, or similar to build images automatically. Generate provenance metadata:

# Using SLSA GitHub Generator
# Generates a signed SLSA provenance attestation
- uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1

SLSA 2: Run builds on hosted runners (GitHub-hosted, GitLab SaaS). Sign provenance with Sigstore:

cosign attest --predicate provenance.json --key cosign.key my-registry/my-app:v1.2.3

SLSA 3: Use isolated build environments (ephemeral runners, no persistent state). Verify build logs are tamper-proof.

SLSA 4: Require two approvals for all code changes. Use hermetic builds (no network access during build, all dependencies vendored).

Verifying Provenance at Admission

# Verify SLSA provenance with Cosign
cosign verify-attestation --type slsaprovenance --key cosign.pub my-registry/my-app:v1.2.3

Kyverno can verify attestations at admission time, ensuring only images with valid build provenance are deployed to your cluster.

Practical Recommendations

Here’s a prioritized approach to securing your Kubernetes supply chain:

Start here (week 1):

  1. Add Trivy scanning to your CI/CD pipeline, fail on CRITICAL
  2. Deploy the “disallow latest tag” Kyverno policy
  3. Pin image versions in all manifests (use tags, move to digests later)

Add next (week 2-3): 4. Sign images with Cosign in your CI/CD pipeline 5. Deploy Trivy Operator for runtime scanning 6. Add resource limits enforcement via Kyverno/OPA

Mature (month 2+): 7. Verify image signatures at admission 8. Generate and store SBOMs for all production images 9. Implement SLSA Level 2+ provenance 10. Use private registries for all production images

Each step builds on the previous one. Scanning without enforcement catches issues but doesn’t prevent them. Signing without verification at admission is security theater. Build incrementally and enforce at each layer.

Conclusion

Supply chain security in Kubernetes requires defense at every stage — build, store, deploy, and runtime. No single tool or policy is sufficient. The combination of image scanning (Trivy), image signing (Cosign), admission control (Kyverno/OPA), and runtime monitoring (Falco, Trivy Operator) creates a layered defense that catches threats regardless of where they originate.

The templates and policies referenced in this guide — Kyverno disallow-latest-tag, Kyverno policy bundle, OPA/Gatekeeper ConstraintTemplates, and Falco runtime rules — are all included in the K8s Security Pro template pack. Get started with the free K8s Security Quick-Start Kit which includes the checklist and essential security templates.


Implement what you’ve learned with these production-ready YAML templates:

Get the Free K8s Security Quick-Start Kit

Join 500+ engineers. Get 5 essential templates + audit checklist highlights delivered to your inbox.

No spam. Unsubscribe anytime.

Secure Your Kubernetes Clusters

Get the complete 50-point audit checklist and 20+ production-ready YAML templates.

View Pricing Plans