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.
Two gates between a commit and the cluster
- 01git push
- 02Trivy + Safety
- 03Registry
- 04Kubernetes API
- 05Gatekeeper decision
What gets refused
| CONTROL | LAYER | ACTION |
|---|---|---|
| Image with a CRITICAL or HIGH CVE | CI · Trivy | BLOCK |
| Vulnerable Python dependency | CI · Safety | BLOCK |
| Image from an unapproved registry | Admission · Rego | DENY |
| Image using the latest tag | Admission · Rego | DENY |
| Pod running as root | Admission · Rego | DENY |
| Writable root filesystem, extra capabilities | Manifest | HARDENED |
Straight from the code
Excerpts from the actual repository and deliverables, trimmed for length.
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.
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: falseThe CI gate: Trivy exits with code 1 on any CRITICAL or HIGH finding, so the push stage never runs.
Results
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.