Cybersecurity is often spoken of in terms of paved roads and guard rails. Paved roads are patterns that we define to do things in a secure way and, conversely, guard rails are limits we set to prevent our users from doing things the wrong way.
We often get questions about guard rails for consumers of Minimus’s minimal, low-CVE images. The paved roads are immediately obvious – use Minimus base images and spend far less time and energy resolving (or contesting) findings from vulnerability scans. What’s less obvious is how to set up appropriate guard rails to enforce the use of our base images in the enterprise.
For organizations who have standardized on Kubernetes, one approach is to use an Admission Controller to enforce policies such as “you must use Minimus base images in what you deploy”. An Admission Controller is a Kubernetes service that reviews all changes to the cluster and either approves/denies them (a Validating Admission Controller) or, potentially, modifies them (a Mutating Admission Controller).
Today, we’re going to use Kyverno, which is an Admission Controller that has become popular for its simple, flexible approach to defining policies in YAML within the configuration of the Kubernetes cluster itself. This approach enables organizations to manage policy in the same way, and with the same tools, that they manage Kubernetes deployments and configuration.
Depending on the scale of the clusters to be deployed on, there are a number of different approaches to deploying Kyverno. For this blog, we’re using a small, non-production Google GKE cluster so we’ve deployed using Minimus’s Kyverno Helm chart to take advantage of low-CVE images for our deployment.
While Kyverno can apply a wide variety of policies, we’re going to work with a relatively simple goal. We have a pipeline in GitHub that builds images and delivers them to Google Artifact Registry. Images are deployed from GAR to our GKE cluster in the `apps` namespace. Our policy requirement is that these images deployed to our cluster use a Minimus image as their base.
In order to define this policy, we need a reliable indicator of the base image. Because we are building OCI images in our pipeline, we can use the common "org.opencontainers.image.base.name" annotation to check the name of the base image used.
The full policy YAML is available at the end of this article but we’ll break down the important pieces one by one. First, we’ll create a rule: we’re choosing to ENFORCE this rule although, in practice, these should be audit-only until you’re sure the policy won’t break anything important.
spec:
validationFailureAction: Enforce
rules:
- name: allow-minimus-base
match:
any:
- resources:
kinds:
- Pod
namespaces:
- apps
Our allow-minimus-base rule applies to (or matches) any Pod in the namespace apps.
A Kubernetes Pod may have multiple containers in it, so we need to loop through them (using foreach) to ensure that all of the containers in this Pod comply with our policy.
We’re also using imageData, which requires Kyverno to pull a copy of the image to extract attributes to make a decision on. This is necessary to extract the annotations on the image to make decisions based on them; otherwise, the Admission Controller only has access to the configuration being sent to Kubernetes and not to the details of the image itself.
In the context, we define the imageData variable to point to the image itself, and then we define basename to extract the annotation we want to use.
validate:
message: >-
The base image for this container is not a Minimus base image.
Please contact the platform team for assistance.
foreach:
- list: "request.object.spec.[ephemeralContainers, initContainers, containers][]"
context:
- name: imageData
imageRegistry:
reference: "{{ element.image }}"
- name: basename
variable:
jmesPath: imageData.manifest.annotations."org.opencontainers.image.base.name"
default: ''
Now, we’re ready to define our deny rule so that Kyverno knows what to reject (and, by extension, what to accept) for a new Pod in the apps namespace.
Using the basename variable that we defined in the context, we test against a wildcard pattern for any image with a base image that comes from reg.mini.dev.
deny:
conditions:
any:
- key: "{{ basename }}"
operator: AnyNotIn
value: "reg.mini.dev/*"
Put everything together, kubectl apply our policy, and we’re ready to test.
Let’s start with our python-library image, which is built on library/python:3.13
from Docker Hub. This is an image that we do not want deployed based on the policy we defined.
❯ kubectl run python-library -n apps --image us-central1-docker.pkg.dev/minimus-sa/python/python-library:latest
Error from server: admission webhook "validate.kyverno.svc-fail" denied the request:
resource Pod/apps/python-library was blocked due to the following policies
allowed-base-images:
allow-minimus-base: 'validation failure: The base image for this container is not
a Minimus base image. Please contact the platform team for assistance.'
Success! Now, let’s test with python-mini, which is built on top of Minimus’s python:3.13 image.
❯ kubectl run python-mini --image us-central1-docker.pkg.dev/minimus-sa/python/python-mini:latest
pod/python-mini created
Obviously, the policies we’ve defined today are functional guard rails; however, they represent only a portion of the policies that a security team might wish to apply. Some further thoughts on reasonable guard rails that might be applied:
By combining Kyverno with Minimus’s hardened, low-CVE base images, you can put practical guard rails in place and keep insecure builds from ever reaching production.
Looking to simplify secure image adoption across your Kubernetes clusters? Explore Minimus base images and start reducing vulnerabilities from the ground up.
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: allowed-base-images
annotations:
policies.kyverno.io/title: Allowed Base Images
policies.kyverno.io/category: Other
policies.kyverno.io/severity: medium
kyverno.io/kyverno-version: 1.7.0
policies.kyverno.io/minversion: 1.7.0
kyverno.io/kubernetes-version: "1.23"
policies.kyverno.io/subject: Pod
policies.kyverno.io/description: >-
Minimal images, threat intelligence, flexible integration and alerting -
Minimus makes application security easy..
spec:
validationFailureAction: Enforce
rules:
- name: allow-minimus-base
match:
any:
- resources:
kinds:
- Pod
namespaces:
- apps
preconditions:
all:
- key: "{{request.operation || 'BACKGROUND'}}"
operator: NotEquals
value: DELETE
validate:
message: >-
The base image for this container is not a Minimus base image.
Please contact the platform team for assistance.
foreach:
- list: "request.object.spec.[ephemeralContainers, initContainers, containers][]"
context:
- name: imageData
imageRegistry:
reference: "{{ element.image }}"
- name: basename
variable:
jmesPath: imageData.manifest.annotations."org.opencontainers.image.base.name"
default: ''
deny:
conditions:
any:
- key: "{{ basename }}"
operator: AnyNotIn
value: "reg.mini.dev/*"