Admission Webhooks Implementation

The implementation of Admission Webhooks is not complex. An application simply accepts https requests on a specific endpoint and returns a modified Admission Review object. In our example we will deny pods with an image which does not match a regex - do not use the latest image.

Application

Our example application will be based on Go HTTP server. The Webhook should listen to port 443 with TLS (a requirement of Kubernetes). We can expose a pod on port 433 or use service to route the traffic. Our application can also be hosted outside of Kubernetes, for example on AWS Serverless Service Lambda.

At first lets define an HTTP Server object with TLS enabled and register 2 endpoints - for Mutation and Validation.

func main() {
    http.HandleFunc("/validation", validation)

    if err := http.ListenAndServeTLS(":443", "./tls.crt", "./tls.key", nil); err != nil {
        panic(err)
    }
}

Incoming Requests

Requests that are sent to the Admission Webhook from the ApiServer are in JSON format. In either case, we will receive a AdmissionReview object, so we can use JSON marshall to create an object from the request body.

    if r.Body == nil {
        return
    }

    defer r.Body.Close()

    data, err := ioutil.ReadAll(r.Body)
    if err != nil {
        panic(err)
    }

    var admissionReview admission.AdmissionReview

    err = json.Unmarshal(data, &admissionReview)
    if err != nil {
        panic(err)
    }

Request Object Kind Detection

Our admission webhook can handle many kinds of Kubernetes objects and more than one operation. Before trying to operate on an object we need to check it's type.

    if admissionReview.Request.Kind.Kind != "Pod" {
        return
    }

    obj, err := admissionReview.Request.Object.MarshalJSON()
    if err != nil {
        panic(err)
    }

    var pod core.Pod

    err = json.Unmarshal(obj, &pod)
    if err != nil {
        panic(err)
    }

Object Validation

Let's validate if our Pod is using valid images. To do that we need to check Container and InitContainer lists in Pod object data.

First, let's define a function which is validating the pod object.

func arePodImagesValid(pod *core.Pod) error {
    for _, container := range pod.Spec.InitContainers {
        if len(strings.Split(container.Image, ":")) != 2 || strings.Split(container.Image, ":")[1] == "latest" {
            return fmt.Errorf("initContainer %s has invalid image %s", container.Name, container.Image)
        }
    }

    for _, container := range pod.Spec.Containers {
        if len(strings.Split(container.Image, ":")) != 2 || strings.Split(container.Image, ":")[1] == "latest" {
            return fmt.Errorf("container %s has invalid image %s", container.Name, container.Image)
        }
    }

    return nil
}

Now, let's use it to validate our image object.

    validationError := arePodImagesValid(&pod)

Response Preparation

To prepare a webhook response we need to create an AdmissionResponse object and a received Admission Review object and copy the UID from the AdmissionRequest object. In our example we will also set up a validation error message in case of a validation error.

    if validationError != nil {
        admissionReview.Response = &admission.AdmissionResponse{
            Allowed: false,
            Result: &v1.Status{
                Message: validationError.Error(),
            },
        }
    } else {
        admissionReview.Response = &admission.AdmissionResponse{
            Allowed: true,
        }
    }

    admissionReview.Response.UID = admissionReview.Request.UID

Finally, let's send a response to the client.

    response, err := json.Marshal(admissionReview)
    if err != nil {
        panic(err)
    }

    _, err = w.Write(response)
    if err != nil {
        panic(err)
    }

Webhook Code

The complete application code is hosted on github.

SSL Certs generation for service

When we are generating certificates for Admission Webhook server we need to remember that we need to set a proper Common Name in the following format: <service name>.<service namespace>.svc. We can generate this certificate using an openssl command:

openssl req -new -newkey rsa:4096 -days 365 -nodes -x509 -subj "/CN=admission.admission.svc" -keyout tls.key -out tls.crt

Preparation for Webhook installation

First of all, let's create a namespace for the Admission Webhook:

kubectl create namespace admission

Then, let's create a secret with provided key and cert:

kubectl -n admission create secret tls ssl --cert tls.crt --key tls.key

Webhook cluster installation

Before installing our Webhook we need to create a docker image. But first, lets compile our code.

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o admission main.go

And lets build our image using a simple dockerfile:

FROM scratch

ADD admission /admission

Before deploying our creation we need to make the image accessible to our cluster. For this example we will use this as admission:0.1, which are already present on machines.

Initially, we need to create a deployment with the admission pod:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: admission
  namespace: admission
  labels:
    app: admission
spec:
  replicas: 1
  selector:
    matchLabels:
      app: admission
  template:
    metadata:
      labels:
        app: admission
    spec:
      containers:
      - image: "admission:0.1"
        imagePullPolicy: "IfNotPresent"
        name: admission
        command:
        - /admission
        volumeMounts:
        - name: certs
          mountPath: /ssl
          readOnly: true
        workingDir: /ssl
      volumes:
      - name: certs
        secret:
          secretName: ssl

And expose it using a service:

apiVersion: v1
kind: Service
metadata:
  name: admission
  namespace: admission
  labels:
    app: admission
spec:
  ports:
  - port: 443
    targetPort: 443
    protocol: TCP
  selector:
    app: admission
  type: ClusterIP

After that we need to create a ValidatingWebhookConfiguration object:

apiVersion: admissionregistration.k8s.io/v1beta1
kind: ValidatingWebhookConfiguration
metadata:
  name: validation-hook
  labels:
    app: admission
webhooks:
- name: namespace.security.example
  failurePolicy: Fail
  rules:
  - apiGroups:
    - ""
    apiVersions:
    - v1
    operations:
    - CREATE
    - UPDATE
    resources:
    - pods
  clientConfig:
    service:
      path: /validation
      namespace: admission
      name: admission
    caBundle: <base64 encoded cert>
  namespaceSelector:
    matchLabels:
      admission: "true"

Admission Webhook tests

Create a namespace for tests:

kubectl create ns admission-test

Enable Webhook Validation in this namespace:

kubectl label namespace admission-test admission=true

Now Webhook Validation is enforced in this namespace. Next, we can try to create a new image with our latest image:

apiVersion: v1
kind: Pod
metadata:
  name: deny-pod-image
  namespace: admission-test
  labels:
    app: admission
spec:
  containers:
  - name: test
    image: "admission:latest"
Error from server: error when creating "STDIN": admission webhook "namespace.security.example" denied the request: container test has invalid image admission:latest

And with an image other than the latest image:

apiVersion: v1
kind: Pod
metadata:
  name: allow-pod-image
  namespace: admission-test
  labels:
    app: admission
spec:
  containers:
  - name: test
    image: "admission:no-latest"
pod/allow-pod-image created

Summary

As you can see, Webhook Admissions are not that hard to implement and deploy on a Kubernetes cluster. They give Kubernetes Admins a powerful tool to enforce additional security policies.