From 7598823bf95946d0bd8eea19b1257808242f567e Mon Sep 17 00:00:00 2001 From: Edward Viaene Date: Wed, 20 Jan 2021 13:47:42 +0100 Subject: [PATCH] mutatingwebhook (#23) * mutatingwebhook --- mutatingwebhook/Dockerfile | 27 ++++++ mutatingwebhook/README.md | 23 +++++ mutatingwebhook/go.mod | 3 + mutatingwebhook/main.go | 167 +++++++++++++++++++++++++++++++++++ mutatingwebhook/pod.yaml | 13 +++ mutatingwebhook/server.go | 71 +++++++++++++++ mutatingwebhook/types.go | 79 +++++++++++++++++ mutatingwebhook/webhook.yaml | 72 +++++++++++++++ 8 files changed, 455 insertions(+) create mode 100644 mutatingwebhook/Dockerfile create mode 100644 mutatingwebhook/README.md create mode 100644 mutatingwebhook/go.mod create mode 100644 mutatingwebhook/main.go create mode 100644 mutatingwebhook/pod.yaml create mode 100644 mutatingwebhook/server.go create mode 100644 mutatingwebhook/types.go create mode 100644 mutatingwebhook/webhook.yaml diff --git a/mutatingwebhook/Dockerfile b/mutatingwebhook/Dockerfile new file mode 100644 index 0000000..ce41a62 --- /dev/null +++ b/mutatingwebhook/Dockerfile @@ -0,0 +1,27 @@ +# +# Build go project +# +FROM golang:1.15-alpine as go-builder + +WORKDIR /go/src/github.com/wardviaene/kubernetes-course/mutatingwebhook + +COPY . . + +RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o mutatingwebhook *.go + +# +# Runtime container +# +FROM alpine:latest + +RUN mkdir -p /app && \ + addgroup -S app && adduser -S app -G app && \ + chown app:app /app + +WORKDIR /app + +COPY --from=go-builder /go/src/github.com/wardviaene/kubernetes-course/mutatingwebhook . + +USER app + +CMD ["./mutatingwebhook"] diff --git a/mutatingwebhook/README.md b/mutatingwebhook/README.md new file mode 100644 index 0000000..623b2a3 --- /dev/null +++ b/mutatingwebhook/README.md @@ -0,0 +1,23 @@ +# Mutating Webhook example + +## Setup the webhook + +``` +kubectl apply -f webhook.yaml +``` + +## Set up the CA Certificate +Once the webhook runs (give it a few seconds to initialize), the CA certificate can be downloaded by executing a curl command within the container. To retrieve the base64 encoded version of this ca.pem, use the following command: +``` +kubectl exec -it -n mutatingwebhook $(kubectl get pods --no-headers -o custom-columns=":metadata.name" -n mutatingwebhook) -- wget -q -O- localhost:8080/ca.pem?base64 +``` + +The output of this command should replace the base64 string in caBundle in webhook.yaml: +``` + caBundle: "cGxhY2Vob2xkZXIK" # <= replace this string within quotes +``` + +Then reapply the webhook using: +``` +kubectl apply -f webhook.yaml +``` diff --git a/mutatingwebhook/go.mod b/mutatingwebhook/go.mod new file mode 100644 index 0000000..e9398a3 --- /dev/null +++ b/mutatingwebhook/go.mod @@ -0,0 +1,3 @@ +module github.com/wardviaene/kubernetes-course/mutatingwebhook + +go 1.15 diff --git a/mutatingwebhook/main.go b/mutatingwebhook/main.go new file mode 100644 index 0000000..48c1f64 --- /dev/null +++ b/mutatingwebhook/main.go @@ -0,0 +1,167 @@ +package main + +import ( + "bytes" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "fmt" + "log" + "math/big" + "net" + "net/http" + "time" +) + +func main() { + + // setup certs + fmt.Printf("Initializing certificates...\n") + serverTLSConf, clientTLSConf, caPEM, err := certsetup() + if err != nil { + panic(err) + } + + s := Server{ + ServerTLSConf: serverTLSConf, + ClientTLSConf: clientTLSConf, + CaPEM: caPEM, + } + + go func() { + handler := http.NewServeMux() + + handler.HandleFunc("/ca.pem", s.getCA) + fmt.Printf("Starting localhost http server on :8080 with ca.pem endpoint\n") + err = http.ListenAndServe("localhost:8080", handler) + + if err != nil { + log.Fatal(err) + } + }() + + // start TLS server + fmt.Printf("Starting TLS server on :8443\n") + handler := http.NewServeMux() + handler.HandleFunc("/webhook", s.postWebhook) + + https := &http.Server{ + Addr: ":8443", + TLSConfig: serverTLSConf, + Handler: handler, + } + + log.Fatal(https.ListenAndServeTLS("", "")) + +} + +// certsetup adapted from https://gist.github.com/shaneutt/5e1995295cff6721c89a71d13a71c251 +func certsetup() (serverTLSConf *tls.Config, clientTLSConf *tls.Config, caPEMBytes []byte, err error) { + // set up our CA certificate + ca := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + IsCA: true, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + BasicConstraintsValid: true, + } + + // create our private and public key + caPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, []byte{}, err + } + + // create the CA + caBytes, err := x509.CreateCertificate(rand.Reader, ca, ca, &caPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, nil, []byte{}, err + } + + // pem encode + caPEM := new(bytes.Buffer) + pem.Encode(caPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: caBytes, + }) + + caPrivKeyPEM := new(bytes.Buffer) + pem.Encode(caPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(caPrivKey), + }) + + // set up our server certificate + cert := &x509.Certificate{ + SerialNumber: big.NewInt(2019), + Subject: pkix.Name{ + Organization: []string{"Company, INC."}, + Country: []string{"US"}, + Province: []string{""}, + Locality: []string{"San Francisco"}, + StreetAddress: []string{"Golden Gate Bridge"}, + PostalCode: []string{"94016"}, + }, + IPAddresses: []net.IP{net.IPv4(127, 0, 0, 1), net.IPv6loopback}, + NotBefore: time.Now(), + NotAfter: time.Now().AddDate(10, 0, 0), + SubjectKeyId: []byte{1, 2, 3, 4, 6}, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}, + KeyUsage: x509.KeyUsageDigitalSignature, + DNSNames: []string{"mutatingwebhook.mutatingwebhook.svc"}, + } + + certPrivKey, err := rsa.GenerateKey(rand.Reader, 4096) + if err != nil { + return nil, nil, []byte{}, err + } + + certBytes, err := x509.CreateCertificate(rand.Reader, cert, ca, &certPrivKey.PublicKey, caPrivKey) + if err != nil { + return nil, nil, []byte{}, err + } + + certPEM := new(bytes.Buffer) + pem.Encode(certPEM, &pem.Block{ + Type: "CERTIFICATE", + Bytes: certBytes, + }) + + certPrivKeyPEM := new(bytes.Buffer) + pem.Encode(certPrivKeyPEM, &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(certPrivKey), + }) + + serverCert, err := tls.X509KeyPair(certPEM.Bytes(), certPrivKeyPEM.Bytes()) + if err != nil { + return nil, nil, []byte{}, err + } + + serverTLSConf = &tls.Config{ + Certificates: []tls.Certificate{serverCert}, + } + + certpool := x509.NewCertPool() + certpool.AppendCertsFromPEM(caPEM.Bytes()) + clientTLSConf = &tls.Config{ + RootCAs: certpool, + } + + caPEMBytes = caPEM.Bytes() + + return +} diff --git a/mutatingwebhook/pod.yaml b/mutatingwebhook/pod.yaml new file mode 100644 index 0000000..ae2cd39 --- /dev/null +++ b/mutatingwebhook/pod.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: Pod +metadata: + namespace: testmutatingwebhook + name: ubuntu + labels: + app: ubuntu +spec: + containers: + - name: ubuntu + image: ubuntu:latest + command: ["/bin/sleep", "1d"] + imagePullPolicy: IfNotPresent diff --git a/mutatingwebhook/server.go b/mutatingwebhook/server.go new file mode 100644 index 0000000..7077ddd --- /dev/null +++ b/mutatingwebhook/server.go @@ -0,0 +1,71 @@ +package main + +import ( + "crypto/tls" + "encoding/base64" + "encoding/json" + "fmt" + "net/http" +) + +//Server contains the functions handling server requests +type Server struct { + ServerTLSConf *tls.Config + ClientTLSConf *tls.Config + CaPEM []byte +} + +func (s Server) getCA(w http.ResponseWriter, req *http.Request) { + if len(s.CaPEM) == 0 { + fmt.Fprintf(w, "No certificate found\n") + return + } + + // if base64 parameter is set, return in base64 format + req.ParseForm() + if _, hasParam := req.Form["base64"]; hasParam { + fmt.Fprintf(w, string(base64.StdEncoding.EncodeToString(s.CaPEM))) + return + } + + fmt.Fprintf(w, string(s.CaPEM)) +} + +func (s Server) postWebhook(w http.ResponseWriter, r *http.Request) { + var request AdmissionReviewRequest + err := json.NewDecoder(r.Body).Decode(&request) + if err != nil { + http.Error(w, fmt.Sprintf("JSON body in invalid format: %s\n", err.Error()), http.StatusBadRequest) + return + } + if request.APIVersion != "admission.k8s.io/v1" || request.Kind != "AdmissionReview" { + http.Error(w, fmt.Sprintf("wrong APIVersion or kind: %s - %s", request.APIVersion, request.Kind), http.StatusBadRequest) + return + + } + fmt.Printf("debug: %+v\n", request.Request) + response := AdmissionReviewResponse{ + APIVersion: "admission.k8s.io/v1", + Kind: "AdmissionReview", + Response: Response{ + UID: request.Request.UID, + Allowed: true, + }, + } + + // add label if we're creating a pod + if request.Request.Kind.Group == "" && request.Request.Kind.Version == "v1" && request.Request.Kind.Kind == "Pod" && request.Request.Operation == "CREATE" { + patch := `[{"op": "add", "path": "/metadata/labels/myExtraLabel", "value": "webhook-was-here"}]` + patchEnc := base64.StdEncoding.EncodeToString([]byte(patch)) + response.Response.PatchType = "JSONPatch" + response.Response.Patch = patchEnc + } + + out, err := json.Marshal(response) + if err != nil { + http.Error(w, fmt.Sprintf("JSON output marshal error: %s\n", err.Error()), http.StatusBadRequest) + return + } + fmt.Printf("Got request, response: %s\n", string(out)) + fmt.Fprintln(w, string(out)) +} diff --git a/mutatingwebhook/types.go b/mutatingwebhook/types.go new file mode 100644 index 0000000..e960aa8 --- /dev/null +++ b/mutatingwebhook/types.go @@ -0,0 +1,79 @@ +package main + +//AdmissionReviewRespons for replies to incoming webhooks +type AdmissionReviewResponse struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Response Response `json:"response"` +} +type Response struct { + UID string `json:"uid"` + Allowed bool `json:"allowed"` + Patch string `json:"patch,omitempty"` + PatchType string `json:"patchType,omitempty"` +} + +//AdmissionReviewRequest for incoming webhooks +type AdmissionReviewRequest struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` + Request Request `json:"request"` +} +type Kind struct { + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` +} +type Resource struct { + Group string `json:"group"` + Version string `json:"version"` + Resource string `json:"resource"` +} +type RequestKind struct { + Group string `json:"group"` + Version string `json:"version"` + Kind string `json:"kind"` +} +type RequestResource struct { + Group string `json:"group"` + Version string `json:"version"` + Resource string `json:"resource"` +} +type Extra struct { + SomeKey []string `json:"some-key"` +} +type UserInfo struct { + Username string `json:"username"` + UID string `json:"uid"` + Groups []string `json:"groups"` + Extra Extra `json:"extra"` +} +type Object struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` +} +type OldObject struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` +} +type Options struct { + APIVersion string `json:"apiVersion"` + Kind string `json:"kind"` +} +type Request struct { + UID string `json:"uid"` + Kind Kind `json:"kind"` + Resource Resource `json:"resource"` + SubResource string `json:"subResource"` + RequestKind RequestKind `json:"requestKind"` + RequestResource RequestResource `json:"requestResource"` + RequestSubResource string `json:"requestSubResource"` + Name string `json:"name"` + Namespace string `json:"namespace"` + Operation string `json:"operation"` + UserInfo UserInfo `json:"userInfo"` + Object Object `json:"object"` + OldObject OldObject `json:"oldObject"` + Options Options `json:"options"` + DryRun bool `json:"dryRun"` +} diff --git a/mutatingwebhook/webhook.yaml b/mutatingwebhook/webhook.yaml new file mode 100644 index 0000000..ed746b1 --- /dev/null +++ b/mutatingwebhook/webhook.yaml @@ -0,0 +1,72 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: "mymutatingwebhook.example.com" +webhooks: +- name: "mymutatingwebhook.example.com" + namespaceSelector: + matchLabels: + webhook: enabled + rules: + - apiGroups: [""] + apiVersions: ["v1"] + operations: ["CREATE"] + resources: ["pods"] + clientConfig: + service: + namespace: "mutatingwebhook" + name: "mutatingwebhook" + path: "/webhook" + caBundle: "cGxhY2Vob2xkZXIK" + admissionReviewVersions: ["v1", "v1beta1"] + sideEffects: None + timeoutSeconds: 5 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: mutatingwebhook +--- +apiVersion: v1 +kind: Namespace +metadata: + name: testmutatingwebhook + labels: + webhook: enabled +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: mutatingwebhook + namespace: mutatingwebhook + labels: + app: mutatingwebhook +spec: + replicas: 1 + selector: + matchLabels: + app: mutatingwebhook + template: + metadata: + labels: + app: mutatingwebhook + spec: + containers: + - name: mutatingwebhook + imagePullPolicy: Always + image: wardviaene/mutatingwebhook-example + ports: + - containerPort: 8443 +--- +apiVersion: v1 +kind: Service +metadata: + name: mutatingwebhook + namespace: mutatingwebhook +spec: + selector: + app: mutatingwebhook + ports: + - protocol: TCP + port: 443 + targetPort: 8443 \ No newline at end of file