Skip to content
mouaad.sek:~$
All case files~/work/cf-02$
CF-02DevSecOpsSTATUS: CONTAINED

Supply chain security on Kubernetes

A vulnerable image never reaches the registry, and a non-compliant pod never reaches the cluster. Two gates: one in CI, one at admission.

ROLE
Solo, personal lab
WHEN
M1, 2026
STACK
GitLab CI, Docker, Trivy, Safety, Kubernetes (kind), OPA Gatekeeper, Rego
01 / CONTEXT

The problem

Most container attacks do not start in production. They start with a vulnerable base image, an outdated dependency, or a pod that runs as root because nobody said no.

The goal: make the insecure path impossible by default, in the pipeline and in the cluster, without relying on developers to remember the rules.

02 / APPROACH

Two gates between a commit and the cluster

  1. 01git push
  2. 02Trivy + Safety
  3. 03Registry
  4. 04Kubernetes API
  5. 05Gatekeeper decision
03 / WHAT GETS REFUSED

What gets refused

CONTROLLAYERACTION
Image with a CRITICAL or HIGH CVECI · TrivyBLOCK
Vulnerable Python dependencyCI · SafetyBLOCK
Image from an unapproved registryAdmission · RegoDENY
Image using the latest tagAdmission · RegoDENY
Pod running as rootAdmission · RegoDENY
Writable root filesystem, extra capabilitiesManifestHARDENED
04 / EVIDENCE

Straight from the code

Excerpts from the actual repository and deliverables, trimmed for length.

gatekeeper/templates/no-latest-template.yamlyaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8snolatestimage
spec:
  crd:
    spec:
      names:
        kind: K8sNoLatestImage
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8snolatestimage
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          endswith(container.image, ":latest")
          msg := sprintf("BLOQUE : image '%v' utilise le tag latest.", [container.image])
        }
        violation[{"msg": msg}] {
          container := input.review.object.spec.containers[_]
          not contains(container.image, ":")
          msg := sprintf("BLOQUE : image '%v' n'a pas de tag.", [container.image])
        }

Admission policy in Rego: any pod using the latest tag, or no tag at all, is rejected by the Kubernetes API.

.gitlab-ci.ymlyaml
variables:
  IMAGE_TAG: "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA"
  TRIVY_SEVERITY: "CRITICAL,HIGH"

stages: [build, scan, push]

trivy-image-scan:
  stage: scan
  script:
    - trivy image --input image.tar --severity "$TRIVY_SEVERITY" --exit-code 1 --format table
    - trivy image --input image.tar --format json --output trivy-report.json
  allow_failure: false

safety-scan:
  stage: scan
  script:
    - pip install safety
    - safety check -r app/requirements.txt --full-report --output text
  allow_failure: false

The CI gate: Trivy exits with code 1 on any CRITICAL or HIGH finding, so the push stage never runs.

05 / OUTCOME

Results

2enforcement gates, CI and admission
3Gatekeeper policies written in Rego
0images pushed when a scan fails
06 / LESSONS

What I'd do differently

Gatekeeper checks where an image comes from, not whether my pipeline really built it. Next step: sign images with Cosign in CI, verify signatures at admission, and generate an SBOM for every build.

NEXT CASE FILE · CF-03JobPilot
Esc

↑↓ navigate · Enter open · Esc close