Container Image Trust with Sigstore Policy Controller and vCluster

Jubril Oyetunji
7 Minute Read

Supply chain security has become increasingly important in cloud native space. Recent incidents like the xz backdoor and even SolarWinds have shown that implicit trust in upstream dependencies can have dangerous cascading effects.

It's common practice for organizations to set up private container registries as a first line of defense. However, how often are these enforced at the cluster level? Many teams overlook this step, leaving their environments vulnerable to running untrusted images.

The Sigstore policy controller is an admission controller that can be used to enforce policy on a Kubernetes cluster based on custom rules. In this tutorial, we will set up the Sigstore policy controller and examine its policy enforcement capabilities.

Why the Sigstore Policy controller

The Sigstore policy controller offers a powerful feature set which can aid multi-tenancy efforts, such as preventing pods from running as root, verifying signed images and much more.

A good use case is multiple teams sharing a single cluster using vCluster , to ensure only verified images are being run on each cluster an image policy can be defined and applied per cluster or as the team sees fit.

In a report by sysdig, 58% of containers are still running as root, the Sigstore policy controller provides a means for disabling privileged containers on a per namespace basis.

Prerequisites

This tutorial assumes some familiarity with Kubernetes, additionally you will need the following installed locally in order to follow along.

What's vCluster?

If you are unfamiliar vCluster is an open-source tool for creating virtual Kubernetes clusters, asides providing true isolation virtual cluster provides you with full blown kubernetes clusters where teams can operate with little fear of affecting other workloads in the same cluster.

That’s great but why would you want to pair the sigstore controller with vCluster? While using a virtual cluster is a huge security boost, its still a security risk if you are running untrusted images or pods which run the risk of compromising the entire cluster.

Create a vCluster

We’ll begin by creating a new vCluster:

vcluster create policy-demo --namespace sigstore

This will create a new vCluster and switch your kube-context to use the new vCluster.

Installing the Sigstore Policy Controller

Next, create a namespace to house the controller and associated resources.

kubectl create namespace cosign-system

Add the policy controller chart repo:

Moving along, we add the sigstore repository to helm.

helm repo add sigstore <https://sigstore.github.io/helm-charts>

Update and install the chart:

Finally we can update the repository and install the controller.

helm repo update && helm install policy-controller -n cosign-system sigstore/policy-controller

Output is similar to:

~ ❯ helm repo update && helm install policy-controller -n cosign-system sigstore/policy-controller
Hang tight while we grab the latest from your chart repositories...
...Successfully got an update from the "sigstore" chart repository
Update Complete. ⎈Happy Helming!⎈
NAME: policy-controller
LAST DEPLOYED: Wed Oct  9 08:12:06 2024
NAMESPACE: cosign-system
STATUS: deployed
REVISION: 1
TEST SUITE: None 

Creating a ClusterImagePolicy

ClusterImagePolicy is a custom resource the Sigstore controller provides, It enables us to explicitly define images which are allowed or denied from being run on the cluster.

To enable to controller in the default namespace, add the following label using kubectl:

kubectl label namespace default policy.sigstore.dev/include=true

Output is similar to:

namespace/default labeled

Verify the controller is active by creating a new pod:

kubectl run --image nginx isthisallowed

Output is similar to:

Error from server (BadRequest): admission webhook "policy.sigstore.dev" denied the request: validation failed: no matching policies: spec.containers[0].image
index.docker.io/library/nginx@sha256:6af79ae5de407283dcea8b00d5c37ace95441fd58a8b1d2aa1ed93f5511bb18c

The output above indicates we have no image policies defined as such we can not run just any image.

Let’s create a ClusterImagePolicy to only accept images from the chainguard image registry. Using kubectl apply the following manifest.

cat << EOF | kubectl apply -f -
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: chainguard-image-policy
spec:
  images:
    - glob: "cgr.dev/chainguard/**"
  authorities:
    - static:
        action: pass
EOF

Using spec.images we define the target registry and in spec.authorities.static we set action to pass to indicate we want allow the image from the registry.

Once applied your output should be similar to:

clusterimagepolicy.policy.sigstore.dev/chainguard-image-policy created

Test the Controller

With an image policy defined, run an image from the Chainguard registry:

kubectl run --image cgr.dev/chainguard/nginx:latest nginx

Vertify the pod was created by by running:

kubectl get pods

Output is similar to:

nginx  1/1  Running     0   16s

In the output we see the nginx pod is running, indicating the pod is compliant with policy defined earlier.

Another interesting use case for the sigstore is disallowing certain image versions. Building on the previous example, we can use a custom policy to disallow usage of the latest version of images, this will encourage pining the version of images being used as well making it easier to track a version of an image causing problems, head back to the command line and apply the following policy:

kubectl apply -f - <<EOF
apiVersion: policy.sigstore.dev/v1beta1
kind: ClusterImagePolicy
metadata:
  name: disallow-latest-tag
spec:
  images: [glob: '**']
  authorities: [static: {action: pass}]
  mode: enforce
  policy:
    includeSpec: true
    type: rego
    data: |
      package sigstore
      default isCompliant = false
      isCompliant {
        # Split the image reference into parts
        parts := split(input.image, ":")
        
        # Check if there's a tag (length should be 2)
        count(parts) == 2
        
        # Ensure the tag is not "latest"
        parts[1] != "latest"
      }
EOF

In the manifest we use rego, one of the sigstore’s supported configuration languages to, split the input image and asser that the stage is not latest.

With the policy applied let’s try and run another nginx pod with the latest tag:

kubectl run --image cgr.dev/chainguard/nginx:latest nginx

Output is similar to:

Error from server (BadRequest): admission webhook "policy.sigstore.dev" denied the request: validation failed: failed policy: disallow-latest-tag: spec.containers[0].image
cgr.dev/chainguard/nginx@sha256:69b5e4eeea7b5f044e2ce5af4d7c5fd3933d81579178af2229aaf67681969b3e failed evaluating rego policy for type ClusterImagePolicy: policy is not compliant for query 'isCompliant = data.sigstore.isCompliant'

We get a response form sigstore controller indicating that the image tag is not compliant.

If we try a using a specific tag:

kubectl run --image cgr.dev/chainguard/nginx:latest-dev thisworks

Output is similar to:

pod/thisworks created

Note in the example we use the tag latest-dev because at the time of writing chainguard doesn’t offer specific versions of their images by default.

Conclusion

In this post, we explored how to enhance container image security using the Sigstore Policy Controller in Kubernetes environments. We walked through the installation process, set up a ClusterImagePolicy to allow images only from a trusted registry (Chainguard in this case), and demonstrated how the policy enforces these rules at the cluster level.

This approach is particularly valuable in multi-tenant environments where ensuring only trusted or vetted images are being run is crucial. The ability to define policies per namespace makes it flexible and adaptable to different security requirements across various teams or projects within the same cluster.

This approach is particularly valuable in multi-tenant environments where ensuring only trusted or vetted images are being run is crucial. Teams can create per cluster policies which can then be tested across staging and production environments, as we have shown in the last example, rego and cue offer flexible ways to customize polices as you see fit.

While we focused on image policies in this tutorial, it's worth noting that the Sigstore Policy Controller is capable of much more, users can also define custom rules using CUE.

Looking to learn more about admission controllers? Here are some ideas:

Sign up for our newsletter

Be the first to know about new features, announcements and industry insights.