DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Because the DevOps movement has redefined engineering responsibilities, SREs now have to become stewards of observability strategy.

Apache Cassandra combines the benefits of major NoSQL databases to support data management needs not covered by traditional RDBMS vendors.

The software you build is only as secure as the code that powers it. Learn how malicious code creeps into your software supply chain.

Generative AI has transformed nearly every industry. How can you leverage GenAI to improve your productivity and efficiency?

Related

  • Containerize Gradle Apps and Deploy to Kubernetes With JKube Kubernetes Gradle Plugin
  • Create a Kubernetes Cluster With Centos
  • Setup and Configure Velero on AKS
  • EFK Stack on Kubernetes (Part 1)

Trending

  • Can You Run a MariaDB Cluster on a $150 Kubernetes Lab? I Gave It a Shot
  • Building a Real-Time Audio Transcription System With OpenAI’s Realtime API
  • AI Speaks for the World... But Whose Humanity Does It Learn From?
  • The Evolution of Scalable and Resilient Container Infrastructure
  1. DZone
  2. Software Design and Architecture
  3. Cloud Architecture
  4. Kubernetes Image Policy Webhook Explained

Kubernetes Image Policy Webhook Explained

How to create and deploy a Kubernetes image policy webhook.

By 
Gabriel Garrido user avatar
Gabriel Garrido
·
Updated Jan. 17, 21 · Tutorial
Likes (2)
Comment
Save
Tweet
Share
27.5K Views

Join the DZone community and get the full member experience.

Join For Free
Image for post

This image was taken from the k8s docs

Introduction

In this article, we will explore how webhooks work in Kubernetes and, more specifically, about the ImagePolicyWebhook. The Kubernetes documentation about it is kind of vague, since there is no real example or implementation that you can get out of it, so here, we will break it down to the different alternatives. In a real-world scenario, I would prefer to rely on OPA Gatekeeper, but I’m planning to make this trip worth it by adding a database and making the webhook allow or disallow images based on a vulnerability scan — for example, allow only medium or lower vulnerabilities in your containers — but that will be a post for another day. If you are interested, you can help in this repo. For more information in general, see here.

There are two ways to make this work, and each one has a slightly different behavior. One way is using the ImagePolicyWebhook and the other is using Admission Controllers. Either validating or mutating works, but here I used the validating webhook. You can learn more here.

This admission controller will reject all the pods that are using images with the latest tag and, in the future, we will see if pods cannot meet required security levels.

Comparison

The ImagePolicyWebhook is an admission controller that evaluates only images. You need to parse the requests do the logic and the response in order to allow or deny images in the cluster.

The good parts of the ImagePolicyWebhook:

  • The API server can be instructed to reject the images if the webhook endpoint is not reachable. This is quite handy, but it can also bring issues, like core pods won’t be able to schedule.

The bad parts of the ImagePolicyWebhook:

  • The configuration is a bit more involved and requires access to the master nodes or to the API server configuration. The documentation is not clear, and it can be hard to make changes, update, etc.
  • The deployment is not trivial. You need to deploy it with systemd or run it as a Docker container in the host, update the DNS, etc.

On the other hand, the ValidatingAdmissionWebhook can be used for way more things than just images (if you use the mutating one, well, you can inject or change things on the fly).

The good parts of the ValidatingAdmissionWebhook:

  • Easier deployment since the service runs as a pod.
  • Everything can be a Kubernetes resource.
  • Less manual intervention, and access to the master is not required.
  • If the pod or service is unavailable then all images are going to be allowed which can be a security risk in some cases, so if you are going this path be sure to make it highly available, this can actually be configured by specifying the failurePolicy to Fail instead of Ignore (Fail is the default).

The bad parts about the ValidatingAdmissionWebhook:

  • Anyone with enough RBAC permissions can update/change the configuration since it’s just another kubernetes resource.

Building

If you intend to use it as a plain service:

Shell
 




x


 
1
$ go get github.com/kainlite/kube-image-bouncer


You can also use this Docker image:

Shell
 




xxxxxxxxxx
1


 
1
$ docker pull kainlite/kube-image-bouncer


Certificates

We can rely on the Kubernetes CA to generate the certificate that we need. If you want to learn more, go here:

Create a CSR:

Shell
 




xxxxxxxxxx
1
21


 
1
$ cat <<EOF | cfssl genkey - | cfssljson -bare server
2
{
3
  "hosts": [
4
    "image-bouncer-webhook.default.svc",
5
    "image-bouncer-webhook.default.svc.cluster.local",
6
    "image-bouncer-webhook.default.pod.cluster.local",
7
    "192.0.2.24",
8
    "10.0.34.2"
9
  ],
10
  "CN": "system:node:image-bouncer-webhook.default.pod.cluster.local",
11
  "key": {
12
    "algo": "ecdsa",
13
    "size": 256
14
  },
15
  "names": [
16
    {
17
      "O": "system:nodes"
18
    }
19
  ]
20
}
21
EOF


Then apply it to the cluster

Shell
 




xxxxxxxxxx
1
13


 
1
$ cat <<EOF | kubectl apply -f -
2
apiVersion: certificates.k8s.io/v1
3
kind: CertificateSigningRequest
4
metadata:
5
  name: image-bouncer-webhook.default
6
spec:
7
  request: $(cat server.csr | base64 | tr -d '\n')
8
  signerName: kubernetes.io/kubelet-serving
9
  usages:
10
  - digital signature
11
  - key encipherment
12
  - server auth
13
EOF


Approve and get your certificate for later use:

Shell
 




xxxxxxxxxx
1


 
1
$ kubectl get csr image-bouncer-webhook.default -o jsonpath='{.status.certificate}' | base64 --decode > server.crt


ImagePolicyWebhook Path

There are two possible ways to deploy this controller (webhook). For this to work, you will need to create the certificates as explained below, but first, we need to take care of other details. Add this to your hosts file in the master (or where the bouncer will run).

We use this name because it has to match with the names from the certificate. Since this will run outside Kubernetes, and it could even be externally available, we just fake it with a hosts entry.

Shell
 




xxxxxxxxxx
1


 
1
$ echo "127.0.0.1 image-bouncer-webhook.default.svc" >> /etc/hosts


Also in the API server you need to update it with these settings:

Shell
 




xxxxxxxxxx
1


 
1
--admission-control-config-file=/etc/kubernetes/kube-image-bouncer/admission_configuration.json --enable-admission-plugins=ImagePolicyWebhook


If you did this, you don’t need to create the validating-webhook-configuration.yaml resource nor apply the Kubernetes deployment to run in the cluster.

Create an admission control configuration file named /etc/kubernetes/kube-image-bouncer/admission_configuration.json with the following contents:

JSON
 




x


 
1
{
2
  "imagePolicy": {
3
     "kubeConfigFile": "/etc/kubernetes/kube-image-bouncer/kube-image-bouncer.yml",
4
     "allowTTL": 50,
5
     "denyTTL": 50,
6
     "retryBackoff": 500,
7
     "defaultAllow": false
8
  }
9
}


Adjust the defaults if you want to allow images by default.

Create a kubeconfig file /etc/kubernetes/kube-image-bouncer/kube-image-bouncer.yml with the following contents:

Properties files
 




xxxxxxxxxx
1
19


 
1
apiVersion: v1
2
kind: Config
3
clusters:
4
- cluster:
5
    certificate-authority: /etc/kubernetes/kube-image-bouncer/pki/server.crt
6
    server: https://image-bouncer-webhook.default.svc:1323/image_policy
7
  name: bouncer_webhook
8
contexts:
9
- context:
10
    cluster: bouncer_webhook
11
    user: api-server
12
  name: bouncer_validator
13
current-context: bouncer_validator
14
preferences: {}
15
users:
16
- name: api-server
17
  user:
18
    client-certificate: /etc/kubernetes/pki/apiserver.crt
19
    client-key:  /etc/kubernetes/pki/apiserver.key


This configuration file instructs the API server to reach the webhook server at https://image-bouncer-webhook.default.svc:1323 and use its /image_policy endpoint. We're reusing the certificates from the API server and the one for kube-image-bouncer that we already generated.

Be aware that you need to be sitting in the folder with the certs for that to work:

Shell
 




xxxxxxxxxx
1


 
1
$ docker run --rm -v `pwd`/server-key.pem:/certs/server-key.pem:ro \
2
-v `pwd`/server.crt:/certs/server.crt:ro -p 1323:1323 \
3
--network hostkainlite/kube-image-bouncer \
4
-k /certs/server-key.pem -c /certs/server.crt


ValidatingAdmissionWebhook Path

If you are doing this, all you need to do is generate the certificates. Everything else can be done with kubectl. First of all, you have to create a TLS secret holding the webhook certificate and key (we just generated this in the previous step):

Shell
 




xxxxxxxxxx
1


 
1
$ kubectl create secret tls tls-image-bouncer-webhook \ 
2
--key server-key.pem \ 
3
--cert server.pem


Then create a Kubernetes deployment for the image-bouncer-webhook:

Shell
 




xxxxxxxxxx
1


 
1
$ kubectl apply -f kubernetes/image-bouncer-webhook.yaml


Finally, create ValidatingWebhookConfiguration that makes use of our webhook endpoint. You can use this, but be sure to update the caBundle with the server.crt content in base64:

Shell
 




xxxxxxxxxx
1


 
1
$ kubectl apply -f kubernetes/validating-webhook-configuration.yaml


Or you can simply generate the validating-webhook-configuration.yaml file like this and apply it in one go:

Shell
 




xxxxxxxxxx
1
25


 
1
$ cat <<EOF | kubectl apply -f -
2
apiVersion: admissionregistration.k8s.io/v1
3
kind: ValidatingWebhookConfiguration
4
metadata:
5
  name: image-bouncer-webook
6
webhooks:
7
  - name: image-bouncer-webhook.default.svc
8
    rules:
9
      - apiGroups:
10
          - ""
11
        apiVersions:
12
          - v1
13
        operations:
14
          - CREATE
15
        resources:
16
          - pods
17
    failurePolicy: Ignore
18
    sideEffects: None
19
    admissionReviewVersions: ["v1", "v1beta1"]
20
    clientConfig:
21
      caBundle: $(kubectl get csr image-bouncer-webhook.default -o jsonpath='{.status.certificate}')
22
      service:
23
        name: image-bouncer-webhook
24
        namespace: default
25
EOF



This could be easily automated (Helm chart coming soon...). Changes can take a bit to reflect, so wait a few seconds and give it a try.

Testing

Both paths should work the same way, and you will see a similar error message:

Shell
 




xxxxxxxxxx
1


 
1
Error creating: pods "nginx-latest-sdsmb" is forbidden: image policy webhook backend denied one or more images: Images using latest tag are not allowed


or

Shell
 




xxxxxxxxxx
1


 
1
Warning FailedCreate 23s (x15 over 43s) replication-controller Error creating: admission webhook "image-bouncer-webhook.default.svc" denied the request: Images using latest tag are not allowed


Create an nginx-versioned RC to validate that the versioned releases still work:

Shell
 




xxxxxxxxxx
1
21


 
1
$ cat <<EOF | kubectl apply -f -
2
apiVersion: v1
3
kind: ReplicationController
4
metadata:
5
  name: nginx-versioned
6
spec:
7
  replicas: 1
8
  selector:
9
    app: nginx-versioned
10
  template:
11
    metadata:
12
      name: nginx-versioned
13
      labels:
14
        app: nginx-versioned
15
    spec:
16
      containers:
17
      - name: nginx-versioned
18
        image: nginx:1.13.8
19
        ports:
20
        - containerPort: 80
21
EOF


Ensure/check the replication controller is actually running:

Shell
 




xxxxxxxxxx
1


 
1
$ kubectl get rc
2
NAME              DESIRED   CURRENT   READY     AGE
3
nginx-versioned   1         1         0         2h


Now create one for nginx-latest to validate that our controller/webhook can actually reject pods with images using the latest tag:

Shell
 




xxxxxxxxxx
1
21


 
1
$ cat <<EOF | kubectl apply -f -
2
apiVersion: v1
3
kind: ReplicationController
4
metadata:
5
  name: nginx-latest
6
spec:
7
  replicas: 1
8
  selector:
9
    app: nginx-latest
10
  template:
11
    metadata:
12
      name: nginx-latest
13
      labels:
14
        app: nginx-latest
15
    spec:
16
      containers:
17
      - name: nginx-latest
18
        image: nginx
19
        ports:
20
        - containerPort: 80
21
EOF


If we check the pod, it should not be created and the RC should show something similar to the following output. You can also check with kubectl get events --sort-by='{.lastTimestamp}':

Shell
 




xxxxxxxxxx
1
26


 
1
$ kubectl describe rc nginx-latest
2
Name:         nginx-latest
3
Namespace:    default
4
Selector:     app=nginx-latest
5
Labels:       app=nginx-latest
6
Annotations:  <none>
7
Replicas:     0 current / 1 desired
8
Pods Status:  0 Running / 0 Waiting / 0 Succeeded / 0 Failed
9
Pod Template:
10
  Labels:  app=nginx-latest
11
  Containers:
12
   nginx-latest:
13
    Image:        nginx
14
    Port:         80/TCP
15
    Host Port:    0/TCP
16
    Environment:  <none>
17
    Mounts:       <none>
18
  Volumes:        <none>
19
Conditions:
20
  Type             Status  Reason
21
  ----             ------  ------
22
  ReplicaFailure   True    FailedCreate
23
Events:
24
  Type     Reason        Age                 From                    Message
25
  ----     ------        ----                ----                    -------
26
  Warning  FailedCreate  23s (x15 over 43s)  replication-controller  Error creating: admission webhook "image-bouncer-webhook.default.svc" denied the request: Images using latest tag are not allowed



Debugging

It’s always useful to see the API server logs if you are using the admission controller path since it will log why it failed there, and also the logs from the image-bouncer. For example: apiserver

Shell
 




xxxxxxxxxx
1


 
1
W0107 17:39:00.619560       1 dispatcher.go:142] rejected by webhook "image-bouncer-webhook.default.svc": &errors.StatusError{ErrStatus:v1.Status{TypeMeta:v1.TypeMeta{Kind:"", APIVersion:""}, ListMeta:v1.ListMeta{ SelfLink:"", ResourceVersion:"", Continue:"", RemainingItemCount:(*int64)(nil)}, Status:"Failure", Message:"admission webhook \"image-bouncer-webhook.default.svc\" denied the request: Images using latest tag are not allowed", Reason:"", Details:(*v1.StatusDetails)(nil), Code:400}}


kube-image-bouncer:

Shell
 




xxxxxxxxxx
1


 
1
echo: http: TLS handshake error from 127.0.0.1:49414: remote error: tls: bad certificate
2
method=POST, uri=/image_policy?timeout=30s, status=200
3
method=POST, uri=/image_policy?timeout=30s, status=200
4
method=POST, uri=/image_policy?timeout=30s, status=200


The error is from a manual test, the others are successful requests from the API server.

The Code Itself

Let's take a really brief look at the critical parts of creating an admission controller or webhook:

This is a section of the main.go. As we can see, we are handling two POST paths with different methods and some other validations. What we need to know is that we will receive a POST method call with a JSON payload and that we need to convert to an admission controller review request.

Go
 




xxxxxxxxxx
1
24


 
1
app.Action = func(c *cli.Context) error {
2
        e := echo.New()
3
        e.POST("/image_policy", handlers.PostImagePolicy())
4
        e.POST("/", handlers.PostValidatingAdmission())e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{
5
            Format: "method=${method}, uri=${uri}, status=${status}\n",
6
        }))if debug {
7
            e.Logger.SetLevel(log.DEBUG)
8
        }if whitelist != "" {
9
            handlers.RegistryWhitelist = strings.Split(whitelist, ",")
10
            fmt.Printf(
11
                "Accepting only images from these registries: %+v\n",
12
                handlers.RegistryWhitelist)
13
            fmt.Println("WARN: this feature is implemented only by the ValidatingAdmissionWebhook code")
14
        } else {
15
            fmt.Println("WARN: accepting images from ALL registries")
16
        }var err error
17
        if cert != "" && key != "" {
18
            err = e.StartTLS(fmt.Sprintf(":%d", port), cert, key)
19
        } else {
20
            err = e.Start(fmt.Sprintf(":%d", port))
21
        }if err != nil {
22
            return cli.NewExitError(err, 1)
23
        }return nil
24
    }app.Run(os.Args)



This is a section from handlers/validating_admission.go. Basically it parses and validates whether the image should be allowed or not, and then it sends an AdmissionReponse back with the flag Allowed set to true or false. If you want to learn more about the different types used here, you can explore the v1beta1.Admission Documentation:

Go
 




xxxxxxxxxx
1
54


 
1
func PostValidatingAdmission() echo.HandlerFunc {
2
    return func(c echo.Context) error {
3
        var admissionReview v1beta1.AdmissionReviewerr := c.Bind(&admissionReview)
4
        if err != nil {
5
            c.Logger().Errorf("Something went wrong while unmarshalling admission review: %+v", err)
6
            return c.JSON(http.StatusBadRequest, err)
7
        }
8
        c.Logger().Debugf("admission review: %+v", admissionReview)pod := v1.Pod{}
9
        if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil {
10
            c.Logger().Errorf("Something went wrong while unmarshalling pod object: %+v", err)
11
            return c.JSON(http.StatusBadRequest, err)
12
        }
13
        c.Logger().Debugf("pod: %+v", pod)admissionReview.Response = &v1beta1.AdmissionResponse{
14
            Allowed: true,
15
            UID:     admissionReview.Request.UID,
16
        }
17
        images := []string{}for _, container := range pod.Spec.Containers {
18
            images = append(images, container.Image)
19
            usingLatest, err := rules.IsUsingLatestTag(container.Image)
20
            if err != nil {
21
                c.Logger().Errorf("Error while parsing image name: %+v", err)
22
                return c.JSON(http.StatusInternalServerError, "error while parsing image name")
23
            }
24
            if usingLatest {
25
                admissionReview.Response.Allowed = false
26
                admissionReview.Response.Result = &metav1.Status{
27
                    Message: "Images using latest tag are not allowed",
28
                }
29
                break
30
            }if len(RegistryWhitelist) > 0 {
31
                validRegistry, err := rules.IsFromWhiteListedRegistry(
32
                    container.Image,
33
                    RegistryWhitelist)
34
                if err != nil {
35
                    c.Logger().Errorf("Error while looking for image registry: %+v", err)
36
                    return c.JSON(
37
                        http.StatusInternalServerError,
38
                        "error while looking for image registry")
39
                }
40
                if !validRegistry {
41
                    admissionReview.Response.Allowed = false
42
                    admissionReview.Response.Result = &metav1.Status{
43
                        Message: "Images from a non whitelisted registry",
44
                    }
45
                    break
46
                }
47
            }
48
        }if admissionReview.Response.Allowed {
49
            c.Logger().Debugf("All images accepted: %v", images)
50
        } else {
51
            c.Logger().Infof("Rejected images: %v", images)
52
        }c.Logger().Debugf("admission response: %+v", admissionReview.Response)return c.JSON(http.StatusOK, admissionReview)
53
    }
54
}



Everything is in this repo.

Closing Words

This example and the original post were done here, so thank you Flavio Castelli for creating such a great example. My changes are mostly about explaining how it works and the required changes for it to work in the latest Kubernetes release (at the moment v1.20.0), as I was learning to use it and to create my own.

The readme file in the project might not match this article but both should work. I haven't updated the entire readme yet.

Errata

If you spot an error or have any suggestions, please send me a message so it gets fixed.

Also, you can check the source code and changes in the generated code and the sources here.

Kubernetes Webhook shell Docker (software)

Published at DZone with permission of Gabriel Garrido. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • Containerize Gradle Apps and Deploy to Kubernetes With JKube Kubernetes Gradle Plugin
  • Create a Kubernetes Cluster With Centos
  • Setup and Configure Velero on AKS
  • EFK Stack on Kubernetes (Part 1)

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!