Extendiendo la funcionalidad de Kubernetes (Admission Controllers)(Parte I)

Una de las ventajas de Kubernetes es su alto grado de “customizacion”. Prácticamente como administradores podemos programar cualquier aspecto del cluster. La potencia de los “Admission controllers” reside en el hecho de que podemos interceptar las peticiones a la API de Kubernetes antes de que el objeto se cree.

Existen dos tipos de Admission Controllers. (Mutating Admission Controllers) que pueden mutar o cambiar un objeto antes de que se cree y (Validating Admission Controllers) que permiten o deniegan un objeto.

En estas primeras entradas vamos a crear un Mutating Admission de una manera sencilla para que se entienda el concepto.

Como se puede observar en la imagen los Mutating Webhook interceptan el Request de la Api, la modifican y envian la respuesta(Response).

Los pasos a seguir serian estos:

  • Crear un servidor Https
  • Programar el Webhook

En esta primera entrada nos vamos a dedicar solo al punto primero. Crear el servidor Https que escuche las peticiones de la API en GO.

Para empezar nos creamos un proyecto go y ejecutamos el “go mod init”.

El código del servidor web es sencillo y no es necesaria ningún tipo de explicación. Utilizaremos esto codigo para simplemente ver si la llamada al webHook es correcta.

package main

import (
"log"
"net/http"
)

func webHook(w http.ResponseWriter, r *http.Request) {

   log.Printf(“Called Webhook”)



}

func main() {

   http.HandleFunc("/mutate", webHook)
   log.Fatal(http.ListenAndServeTLS(":443", "/certs/webhook.crt", "/certs/webhook-key.pem", nil))
}

Creamos un Dockerfile para crear la Imagen.

FROM golang:alpine

WORKDIR /code
COPY . /code

RUN go build -o kcontroller

ENTRYPOINT ["./kcontroller"]

Construimos la imagen y la subimos al repositorio

docker image build -t anmartsan/webhook:latest .

docker push anmartsan/webhook:latest 

Quiza la parte mas complicada es la creacion de los certificados que utiliza el web server. Debemos añadir un CSR a kubernetes y validarlo y utilizar ese certificado ya validado en un Secret al que accedera el deployment.

Utilizaremos la herramienta “cfssl”. Primero tenemos que editar el JSON del csr. Teniendo en cuenta la documentacion de Kubernetes .

“This admission controller limits the Node and Pod objects a kubelet can modify. In order to be limited by this admission controller, kubelets must use credentials in the system:nodes group, with a username in the form system:node:<nodeName>”.

El csr.json quedaria de este modo:

{
    "CN": "system:node:kcontroller.default.svc.cluster.local",
    "hosts": [
      "kcontroller.default.svc",
      "kcontroller.default.svc.cluster.local"
    ],
    "key": {
      "algo": "rsa",
      "size": 2048
     },
    "names": [{
      
      "O": "system:nodes"
     
    }]
  }
cat ./csr/csr.json |cfssl genkey - | cfssljson -bare webhook

2021/03/31 10:31:48 [INFO] generate received request
2021/03/31 10:31:48 [INFO] received CSR
2021/03/31 10:31:48 [INFO] generating key: rsa-2048
2021/03/31 10:31:49 [INFO] encoded CSR

Una vez que tenemos el Certifcated Signing Request y la clave primaria para el servidor web tenemos que aprobarlos contra el CA del cluster kubernetes.

cat webhook.csr |base64 -w 0
LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQzlEQ0NBZHdDQVFBd1V6RVZNQk1HQTFVRUNoTU1jM2x6ZEdWdE9tNXZaR1Z6TVRvd09BWURWUVFERXpGegplWE4w………………………………
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: kcontroller.default
spec:
  groups:
  - system:authenticated
  request: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURSBSRVFVRVNULS0tLS0KTUlJQzlEQ0NBZHdDQVFBd1V6RVZNQk1HQTFVRUNoTU1jM2x6ZEdWdE9tNXZaR1Z6TVRvd09BWURWUVFERXpGegplWE4w………………………………
  signerName: kubernetes.io/kubelet-serving
  usages:
  - server auth
  - digital signature
  - key encipherment
kubectl create -f kubernetes/cert.yaml 
certificatesigningrequest.certificates.k8s.io/kcontroller.default created

kubectl certificate approve kcontroller.default
certificatesigningrequest.certificates.k8s.io/kcontroller.default approved

kubectl get csr kcontroller.default
NAME                  AGE   SIGNERNAME                      REQUESTOR          CONDITION
kcontroller.default   87s   kubernetes.io/kubelet-serving   kubernetes-admin   Approved,Issued
Con esto ya tendríamos el certificado y la clave privada para el servidor web.

Creamos ahora el “MutatingWebhookConfiguration”. Para ello podemos los ejemplos de la documentacion de kubernetes “https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/

El caBundle lo tenemos que sacar de nuestro cluster con por ejemplo:

‘kubectl config view –minify –raw –output ‘jsonpath={..cluster.certificate-authority-data}’

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
  name: kcontroller
webhooks:
- name: kcontroller.default.svc.cluster.local
  rules:
  - operations: ["CREATE"]
    apiGroups: [""]
    apiVersions: ["v1"]
    resources: ["pods"]
  failurePolicy: Ignore
  clientConfig:
    service:
      path: "/mutate"
      port: 443
      name: kcontroller
      namespace: default
    caBundle: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM1ekNDQWMrZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRF……….
  sideEffects: None
  admissionReviewVersions: 
      - "v1"

Como se puede ver la operación es “CREATE” de los resources “pods”. Es decir el clientConfig se ejecutara en el caso de que la API reciba una operación de creación de Pods.

kubcetl create -f admission.yaml

mutatingwebhookconfiguration.admissionregistration.k8s.io/kcontroller created

Generamos el deployment que se va ha encargar de interceptar las peticiones del admission controller y el secret que llevara la autorizacion TLS (llave privada y certificado que hemos creado antes):

apiVersion: apps/v1
kind: Deployment
metadata:
  name: kcontroller
spec:
  selector:
    matchLabels:
      name: kcontroller
  replicas: 1
  template:
    metadata:
      labels:
        name: kcontroller
    spec:
      volumes:
        - name: certs
          secret:
            secretName: kcontroller
      containers:
      - image:  anmartsan/webhook
        name:  kcontroller
        resources:
          requests:
            cpu: "20m"
            memory: "55M"
        ports:
        - containerPort:  443
          name:  kcontroller
        volumeMounts:
          - name:  certs
            mountPath: /certs
        imagePullPolicy: Always
            
      restartPolicy: Always
        
---

apiVersion: v1
kind: Secret
metadata:
  name:  kcontroller
data:
  webhook-key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBd3M0bW91cVFPbUtVN1QzNnYzbmxBWkVwQ0poY2oyN1Rxa3BzRStNaWZZNjRDcDZ4CmlHdWtjQ1FUWHF3MWp5bm9LdW5JNXpXbFBHeH.........................
  webhook.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURsakNDQW42Z0F3SUJBZ0lSQUthaVpzNGRiL2wrMXFoYXdIQXByeUl3RFFZSktvWklodmNOQVFFTEJRQXcKRlRFVE1CRUdBMVVFQXhNS2EzVmlaWEp1WlhSbGN6QW.........................
kubectl create -f ./kubernetes/webhook.yaml

Podemos hacer la comprobación para ver si intercepta las llamadas a la creación de un Pod. Simplemente ejecutamos en un terminal

k logs -f kcontroller-f58fb84dc-vpts7

Y en otro terminal diferente creamos un Pod

kubernetes run nginx --image=nginx –restart=Never

Hasta ahora el webHook no hace nada, en el siguiente Post le añadiremos la funcionalidad al webHook.