Kubernetes Supply Chain Security
In 2020, the SolarWinds attack compromised thousands of organizations by injecting malicious code into a software update. In 2021, a single developer controlled a npm package used by millions of projects. Software supply chain attacks are not theoretical — they target the build pipeline, not the running application. This guide covers the tools and patterns Kubernetes practitioners use to verify everything that runs in the cluster came from a trusted, auditable source.
Supply Chain Threat Model
The attack surface spans from source code to the running pod:
SLSA Levels
SLSA (Supply-chain Levels for Software Artifacts) is a security framework that defines four progressive levels of supply chain integrity, from basic (level 1) to hermetic, verified builds (level 4).
| Level | Requirements | What it gives you |
|---|---|---|
| SLSA 1 | Build provenance generated and published (unsigned). | Audit trail. Know what was built from what. |
| SLSA 2 | Signed provenance. Hosted build service (GitHub Actions, Cloud Build). | Tamper detection. Provenance can be verified. |
| SLSA 3 | Hardened build service. Isolated builds. No persistent credentials on build runner. | Prevents build environment compromise from injecting malicious artifacts. |
| SLSA 4 | Hermetic builds. Two-party review. Verifiable reproducibility. | Maximum assurance. Can independently reproduce the exact artifact. |
SLSA 3 and 4 require significant CI infrastructure changes. For most teams, reaching SLSA 2 (signed provenance from a hosted CI service like GitHub Actions) provides substantial protection with manageable effort. GitHub Actions natively supports SLSA provenance generation via the slsa-framework/slsa-github-generator action.
SBOMs
A Software Bill of Materials (SBOM) is a machine-readable inventory of every component in an artifact — OS packages, language dependencies, transitive dependencies. When a vulnerability like Log4Shell is disclosed, an SBOM lets you instantly answer "which of my images contains log4j?"
# Generate SBOM in SPDX format
syft myapp:1.2.3 -o spdx-json > sbom.spdx.json
# Generate SBOM in CycloneDX format
syft myapp:1.2.3 -o cyclonedx-json > sbom.cdx.json
# Attach SBOM to the image in the registry (as OCI artifact)
cosign attach sbom --sbom sbom.spdx.json myapp@sha256:abc123...
# Scan an SBOM for vulnerabilities
grype sbom:./sbom.spdx.json
# Query the SBOM for a specific package
cat sbom.spdx.json | jq '.packages[] | select(.name == "log4j")'
Sigstore & Provenance
Sigstore is a set of tools for signing software artifacts and recording the signatures in a public, append-only transparency log (Rekor). Cosign handles image signing; Fulcio provides keyless certificate issuance tied to OIDC identities.
# .github/workflows/build.yml
jobs:
build:
permissions:
id-token: write # required for keyless signing
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Build and push
id: build
run: |
docker build -t ghcr.io/myorg/myapp:${{ github.sha }} .
docker push ghcr.io/myorg/myapp:${{ github.sha }}
echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' ghcr.io/myorg/myapp:${{ github.sha }})" >> $GITHUB_OUTPUT
- name: Sign image (keyless — Sigstore)
run: |
cosign sign ghcr.io/myorg/myapp@${{ steps.build.outputs.digest }}
- name: Generate SLSA provenance
uses: slsa-framework/slsa-github-generator/.github/workflows/generator_container_slsa3.yml@v1.9.0
with:
image: ghcr.io/myorg/myapp
digest: ${{ steps.build.outputs.digest }}
Policy Admission
Admission controllers run as webhooks and evaluate every create/update request to the API server. Policy engines let you write rules in a policy language (Kyverno uses YAML; OPA uses Rego) rather than building a custom webhook server.
| Tool | Language | Strengths |
|---|---|---|
| Kyverno | YAML/CEL | Kubernetes-native policies. Easy to read. Mutation + generation built in. No separate language to learn. |
| OPA Gatekeeper | Rego | Powerful Rego language. Better for complex logic. Widely adopted in enterprises. |
| Sigstore Policy Controller | YAML | Focused on image signature verification. Simple setup for cosign-based workflows. |
Kyverno Policies
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: require-image-digest
spec:
validationFailureAction: Enforce
rules:
- name: check-image-digest
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "Images must be pinned to a digest (@sha256:...)."
foreach:
- list: "request.object.spec.containers"
deny:
conditions:
any:
- key: "{{ element.image }}"
operator: NotContains
value: "@sha256:"
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-slsa-provenance
spec:
validationFailureAction: Enforce
rules:
- name: check-slsa-provenance
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "ghcr.io/myorg/*:*"
attestations:
- type: https://slsa.dev/provenance/v0.2
attestors:
- count: 1
entries:
- keyless:
subject: "https://github.com/myorg/myapp/.github/workflows/build.yml@refs/heads/main"
issuer: "https://token.actions.githubusercontent.com"
conditions:
- all:
- key: "{{ buildType }}"
operator: Equals
value: "https://github.com/slsa-framework/slsa-github-generator/container@v1"
OPA Gatekeeper
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sbanlatestimage
spec:
crd:
spec:
names:
kind: K8sBanLatestImage
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sbanlatestimage
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Image %v uses :latest tag — pin to a specific version", [container.image])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, ":")
msg := sprintf("Image %v has no tag — pin to a specific version", [container.image])
}
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBanLatestImage
metadata:
name: ban-latest-image
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces: ["kube-system"]
CI/CD Security Gates
Defense-in-depth means catching issues as early as possible. A practical CI pipeline for supply chain security:
- Dependency scanning — run
trivy fs .orsnyk testagainst the source tree to catch vulnerable dependencies before building. - Build from a pinned, verified base image — pin
FROM debian:12@sha256:abc...and verify the base image signature in CI. - Generate SBOM during build — attach to the image with
cosign attach sbom. - Scan the built image —
trivy image --exit-code 1 --severity CRITICAL myapp:sha. - Sign the image and generate SLSA provenance — keyless via GitHub Actions OIDC.
- Admission verification at deploy — Kyverno or Policy Controller verifies the signature and provenance attestation before the pod runs.
Admission controllers can be bypassed if a cluster admin has broad permissions. They are a gate, not a moat. Combined with image signing and SBOM attestations that trace back to an auditable CI run, you create a chain of custody — not just a yes/no gate.