From bff03ec54d3bb33d8d045a780470c09ea139cc57 Mon Sep 17 00:00:00 2001 From: Michele Cereda Date: Sat, 22 Apr 2023 14:17:14 +0200 Subject: [PATCH] chore: imported articles and examples from the private kb --- .../create an admission webhook/Dockerfile | 8 + .../create an admission webhook/README.md | 263 ++++++++++++++++++ .../create-signed-cert.sh | 131 +++++++++ .../resources.yaml | 89 ++++++ .../create an admission webhook/webhook.py | 64 +++++ knowledge base/golang.md | 43 +++ 6 files changed, 598 insertions(+) create mode 100644 examples/kubernetes/create an admission webhook/Dockerfile create mode 100644 examples/kubernetes/create an admission webhook/README.md create mode 100755 examples/kubernetes/create an admission webhook/create-signed-cert.sh create mode 100644 examples/kubernetes/create an admission webhook/resources.yaml create mode 100644 examples/kubernetes/create an admission webhook/webhook.py create mode 100644 knowledge base/golang.md diff --git a/examples/kubernetes/create an admission webhook/Dockerfile b/examples/kubernetes/create an admission webhook/Dockerfile new file mode 100644 index 0000000..a1bdbc0 --- /dev/null +++ b/examples/kubernetes/create an admission webhook/Dockerfile @@ -0,0 +1,8 @@ +FROM python:alpine + +RUN pip3 --no-cache-dir install flask jsonpatch +ADD webhook.py / + +USER nobody +EXPOSE 8080 +ENTRYPOINT python3 /webhook.py diff --git a/examples/kubernetes/create an admission webhook/README.md b/examples/kubernetes/create an admission webhook/README.md new file mode 100644 index 0000000..f4b58a1 --- /dev/null +++ b/examples/kubernetes/create an admission webhook/README.md @@ -0,0 +1,263 @@ +# Create an admission webhook + +The example below will create a webhook which acts as both a `ValidatingAdmissionWebhook` and a `MutatingAdmissionWebhook`, but a real world one can act as only one of them. Or more. Your choice. +The procedure is executed in a `minikube` cluster, and will use a self signed certificate for the webhook connection. + +> Be aware of the pros and cons of an `AdmissionWebhook` before deploying one: +> +> - when deploying the resources it validates it will **need to be up and running**, or those resources will be rejected +> - it will need to manage exception to avoid downtime + +## Table of content + +1. [Concepts reminder](#concepts-reminder) +1. [Check the webhook controllers are enabled](#check-the-webhook-controllers-are-enabled) +1. [Write the webhook](#write-the-webhook) + 1. [Create a certificate](#create-a-certificate) +1. [Configure the webhook](#configure-the-webhook) +1. [Troubleshooting](#troubleshooting) + 1. [I cannot deploy the resources because the webhook cannot be reached](#i-cannot-deploy-the-resources-because-the-webhook-cannot-be-reached) +1. [Further readings](#further-readings) +1. [Sources](#sources) + +## Concepts reminder + +There are 2 special admission controllers in the list included in the Kubernetes `apiserver`: + +- `ValidatingAdmissionWebhook`s, which can reject a request but cannot modify the object they are receiving in the admission request, and +- `MutatingAdmissionWebhook`s, which can modify objects by creating a patch that will be sent back in the admission response + +These send admission requests to external HTTP callbacks and receive admission responses. If these two controllers are enabled, a Kubernetes administrator can create and configure an admission webhook in the cluster. + +To do this: + +1. [check if the admission webhook controllers are enabled](#check-the-webhook-controllers-are-enabled) in the cluster, and configure them if needed +1. [write the HTTP callback](#write-the-webhook) that will handle an admission requests; this can be a simple HTTP server that's deployed to the cluster, or even a serverless function just like in [Kelsey's validating webhook demo] +1. [configure the webhook](#configure-the-webhook) through the `ValidatingWebhookConfiguration` and/or `MutatingWebhookConfiguration` resources + +## Check the webhook controllers are enabled + +Check: + +1. if the `MutatingAdmissionWebhook` and `ValidatingAdmissionWebhook` admission controllers are listed in the correct order in the `admission-control` flag of `kube-apiserver`: + + ```sh + $ kubectl get pods --namespace 'kube-system' 'kube-apiserver-minikube' -o 'yaml' \ + | yq -y '.spec.containers[] + | select(.name == "kube-apiserver") + | .command' \ + - \ + | grep -e '--enable-admission-plugins' \ + | tr ',' '\n' \ + | grep 'AdmissionWebhook' + MutatingAdmissionWebhook + ValidatingAdmissionWebhook + ``` + +1. if the admission registration API is enabled in your cluster by running the following: + + ```sh + $ kubectl api-versions | grep 'admissionregistration.k8s.io' + admissionregistration.k8s.io/v1 + admissionregistration.k8s.io/v1beta1 + ``` + +## Write the webhook + +Every language is fine as long as the webhook: + +- accepts an [admission request]; +- spits out an [admission response]; +- uses a certificate to secure the connection, as all admission webhooks need to be on SSL; a self-signed certificate will be more than fine for testing + +Example: [webhook.py] + +After the webhook's creation: + +1. create a containerized image of the webhook and save it to the registry + Dockerfile: [Dockerfile] + + ```sh + $ docker build -t webhook . + + # on minikube + # will also need imagePullPolicy=Never in the container's spec + $ minikube cache add webhook + ``` + +1. (if needed, see [below](#create-a-certificate)) to generate the self signed CA, a `CertificateSigningRequest` and the certificate, then create a `Secret` based on this certificate; +1. create a `Deployment` that will use the image created above; the service **must** be secured via SSL, so mount the secret created from the previous step as volumes in it + + ```yaml + apiVersion: apps/v1 + kind: Deployment + metadata: + name: webhook + namespace: test + labels: + app: webhook + spec: + selector: + matchLabels: + app: webhook + template: + metadata: + namespace: test + labels: + app: webhook + spec: + containers: + - name: webhook + image: webhook + imagePullPolicy: Never # needed by minikube + ports: + - containerPort: 8443 + volumeMounts: + - mountPath: /cert + name: cert + volumes: + - name: cert + secret: + secretName: webhook + ``` + +1. create a `Service` pointing to the correct ports in same namespace as the `Deployment` + + ```yaml + apiVersion: v1 + kind: Service + metadata: + name: webhook + namespace: test + spec: + selector: + app: webhook + ports: + - protocol: TCP + port: 443 + targetPort: 8443 + ``` + +### Create a certificate + +For testing purposes, let's just reuse the [script][certificate script] originally written by the Istio team to generate a certificate signing request: + +```sh +cd /tmp +curl --continue-at - --remote-name https://raw.githubusercontent.com/istio/istio/release-0.7/install/kubernetes/webhook-create-signed-cert.sh +bash /tmp/webhook-create-signed-cert.sh --service webhook --namespace test --secret webhook +cd - +``` + +## Configure the webhook + +Create a `ValidatingWebhookConfiguration` to register the service for validation upon pod creation: + +```yaml +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook + namespace: test +webhooks: + - name: webhook.example.com + failurePolicy: Fail + clientConfig: + service: + name: webhook + namespace: test + path: /validate + rules: + - apiGroups: [""] + resources: + - "pods" + apiVersions: + - "*" + operations: + - CREATE +``` + +At the same way, create a `MutatingWebhookConfiguration` to register the service for mutation upon pod creation: + +```yaml +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook + namespace: test + labels: + component: mutating-controller +webhooks: + - name: webhook.example.com + failurePolicy: Fail + clientConfig: + service: + name: webhook + namespace: test + path: /mutate + rules: + - apiGroups: [""] + resources: + - "pods" + apiVersions: + - "*" + operations: + - CREATE +``` + +## Troubleshooting + +### I cannot deploy the resources because the webhook cannot be reached + +**Solution:** remove the AdmissionWebhook and the Service forwarding to it, then reapply its definition + +## Further readings + +- [Admission request] +- [Admission response] +- This example's [Dockerfile] +- This example's [webhook source][webhook.py] +- This example's [resources] +- This example's [cert script] +- [Extensible admission controllers] + +## Sources + +All the references in the [further readings] section, plus the following: + +- [Creating your own admission controller] +- [Diving into Kubernetes mutatingAdmissionWebhook] +- [how to write validating and mutating admission controller webhooks in python for kubernetes] +- [building a kubernetes mutating admission webhook] +- [K8S admission webhooks] +- [How to Master Admission Webhooks In Kubernetes] +- [openshift's generic admission server] +- [kelsey's validating webhook demo] +- [morvencao's kube-mutating-webhook-tutorial] +- [writing a very basic kubernetes mutating admission webhook] +- Istio's [script][certificate script] to generate a certificate signing request + + +[admission request]: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#request +[admission response]: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#response +[cert script]: create-signed-cert.sh +[certificate script]: https://raw.githubusercontent.com/istio/istio/release-0.7/install/kubernetes/webhook-create-signed-cert.sh +[dockerfile]: Dockerfile +[extensible admission controllers]: https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/ +[resources]: resources.yaml +[webhook.py]: webhook.py + + +[further readings]: #further-readings + + +[building a kubernetes mutating admission webhook]: https://didil.medium.com/building-a-kubernetes-mutating-admission-webhook-7e48729523ed +[creating your own admission controller]: https://docs.giantswarm.io/guides/creating-your-own-admission-controller/ +[diving into kubernetes mutatingadmissionwebhook]: https://medium.com/ibm-cloud/diving-into-kubernetes-mutatingadmissionwebhook-6ef3c5695f74 +[how to master admission webhooks in kubernetes]: https://digizoo.com.au/1376/mastering-admission-webhooks-in-kubernetes-gke-part-1/ +[how to write validating and mutating admission controller webhooks in python for kubernetes]: https://medium.com/analytics-vidhya/how-to-write-validating-and-mutating-admission-controller-webhooks-in-python-for-kubernetes-1e27862cb798 +[k8s admission webhooks]: https://banzaicloud.com/blog/k8s-admission-webhooks/ +[kelsey's validating webhook demo]: https://github.com/kelseyhightower/denyenv-validating-admission-webhook +[morvencao's kube-mutating-webhook-tutorial]: https://github.com/morvencao/kube-mutating-webhook-tutorial +[openshift's generic admission server]: https://github.com/openshift/generic-admission-server +[writing a very basic kubernetes mutating admission webhook]: https://medium.com/ovni/writing-a-very-basic-kubernetes-mutating-admission-webhook-398dbbcb63ec diff --git a/examples/kubernetes/create an admission webhook/create-signed-cert.sh b/examples/kubernetes/create an admission webhook/create-signed-cert.sh new file mode 100755 index 0000000..a9fc473 --- /dev/null +++ b/examples/kubernetes/create an admission webhook/create-signed-cert.sh @@ -0,0 +1,131 @@ +#!/bin/bash + +set -e + +usage() { + cat <> "${tmpdir}/csr.conf" +[req] +req_extensions = v3_req +distinguished_name = req_distinguished_name +[req_distinguished_name] +[ v3_req ] +basicConstraints = CA:FALSE +keyUsage = nonRepudiation, digitalSignature, keyEncipherment +extendedKeyUsage = serverAuth +subjectAltName = @alt_names +[alt_names] +DNS.1 = ${service} +DNS.2 = ${service}.${namespace} +DNS.3 = ${service}.${namespace}.svc +EOF + +openssl genrsa -out "${tmpdir}/server-key.pem" 2048 +openssl req -new -key "${tmpdir}/server-key.pem" -subj "/CN=${service}.${namespace}.svc" -out ${tmpdir}/server.csr -config ${tmpdir}/csr.conf + +# clean-up any previously created CSR for our service. Ignore errors if not present. +kubectl delete csr "${csrName}" 2>/dev/null || true + +# create server cert/key CSR and send to k8s API +cat <&2 + echo "See https://istio.io/docs/setup/kubernetes/sidecar-injection.html for more details on troubleshooting." >&2 + exit 1 +fi +echo "${serverCert}" | openssl base64 -d -A -out "${tmpdir}/server-cert.pem" + + +# create the secret with CA cert and server cert/key +kubectl create secret generic "${secret}" \ + --from-file="key.pem=${tmpdir}/server-key.pem" \ + --from-file="cert.pem=${tmpdir}/server-cert.pem" \ + --dry-run -o 'yaml' | + kubectl -n "${namespace}" apply -f - diff --git a/examples/kubernetes/create an admission webhook/resources.yaml b/examples/kubernetes/create an admission webhook/resources.yaml new file mode 100644 index 0000000..c264230 --- /dev/null +++ b/examples/kubernetes/create an admission webhook/resources.yaml @@ -0,0 +1,89 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webhook + namespace: test + labels: + app: webhook +spec: + selector: + matchLabels: + app: webhook + template: + metadata: + namespace: test + labels: + app: webhook + spec: + containers: + - name: webhook + image: webhook + imagePullPolicy: Never # only needed locally + ports: + - containerPort: 8443 + volumeMounts: + - mountPath: /cert + name: cert + volumes: + - name: cert + secret: + secretName: webhook +--- +apiVersion: v1 +kind: Service +metadata: + name: webhook + namespace: test +spec: + selector: + app: webhook + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: webhook-validate + namespace: test +webhooks: + - name: test.example.com + failurePolicy: Fail + clientConfig: + service: + name: webhook + namespace: test + path: /validate + rules: + - apiGroups: [""] + resources: + - "pods" + apiVersions: + - "*" + operations: + - CREATE +--- +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: MutatingWebhookConfiguration +metadata: + name: webhook-mutate + namespace: test + labels: + component: mutating-controller +webhooks: + - name: test.example.com + failurePolicy: Fail + clientConfig: + service: + name: webhook + namespace: test + path: /mutate + rules: + - apiGroups: [""] + resources: + - "pods" + apiVersions: + - "*" + operations: + - CREATE diff --git a/examples/kubernetes/create an admission webhook/webhook.py b/examples/kubernetes/create an admission webhook/webhook.py new file mode 100644 index 0000000..7804dcf --- /dev/null +++ b/examples/kubernetes/create an admission webhook/webhook.py @@ -0,0 +1,64 @@ +#!/usr/bin/env python3 + +from base64 import b64encode +from flask import Flask, request, jsonify +from jsonpatch import JsonPatch + +admission_webhook = Flask(__name__) + +labels = [ + "app.kubernetes.io/component", + "app.kubernetes.io/name", + "app.kubernetes.io/part-of", + "app.kubernetes.io/version" +] + +@admission_webhook.route('/validate', methods=['POST']) +def validate(): + info = request.get_json() + for label in labels: + try: + info["request"]["object"]["metadata"]["labels"][label] + except: + return validation_response(info["request"]["uid"], False, f"missing required label '{label}'") + return validation_response(info["request"]["uid"], True, "request validated successfully") +def validation_response(uid, allowed, message): + return jsonify({ + "response": { + "allowed": allowed, + "status": { + "message": message + }, + "uid": uid + } + }) + +@admission_webhook.route('/mutate', methods=['POST']) +def mutate(): + info = request.get_json() + return mutation_response( + info["request"]["uid"], + True, + "adding label 'revised'", + json_patch = JsonPatch([{ + "op": "add", + "path": "/metadata/labels/revised", + "value": "yes" + }]) + ) +def mutation_response(uid, allowed, message, json_patch): + base64_patch = b64encode(json_patch.to_string().encode("utf-8")).decode("utf-8") + return jsonify({ + "response": { + "allowed": allowed, + "status": { + "message": message + }, + "patchType": "JSONPatch", + "patch": base64_patch, + "uid": uid + } + }) + +if __name__ == '__main__': + admission_webhook.run(host='0.0.0.0', port=8443, ssl_context=("/cert/cert.pem", "/cert/key.pem")) diff --git a/knowledge base/golang.md b/knowledge base/golang.md new file mode 100644 index 0000000..a9be07f --- /dev/null +++ b/knowledge base/golang.md @@ -0,0 +1,43 @@ +# Golang + +## Table of contents + +1. [Inspect a variable](#inspect-a-variable) +1. [JSON data](#json-data) +1. [Create a container image of a Go app](#create-a-container-image-of-a-go-app) +1. [Further readings](#further-readings) +1. [Sources](#sources) + +## Inspect a variable + +See [how to print struct variables in console] + +## JSON data + +See [deserializing json in go a tutorial] + +## Create a container image of a Go app + +See [building minimal docker containers for go applications] and [create the smallest and secured golang docker image based on scratch] + +## Further readings + +- [Building minimal Docker containers for Go applications] +- [Create the smallest and secured Golang Docker image based on Scratch] +- [Deserializing JSON in Go a tutorial] +- [How to print struct variables in console] + +## Sources + +All the references in the [further readings] section, plus the following: + + + + +[further readings]: #further-readings + + +[building minimal docker containers for go applications]: https://www.cloudbees.com/blog/building-minimal-docker-containers-for-go-applications/ +[create the smallest and secured golang docker image based on scratch]: https://medium.com/@chemidy/create-the-smallest-and-secured-golang-docker-image-based-on-scratch-4752223b7324 +[deserializing json in go a tutorial]: https://medium.com/@fsufitch/deserializing-json-in-go-a-tutorial-d042412958ea +[how to print struct variables in console]: https://stackoverflow.com/questions/24512112/how-to-print-struct-variables-in-console#24512194