Skip to content
Français
Account: not signed in, sync available My learning
Select theme
Français

Kubernetes supply chain security: securing the delivery chain

By updated

Improve this page

Supply chain security accounts for 20% of the CKS and covers reducing the image footprint, understanding the supply chain, securing artifacts and the static analysis of workloads and images. This guide focuses on five concrete practices: minimal images, vulnerability scanning, SBOM, image signing and admission control.

These five practices cover the foundation, but they are only worth something once inserted into a wider chain: continuous integration, artifact storage and admission controls.

Supply chain security is the broadest domain of the CKS, with 20% of the exam. One point of method matters for studying well: the official curriculum describes skills, reducing the image footprint, securing artifacts, analysing statically, controlling admission, and imposes no tool. This guide therefore follows that structure by objective, and the tools appearing in it are interchangeable means.

Prerequisites

  • A working Kubernetes cluster with cluster-admin access
  • Docker and container basics (building images)
  • kubectl configured
  • Security notions (CVE, vulnerabilities)

What is an image's supply chain?

The supply chain of a container image covers its whole lifecycle before deployment:

Supply chain of a container image: source code, build, image, registry, then cluster, each stage with its own security control: dependencies, artifacts, scan, signature and verification.Source codeBuildImageRegistryClusterDependenciesArtifactsScanSignatureVerification
Source · SVG
Esc to close, + and − to zoom, scroll to move around. 100 %

An attack on the supply chain compromises any link: a malicious dependency, a hijacked build, an image modified in transit, or an unauthenticated registry.

Golden rule: never trust an image just because it "works". An image that works can still contain critical CVEs or have been tampered with.

1. Minimising the base image footprint

This is the first step because it is the only one that removes risk instead of adding detection. Every measure that follows, signing, analysis, SBOM, deals with what the image contains: reducing that content reduces just as much what they have to cover.

The gain is immediate: a package absent from the image generates no vulnerability alert, needs no update, and gives no lever to anyone who manages to run code in the container.

Why lightweight images reduce the attack surface

An image shipping apt, bash, curl and system tools offers an attacker as many levers if they manage to run inside the container. A distroless image contains only the runtime and your binary.

Image typeSizeAttack surfaceUse case
ubuntu:22.04~70 MBHigh (shell, apt, utils)Development
alpine:3.19~7 MBModerate (BusyBox shell)General use
gcr.io/distroless/static~2 MBMinimal (no shell)Go, static binaries
gcr.io/distroless/base~20 MBVery low (glibc only)C, C++, Java
scratch0 MBMinimal on the base image side (security still depends on the shipped binary)Fully static binaries

Multi-stage build: compiling without shipping the tools

A multi-stage build lets you compile in a full "build" image and copy only the final binary into a minimal image:

Dockerfile
# Stage 1: compilation (full image)
FROM golang:1.23@sha256:60deed95d3888cc5e4d9ff8a10c54e5edc008c6ae3fba6187be6fb592e19e8c0 AS builder
WORKDIR /app
COPY . .
RUN CGO_ENABLED=0 go build -o myapp .
# Stage 2: minimal final image (distroless)
FROM gcr.io/distroless/static-debian12:nonroot@sha256:afa5c872c891853ca7fcf1f12c3edb23f7eeef36189728842dd51042ff57f7ab
COPY --from=builder /app/myapp /myapp
USER nonroot:nonroot
ENTRYPOINT ["/myapp"]

Image good practices

These four rules address the same question: will the image you deploy today be identical to the one you tested yesterday? A tag is a mutable pointer on the registry side, reassignable without anything changing in your manifest; a digest is the hash of the content, and it cannot lie. Hence the hierarchy: latest guarantees nothing, a version tag guarantees an intent, a digest guarantees the content.

  • Freeze the version: nginx:1.25.3 rather than nginx:latest, an update can introduce CVEs
  • Add the digest to guarantee immutability, without removing the tag: nginx:1.25.3@sha256:abc123.... The tag stays readable for humans, the digest commits the content
  • Do not install debuggers in production images (gdb, strace, tcpdump)
  • Clean the cache in the same RUN: apt-get install -y curl && rm -rf /var/lib/apt/lists/*
# Recommended: tag AND digest, readable and immutable
FROM nginx:1.31.3@sha256:8541484afbc9c8a5a8a99b379568ebbc957f658583ec9448fc43104229c03cf8
# ❌ to ban: latest is not reproducible
FROM nginx:latest

The # ❌ marker is not decorative. The site's pinning tooling rewrites image references, and only stops at this marker: without it, a latest counter-example gets "fixed" into a pinned image, and the demonstration disappears silently.

2. Scanning for vulnerabilities

Before anything else: scan your images to detect known CVEs (Common Vulnerabilities and Exposures).

The March 2026 Trivy incident, and what it changes here

In March 2026, the Trivy ecosystem suffered a multi-wave compromise: publication of a malicious trivy v0.69.4, compromise of the trivy-action tags, exposure of setup-trivy, then malicious Docker Hub images v0.69.5 and v0.69.6. Aqua recommended treating as compromised every secret reachable by the affected pipelines.

The episode illustrates exactly what this chapter teaches: a supply chain security tool is part of that chain, and gets compromised like any other link. It also justifies the position taken here. The Trivy commands stay in this guide, because they help to read existing pipelines and they are everywhere in exam preparation material; but the recommended path for a new deployment goes through another scanner.

Scanning with Grype, my main choice

I favour it for its integration with Syft, for its prioritisation of real risk, backed by EPSS, KEV and OpenVEX, and for its ability to scan images, filesystems and SBOMs alike.

Terminal window
# Installation from the verified official binary (SHA256 checksum)
GRYPE_VERSION=0.111.0
base="https://github.com/anchore/grype/releases/download/v${GRYPE_VERSION}"
curl -sSfLO "${base}/grype_${GRYPE_VERSION}_linux_amd64.tar.gz"
curl -sSfLO "${base}/grype_${GRYPE_VERSION}_checksums.txt"
grep "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" "grype_${GRYPE_VERSION}_checksums.txt" | sha256sum -c -
sudo tar -C /usr/local/bin -xzf "grype_${GRYPE_VERSION}_linux_amd64.tar.gz" grype
# Scan an image
grype nginx:1.25.3
# Scan with a severity threshold (fail if CRITICAL found)
grype nginx:1.25.3 --fail-on critical
# JSON format for CI/CD
grype nginx:1.25.3 -o json > results.json

Scanning with Trivy, ecosystem culture

You will meet these commands in almost every existing pipeline and CKS preparation material, which is enough to justify knowing them. Two options deserve an explanation. --ignore-unfixed hides CVEs without a published fix: useful not to block a build on a problem nobody can solve, dangerous if you enable it without tracking those vulnerabilities elsewhere. --exit-code 1, for its part, turns the scan into a guardrail: without this option, Trivy prints the CVEs and the job stays green.

Terminal window
# Scan an image
trivy image nginx:1.25.3
# Ignore unfixed CVEs
trivy image --ignore-unfixed nginx:1.25.3
# Format for CI/CD
trivy image --exit-code 1 --severity CRITICAL nginx:1.25.3

A useful precision if you study from several sources: you will see Trivy almost everywhere, and might conclude it is part of the curriculum. It is not. The official curriculum describes the skill, analysing an image and deciding on the result, without ever naming a tool. Knowing how to read Trivy output stays useful; making it the only scanner you know would be a study mistake.

The limit to keep in mind before moving on: an image without known CVEs is not a trustworthy image. It may have been tampered with, come from a compromised source, or carry malicious code no database references, the Trivy incident being the direct demonstration. The scan answers a precise question, "which known flaws does this image contain", and it replaces neither signing, which establishes integrity, nor provenance, which establishes origin, nor the admission policy, which decides what enters the cluster.

To go deeper into scanning and CVE scores: Understanding CVE, CVSS and EPSS (in French).

3. Generating and using an SBOM

An SBOM (Software Bill of Materials) is the exhaustive inventory of an image's components: system packages, libraries, versions and licences. It is the ingredient list of your software.

Terminal window
# Generate an SBOM with Syft
syft nginx:1.25.3 -o cyclonedx-json > nginx-sbom.json
# Use the SBOM to scan with Grype
grype sbom:nginx-sbom.json
# Generate and scan in one command
syft nginx:1.25.3 | grype

See the full SBOM guide for the SPDX and CycloneDX formats and CI/CD integration.

4. Signing and verifying images with Cosign

Image signing guarantees authenticity (the image really comes from the expected build) and integrity (it has not been modified). Sigstore's Cosign is the standard tool.

Signing an image

A supply chain guide that installed its own tool without verification would contradict itself: the block below pins a precise version and checks the SHA256 checksum published by the project before making the binary executable. Sigstore publishes this file as cosign_checksums.txt in every release. The cosign generate-key-pair command then asks for a passphrase: it protects cosign.key, the one thing never to let out of your vault.

Terminal window
# Cosign installation: pinned version + checksum verification
COSIGN_VERSION=3.1.3
base="https://github.com/sigstore/cosign/releases/download/v${COSIGN_VERSION}"
curl -sSfLO "${base}/cosign-linux-amd64"
curl -sSfLO "${base}/cosign_checksums.txt"
grep " cosign-linux-amd64$" cosign_checksums.txt | sha256sum -c -
chmod +x cosign-linux-amd64 && sudo mv cosign-linux-amd64 /usr/local/bin/cosign
# Generate a key pair
cosign generate-key-pair
# Produces cosign.key (private) and cosign.pub (public)
# Sign by digest (recommended: the tag can be reassigned)
cosign sign --key cosign.key registry.example.com/myapp@sha256:abc123...
# Sign by tag (acceptable but less robust)
cosign sign --key cosign.key registry.example.com/myapp:1.0.0
# Verify the signature
cosign verify --key cosign.pub registry.example.com/myapp:1.0.0

Checking that the signature really protects

A signature that verifies proves nothing on its own: you need to see the verification fail when it should fail. The three cases below were run on a local registry with cosign 3.1.3, and they return three different exit codes.

With the right key, cosign details what it checked:

Output
Verification for localhost:5555/lab-signe:1.0.0 --
The following checks were performed on each of these signatures:
- The cosign claims were validated
- Existence of the claims in the transparency log was verified offline
- The signatures were verified against the specified public key
SituationExit codeMessage
Valid signature0the detail of the checks above
Wrong public key1no matching attestations: failed to verify log inclusion
Unsigned image10no signatures found

The distinction between 1 and 10 can be put to use: an unsigned image signals an omission in the build chain, a signature that does not match signals an identity problem, and these two incidents do not call for the same response.

Where the signature lives

It is not in the image: it is a separate artifact, pushed to the same repository under a tag derived from the digest. After signing, the repository therefore holds two tags:

Terminal window
curl -s http://localhost:5555/v2/lab-signe/tags/list
Output
{"name":"lab-signe","tags":["sha256-7c38f24774e3cbd906d2d33c38354ccf787635581c122965132c9bd309754d4a","1.0.0"]}

Two practical consequences. Copying an image from one registry to another does not carry its signature, unless you also copy that artifact: it is the most frequent cause of a failing verification after a registry migration. And since the tag is derived from the digest, re-signing a rebuilt image produces a new artifact, the old one staying in place.

Verification in Kubernetes with ImagePolicyWebhook

ImagePolicyWebhook is a historical admission controller, disabled by default, which delegates the authorisation decision to an external backend. It is still documented, and useful to understand image control mechanisms; but on recent clusters, you rather meet generic admission webhooks (ValidatingAdmissionWebhook) or policy engines such as Kyverno.

An architecture point that changes how to assess this mechanism: ImagePolicyWebhook decides nothing by itself. It forwards the request to an external service reachable over TLS, which allows or refuses. The real security of the setup is therefore that of this backend, its availability and the rules it applies, not that of the Kubernetes controller, which merely relays.

The setup takes two files. First the kube-apiserver flags, to add in /etc/kubernetes/manifests/kube-apiserver.yaml on every control plane node: the first enables the plugin, the second says where to read its configuration. A mistake here prevents the apiserver from restarting: keep a copy of the manifest before modifying it.

Flags to add to the kube-apiserver
--enable-admission-plugins=...,ImagePolicyWebhook
--admission-control-config-file=/etc/kubernetes/admission/admission-config.yaml
/etc/kubernetes/admission/admission-config.yaml
apiVersion: apiserver.config.k8s.io/v1
kind: AdmissionConfiguration
plugins:
- name: ImagePolicyWebhook
configuration:
imagePolicy:
kubeConfigFile: /etc/kubernetes/admission/webhook-kubeconfig.yaml
allowTTL: 50
denyTTL: 50
retryBackoff: 500
defaultAllow: false # Block if the webhook is unreachable

A word on the signing mode, since two exist and the second is not always presented. Key-based signing, the one this guide details, has the advantage of being explicit: you see what you generate and what you verify. It does leave you a long-lived private key to protect and rotate, which is precisely the stuff of supply chain incidents.

Keyless signing removes that problem by relying on an OIDC identity, that of your GitHub Actions or GitLab CI run, and on an ephemeral certificate. That is what to aim for in a modern chain. Keep key-based signing to understand the mechanisms, and for isolated environments where no OIDC provider is reachable. The Cosign guide details both.

5. Limiting the allowed registries

On top of signing, you can restrict Kubernetes to only accept images coming from approved registries. That is the modern, recommended way, through a policy engine you install, Kyverno or Gatekeeper, more flexible than ImagePolicyWebhook. Neither is part of Kubernetes. What is native, however, are CEL admission policies: ValidatingAdmissionPolicy, stable since 1.30, and MutatingAdmissionPolicy, stable and enabled by default since 1.36.

The policy below uses the policies.kyverno.io/v1 API, which appeared in Kyverno 1.17 and has been stable since 1.18. Two fields carry all the behaviour: validationActions: [Deny] refuses the creation instead of merely reporting, and the CEL expression walks every container of the Pod with all(...), which also blocks a Pod in which a single container comes from elsewhere. Note that matchConstraints only targets Pods here: a Deployment will pass, then its Pod will be refused, with an error message showing in the ReplicaSet and not in the Deployment.

apiVersion: policies.kyverno.io/v1
kind: ValidatingPolicy
metadata:
name: restrict-registries
spec:
validationActions: [Deny]
matchConstraints:
resourceRules:
- apiGroups: [""]
apiVersions: ["v1"]
operations: ["CREATE", "UPDATE"]
resources: ["pods"]
validations:
- expression: >-
object.spec.containers.all(c,
c.image.startsWith('registry.example.com/')
)
message: "Only images from registry.example.com are allowed."

With Gatekeeper (OPA)

Gatekeeper separates the policy code from its parameters. The manifest below is a Constraint, in other words an instance: it assumes that the K8sAllowedRepos ConstraintTemplate, provided by the official gatekeeper-library, has already been applied to the cluster. Without it, the API rejects the manifest as an unknown resource. The repos field works by string prefix, not by regular expression: an entry without a trailing slash would also allow a registry whose name starts the same way.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "registry.example.com/"
- "gcr.io/distroless/"

See the Kyverno guide and the Gatekeeper guide (in French).

6. Analysing manifests before applying them

Scanning an image looks for known vulnerabilities in packages. It says nothing about a manifest declaring privileged: true or mounting the node's root: the package can be perfectly up to date, the deployment stays dangerous. The CKS syllabus names the two reference tools, Kubesec and KubeLinter.

They do not do the same job. Showing it on a deliberately weak manifest, carrying privileged: true, runAsUser: 0, an image on latest and a hostPath on the root, is enough to understand which one to choose:

ToolWhat it returnsOn the weak manifestOnce hardened
kube-linter 0.8.3a list of named defects, with the remediation8 errors, exit 12 errors left
kubesec 2.14.2a numeric score and a verdict-30 points, exit 2+13, "Passed", exit 0
Terminal window
kube-linter lint deployment.yaml
kubesec scan deployment.yaml

Remember the difference in use: kube-linter lists, which helps fix; kubesec scores, which helps set a threshold in continuous integration.

Two neighbouring tools do the same work from another angle: kube-score (in French), designed for continuous integration, and Polaris (in French), which watches a live cluster continuously.

7. Verifying the platform binaries

The supply chain does not stop at images: the kubectl you install on an administration workstation is part of it. The CKS syllabus asks for it explicitly, "verify platform binaries before deploying".

Kubernetes publishes the checksum of each binary next to the binary:

Terminal window
VER=v1.37.0
curl -LO "https://dl.k8s.io/release/${VER}/bin/linux/amd64/kubectl"
curl -LO "https://dl.k8s.io/release/${VER}/bin/linux/amd64/kubectl.sha256"
echo "$(cat kubectl.sha256) kubectl" | sha256sum --check
Output
kubectl: OK

The exit code is what counts, not the message. Measured on the 1.37.0 binary: 0 when the file is intact, 1 as soon as it is tampered with, and a single byte added to a 61 MB binary is enough to flip it. In a script, test this code rather than looking for the word OK in the output.

Full supply chain workflow

In production and for the CKS, here is the recommended cycle, from the build to the admission of the image into the cluster:

  1. Build with a minimal base image (distroless or alpine) and a multi-stage build

  2. Scan the image before pushing: grype myapp:1.0.0 --fail-on high

  3. Generate the SBOM and attach it to the image: syft myapp:1.0.0 -o cyclonedx-json

  4. Pin by digest: reference the image by its SHA256 to guarantee immutability

  5. Sign by digest: cosign sign --key cosign.key registry/myapp@sha256:abc123...

  6. Push to a private, controlled artifact repository, an approved internal registry

  7. Verify at admission: Kyverno or Gatekeeper policy, or ImagePolicyWebhook

Troubleshooting

The four situations below share one trait: the cluster behaves exactly as it was configured, and it is the configuration that is the problem. The first two rows describe visible blocks; the last two are more insidious, because nothing fails: the pipeline stays green while the expected control never happened. Check those first if your chain seems "too" quiet.

SymptomLikely causeSolution
Pod blocked by ImagePolicyWebhookWebhook unreachable with defaultAllow: falseCheck the webhook's availability
Signature not verifiedWrong public key or re-tagged imageRe-sign or use the SHA256 digest
CRITICAL CVEs not blockingThreshold not configuredAdd --fail-on critical in the CI/CD
latest images in productionPermissive tag conventionKyverno policy forbidding latest

Key points

  • Minimal images: distroless > alpine > ubuntu, multi-stage build; scratch reduces the base footprint but security depends on the shipped binary
  • Scan before push: Grype (my production choice, EPSS, KEV, OpenVEX) or Trivy (ecosystem culture)
  • Scanning is not enough: an image without CVEs can still be malicious or compromised, complete it with signing and admission
  • SBOM: an exhaustive inventory with Syft, re-scannable separately without a rebuild
  • Signing: Cosign, preferably by digest and keyless in production
  • Admission control: Kyverno or Gatekeeper as the modern way; ImagePolicyWebhook as the historical mechanism
  • Registries: restrict to trusted sources, prefer an internal artifact repository
  • CI/CD: avoid installing tools on the fly from the Internet in production

Test your knowledge

The questions below only cover the supply chain domain. They deliberately stress the distinctions the exam exploits: scan versus signature, tag versus digest, and admission control versus CI control.

Knowledge check

Check what you have learned with this interactive quiz

8 questions
8 min
70% to pass

How it works

  • The timer starts when you click Start
  • Multiple choice, true/false and short answer questions
  • You can move between questions
  • Detailed results are shown at the end

Starts the quiz and the timer

Put it into practice

Three links of the chain, each validated by a refusal. You sign an image with cosign and prove that the same key refuses the other one, fix the five findings of a Dockerfile static analysis without changing what the image does, and pin a Deployment on the digest actually resolved by the runtime, not on a digest that merely looks like it.

Is this project useful to you?

The guides, courses, quizzes and labs stay free. If you use them regularly, you can contribute to their upkeep and development.

Become a supporting member (new tab) One-time support (new tab) Learn more

Follow the project on LinkedIn