diff --git a/.gitignore b/.gitignore index 951fdc2..89f5237 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,3 @@ /.idea/ -webhook.yaml -deployment.yaml \ No newline at end of file +k8s/manifests/webhook.yaml +k8s/manifests/deployment.yaml \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index e4f4bbf..dd231e0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ FROM node:18.16.0-bullseye-slim WORKDIR /workdir -COPY kac-api/package*.json ./ +COPY admission-webhook/package*.json ./ RUN npm ci --ignore-scripts -COPY ./kac-api/src ./src -COPY ./kac-api/tsconfig.json ./ +COPY ./admission-webhook/src ./src +COPY ./admission-webhook/tsconfig.json ./ CMD ["npm", "run", "prod"] EXPOSE 8443 diff --git a/README.md b/README.md index f5a2314..f1d4bc0 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,19 @@ -# K8s Admission Controller Demo +# Monokle Admission Controller -This was heavily inspired by https://github.com/stackrox/admission-controller-webhook-demo. +Monokle Admission Controller is an admission controller for validating resources in the cluster. -And since it is hackaton PoC there was massive amount of duct tape applied in some places ;) +## Development -## Prerequisites +### Prerequisites * Minikube (or any other K8s cluster running) * kubectl * Skaffold * nodejs -## Running +### Running -### Minikube +#### Minikube Start Minikube: @@ -21,58 +21,111 @@ Start Minikube: minikube start --uuid 00000000-0000-0000-0000-000000000001 --extra-config=apiserver.enable-admission-plugins=ValidatingAdmissionWebhook ``` -Every resource will be deployed to `webhook-demo` namespace, to watch it you can run: +#### Deploying ```bash -watch kubectl -n webhook-demo get all,ValidatingWebhookConfiguration,MutatingWebhookConfiguration +./scripts/deploy.sh ``` -### Deploying +Namespaced resources (webhook server) will be deployed to dedicated `monokle-admission-controller` namespace, to watch it you can run: ```bash -./deploy.sh +watch kubectl -n monokle-admission-controller get all,CustomResourceDefinition,ValidatingWebhookConfiguration,MutatingWebhookConfiguration ``` After it runs, the result should be something like: ```bash -NAME READY STATUS RESTARTS AGE -pod/webhook-server-55dd5d6f44-lwwnw 1/1 Running 0 11s +NAME READY STATUS RESTARTS AGE +pod/webhook-server-7cd54c9fcf-wdkdn 0/1 Error 1 (13s ago) 25s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE -service/webhook-server ClusterIP 10.96.18.123 443/TCP 11s +service/webhook-server ClusterIP 10.97.66.194 443/TCP 25s NAME READY UP-TO-DATE AVAILABLE AGE -deployment.apps/webhook-server 1/1 1 1 11s +deployment.apps/webhook-server 0/1 1 0 25s NAME DESIRED CURRENT READY AGE -replicaset.apps/webhook-server-55dd5d6f44 1 1 1 11s +replicaset.apps/webhook-server-7cd54c9fcf 1 1 0 25s + +NAME CREATED AT +customresourcedefinition.apiextensions.k8s.io/policies.monokle.com 2023-10-02T12:17:02Z +customresourcedefinition.apiextensions.k8s.io/policybindings.monokle.com 2023-10-02T12:17:02Z NAME WEBHOOKS AGE -validatingwebhookconfiguration.admissionregistration.k8s.io/demo-webhook 1 6s +validatingwebhookconfiguration.admissionregistration.k8s.io/demo-webhook 1 17s +``` + +For getting info about CRDs: + +```bash +kubectl get crd +kubectl describe crd policies.monokle.com +kubectl describe crd policybindings.monokle.com +``` + +#### Testing + +First you need to create policy resource, for example: + +```bash +kubectl apply -f examples/policy-sample-1.yaml +kubectl apply -f examples/policy-sample-2.yaml +``` + +Then it needs to be bind to be used for validation. Either without scope (globally to all, but ignored namespaces) or with `matchResource` field: + +```bash +kubectl apply -f examples/policy-binding-sample-1.yaml +kubectl apply -f examples/policy-binding-sample-2.yaml +kubectl apply -f examples/policy-binding-sample-3.yaml ``` -### Testing +You can inspect deployed policies with: -You can try to create sample resource and see webhook response: +```bash +kubectl get policy +kubectl describe policy + +kubectl get policybinding +kubectl describe policybinding +``` + +Then you can try to create sample resource and see webhook response: ```bash -kubectl -n webhook-demo create -f examples/pod-with-conflict.yaml +kubectl apply -f examples/pod-valid.yaml +kubectl apply -f examples/pod-warning.yaml +kubectl apply -f examples/pod-errors.yaml ``` -### Iterating +#### Iterating After everything is running you can allow hot-reload by running: ```bash -skaffold dev +skaffold dev -f k8s/skaffold.yaml ``` **Important**: Skaffold will recreate deployment on every change so make sure that webhook pod doesn't get rejected by it's previous version (via Validation Admission Controller). -## Refs +You can also do manual clean-up and re-run `./deploy.sh` script again: + +```bash +kubectl delete all -n monokle-admission-controller --all && \ +kubectl delete validatingwebhookconfiguration.admissionregistration.k8s.io/demo-webhook -n monokle-admission-controller && \ +kubectl delete namespace monokle-admission-controller && \ +kubectl delete namespace nstest1 && \ +kubectl delete namespace nstest2 && \ +kubectl delete crd policies.monokle.com && \ +kubectl delete crd policybindings.monokle.com +``` + +### Refs * https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/ * https://github.com/stackrox/admission-controller-webhook-demo/tree/master * https://www.witodelnat.eu/blog/2021/local-kubernetes-development -* https://minikube.sigs.k8s.io/docs/tutorials/using_psp/ \ No newline at end of file +* https://minikube.sigs.k8s.io/docs/tutorials/using_psp/ +* https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/ +* https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/ diff --git a/README.old.md b/README.old.md deleted file mode 100644 index ed85948..0000000 --- a/README.old.md +++ /dev/null @@ -1,98 +0,0 @@ -# Kubernetes Admission Controller Webhook Demo - -This repository contains a small HTTP server that can be used as a Kubernetes -[MutatingAdmissionWebhook](https://kubernetes.io/docs/admin/admission-controllers/#mutatingadmissionwebhook-beta-in-19). - -The logic of this demo webhook is fairly simple: it enforces more secure defaults for running -containers as non-root user. While it is still possible to run containers as root, the webhook -ensures that this is only possible if the setting `runAsNonRoot` is *explicitly* set to `false` -in the `securityContext` of the Pod. If no value is set for `runAsNonRoot`, a default of `true` -is applied, and the user ID defaults to `1234`. - -## Prerequisites - -A cluster on which this example can be tested must be running Kubernetes 1.9.0 or above, -with the `admissionregistration.k8s.io/v1beta1` API enabled. You can verify that by observing that the -following command produces a non-empty output: -``` -kubectl api-versions | grep admissionregistration.k8s.io/v1beta1 -``` -In addition, the `MutatingAdmissionWebhook` admission controller should be added and listed in the admission-control -flag of `kube-apiserver`. - -For building the image, [GNU make](https://www.gnu.org/software/make/) and [Go](https://golang.org) are required. - -## Deploying the Webhook Server - -1. Bring up a Kubernetes cluster satisfying the above prerequisites, and make -sure it is active (i.e., either via the configuration in the default location, or by setting -the `KUBECONFIG` environment variable). -2. Run `./deploy.sh`. This will create a CA, a certificate and private key for the webhook server, -and deploy the resources in the newly created `webhook-demo` namespace in your Kubernetes cluster. - - -## Verify - -1. The `webhook-server` pod in the `webhook-demo` namespace should be running: -``` -$ kubectl -n webhook-demo get pods -NAME READY STATUS RESTARTS AGE -webhook-server-6f976f7bf-hssc9 1/1 Running 0 35m -``` - -2. A `MutatingWebhookConfiguration` named `demo-webhook` should exist: -``` -$ kubectl get mutatingwebhookconfigurations -NAME AGE -demo-webhook 36m -``` - -3. Deploy [a pod](examples/pod-with-defaults.yaml) that neither sets `runAsNonRoot` nor `runAsUser`: -``` -$ kubectl create -f examples/pod-with-defaults.yaml -``` -Verify that the pod has default values in its security context filled in: -``` -$ kubectl get pod/pod-with-defaults -o yaml -... - securityContext: - runAsNonRoot: true - runAsUser: 1234 -... -``` -Also, check the logs that the pod had in fact been running as a non-root user: -``` -$ kubectl logs pod-with-defaults -I am running as user 1234 -``` - -4. Deploy [a pod](examples/pod-with-override.yaml) that explicitly sets `runAsNonRoot` to `false`, allowing it to run as the -`root` user: -``` -$ kubectl create -f examples/pod-with-override.yaml -$ kubectl get pod/pod-with-override -o yaml -... - securityContext: - runAsNonRoot: false -... -$ kubectl logs pod-with-override -I am running as user 0 -``` - -5. Attempt to deploy [a pod](examples/pod-with-conflict.yaml) that has a conflicting setting: `runAsNonRoot` set to `true`, but `runAsUser` set to 0 (root). -The admission controller should block the creation of that pod. -``` -$ kubectl create -f examples/pod-with-conflict.yaml -Error from server (InternalError): error when creating "examples/pod-with-conflict.yaml": Internal error -occurred: admission webhook "webhook-server.webhook-demo.svc" denied the request: runAsNonRoot specified, -but runAsUser set to 0 (the root user) -``` - -## Build the Image from Sources (optional) - -An image can be built by running `make`. -If you want to modify the webhook server for testing purposes, be sure to set and export -the shell environment variable `IMAGE` to an image tag for which you have push access. You can then -build and push the image by running `make push-image`. Also make sure to change the image tag -in `deployment/deployment.yaml.template`, and if necessary, add image pull secrets. - diff --git a/kac-api/.gitignore b/admission-webhook/.gitignore similarity index 100% rename from kac-api/.gitignore rename to admission-webhook/.gitignore diff --git a/kac-api/package-lock.json b/admission-webhook/package-lock.json similarity index 82% rename from kac-api/package-lock.json rename to admission-webhook/package-lock.json index 9eaa45e..603da6c 100644 --- a/kac-api/package-lock.json +++ b/admission-webhook/package-lock.json @@ -1,19 +1,21 @@ { - "name": "kac2", + "name": "admission-webhook", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "kac2", + "name": "admission-webhook", "version": "1.0.0", "license": "ISC", "dependencies": { "@kubernetes/client-node": "^0.18.1", - "@monokle/validation": "^0.16.0", + "@monokle/parser": "0.2.0", + "@monokle/validation": "^0.30.1", "fastify": "^3.28.0", "isomorphic-fetch": "^3.0.0", "node-fetch": "^2.6.6", + "pino": "^8.15.1", "type-fest": "^3.10.0" }, "devDependencies": { @@ -61,15 +63,31 @@ } }, "node_modules/@kubernetes/client-node/node_modules/@types/node": { - "version": "18.16.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.12.tgz", - "integrity": "sha512-tIRrjbY9C277MOfP8M3zjMIhtMlUJ6YVqkGgLjz+74jVsdf4/UjC6Hku4+1N0BS0qyC0JAS6tJLUk9H6JUKviQ==" + "version": "18.17.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.17.19.tgz", + "integrity": "sha512-+pMhShR3Or5GR0/sp4Da7FnhVmTalWm81M6MkEldbwjETSaPalw138Z4KdpQaistvqQxLB7Cy4xwYdxpbSOs9Q==" + }, + "node_modules/@monokle/parser": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@monokle/parser/-/parser-0.2.0.tgz", + "integrity": "sha512-gcCWcfOUqTubwi9mJlrOb4X5NW5B6N4/lCR7oIh5pDL75rQUfuHbSbx+BkymZnrGgNNwGX9LMGukdW8A9Y5TMQ==", + "dependencies": { + "lodash": "4.17.21", + "path-browserify": "^1.0.1", + "yaml": "2.2.2" + } + }, + "node_modules/@monokle/types": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@monokle/types/-/types-0.2.2.tgz", + "integrity": "sha512-GGhl30a1WImxfxWg9Ys7MY1VQi2NU2iCH+gf4kKGZxS6nqAkgU4G1kTTYnxQzGNoM4Zbabh5aRxINsckIV5a+g==" }, "node_modules/@monokle/validation": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/@monokle/validation/-/validation-0.16.0.tgz", - "integrity": "sha512-BSuAFzxffNGObHywTpYStYxTzzFYpXekQ8ciQBUZUdTjFCjBCbPMTXjperORTJY0zIPohUmtsse6jfxVcUnw4A==", + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/@monokle/validation/-/validation-0.30.1.tgz", + "integrity": "sha512-gm8FCGQA4VkbSL6DgEk10tDJTbfhqvc81BJ/oHsTMfArT3hjdksYOiNsHj6gzGEFsHXpGHYFqywD1qD1h0jkIA==", "dependencies": { + "@monokle/types": "*", "@open-policy-agent/opa-wasm": "1.8.0", "@rollup/plugin-virtual": "3.0.1", "ajv": "6.12.6", @@ -77,9 +95,9 @@ "isomorphic-fetch": "3.0.0", "lodash": "4.17.21", "node-fetch": "3.3.0", - "react-fast-compare": "3.2.1", "require-from-string": "2.0.2", "rollup": "3.18.0", + "uuid": "9.0.0", "yaml": "2.2.2", "zod": "3.19.1" } @@ -135,19 +153,19 @@ } }, "node_modules/@types/caseless": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.2.tgz", - "integrity": "sha512-6ckxMjBBD8URvjB6J3NcnuAn5Pkl7t3TizAg+xdlzzQGSPSmBcXf8KoIH0ua/i+tio+ZRUHEXp0HEmvaR4kt0w==" + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.3.tgz", + "integrity": "sha512-ZD/NsIJYq/2RH+hY7lXmstfp/v9djGt9ah+xRQ3pcgR79qiKsG4pLl25AI7IcXxVO8dH9GiBE5rAknC0ePntlw==" }, "node_modules/@types/js-yaml": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz", - "integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==" + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.6.tgz", + "integrity": "sha512-ACTuifTSIIbyksx2HTon3aFtCKWcID7/h3XEmRpDYdMCXxPbl+m9GteOJeaAkiAta/NJaSFuA7ahZ0NkwajDSw==" }, "node_modules/@types/node": { - "version": "20.2.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.2.0.tgz", - "integrity": "sha512-3iD2jaCCziTx04uudpJKwe39QxXgSUnpxXSvRQjRvHPxFQfmfP4NXIm/NURVeNlTCc+ru4WqjYGTmpXrW9uMlw==" + "version": "20.6.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.6.5.tgz", + "integrity": "sha512-2qGq5LAOTh9izcc0+F+dToFigBWiK1phKPt7rNhOqJSr35y8rlIBjDwGtFSgAI6MGIhjwOVNSQZVdJsZJ2uR1w==" }, "node_modules/@types/request": { "version": "2.48.8", @@ -161,18 +179,29 @@ } }, "node_modules/@types/tough-cookie": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.2.tgz", - "integrity": "sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==" + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.3.tgz", + "integrity": "sha512-THo502dA5PzG/sfQH+42Lw3fvmYkceefOspdCwpHRul8ik2Jv1K8I5OZz1AT3/rs46kwgMCe9bSBmDLYkkOMGg==" }, "node_modules/@types/ws": { - "version": "8.5.4", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.4.tgz", - "integrity": "sha512-zdQDHKUgcX/zBc4GrwsE/7dVdAD8JR4EuiAXiiUhhfyIJXXb2+PrGshFyeXWQPMmmZ2XxgaqclgpIC7eTXc1mg==", + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", "dependencies": { "@types/node": "*" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/abstract-logging": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", @@ -261,6 +290,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -278,6 +326,29 @@ "concat-map": "0.0.1" } }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, "node_modules/byline": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz", @@ -445,6 +516,22 @@ "safer-buffer": "^2.1.0" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -459,9 +546,9 @@ ] }, "node_modules/fast-content-type-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.0.0.tgz", - "integrity": "sha512-Xbc4XcysUXcsP5aHUU7Nq3OwvHq97C+WnbkeIefpeYLX+ryzFJlU6OStFJhs6Ol0LkUGpcK+wL0JwfM+FCU5IA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-1.1.0.tgz", + "integrity": "sha512-fBHHqSTFLVnR61C+gltJuE5GkVQMV0S2nqUO8TJ+5Z3qAKG8vAx4FKai1s5jq/inV1+sREynIWSuQ6HgoSXpDQ==" }, "node_modules/fast-decode-uri-component": { "version": "1.0.1", @@ -493,9 +580,9 @@ } }, "node_modules/fast-redact": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.2.0.tgz", - "integrity": "sha512-zaTadChr+NekyzallAMXATXLOR8MNx3zqpZ0MUF2aGf4EathnG0f32VLODNlY8IuGY3HoRO2L6/6fSzNsLaHIw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", "engines": { "node": ">=6" } @@ -528,6 +615,37 @@ "tiny-lru": "^8.0.1" } }, + "node_modules/fastify/node_modules/pino": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", + "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "dependencies": { + "fast-redact": "^3.0.0", + "fast-safe-stringify": "^2.0.8", + "flatstr": "^1.0.12", + "pino-std-serializers": "^3.1.0", + "process-warning": "^1.0.0", + "quick-format-unescaped": "^4.0.3", + "sonic-boom": "^1.0.2" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/fastify/node_modules/pino-std-serializers": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", + "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + }, + "node_modules/fastify/node_modules/sonic-boom": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", + "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "flatstr": "^1.0.12" + } + }, "node_modules/fastq": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", @@ -645,9 +763,9 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "node_modules/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "hasInstallScript": true, "optional": true, "os": [ @@ -728,6 +846,25 @@ "npm": ">=1.3.7" } }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -778,9 +915,9 @@ "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "node_modules/jose": { - "version": "4.14.4", - "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.4.tgz", - "integrity": "sha512-j8GhLiKmUAh+dsFXlX1aJCbt5KMibuKb+d7j1JaOJG6s2UjX1PQlW+OKB/sD4a/5ZYF4RcmYmLSndOoU3Lt/3g==", + "version": "4.14.6", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.14.6.tgz", + "integrity": "sha512-EqJPEUlZD0/CSUMubKtMaYUOtWe91tZXTWMJZoKSbLk+KtdhNdcvppH8lA9XwVu2V4Ailvsj0GBZJ2ZwDjfesQ==", "optional": true, "funding": { "url": "https://github.com/sponsors/panva" @@ -999,14 +1136,22 @@ } }, "node_modules/node-fetch": { - "version": "2.6.6", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.6.tgz", - "integrity": "sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", "dependencies": { "whatwg-url": "^5.0.0" }, "engines": { "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } } }, "node_modules/oauth-sign": { @@ -1035,6 +1180,11 @@ "node": "^10.13.0 || >=12.0.0" } }, + "node_modules/on-exit-leak-free": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.0.tgz", + "integrity": "sha512-VuCaZZAjReZ3vUwgOB8LxAosIurDiAW0s13rI1YwmaP++jvcxP77AWoQvenZebpCA2m8WC1/EosPYPMjnRAp/w==" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -1044,12 +1194,12 @@ } }, "node_modules/openid-client": { - "version": "5.4.2", - "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.4.2.tgz", - "integrity": "sha512-lIhsdPvJ2RneBm3nGBBhQchpe3Uka//xf7WPHTIglery8gnckvW7Bd9IaQzekzXJvWthCMyi/xVEyGW0RFPytw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.5.0.tgz", + "integrity": "sha512-Y7Xl8BgsrkzWLHkVDYuroM67hi96xITyEDSkmWaGUiNX6CkcXC3XyQGdv5aWZ6dukVKBFVQCADi9gCavOmU14w==", "optional": true, "dependencies": { - "jose": "^4.14.1", + "jose": "^4.14.4", "lru-cache": "^6.0.0", "object-hash": "^2.2.0", "oidc-token-hash": "^5.0.3" @@ -1076,6 +1226,11 @@ "tslib": "^2.0.3" } }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==" + }, "node_modules/path-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", @@ -1099,26 +1254,52 @@ "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "node_modules/pino": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-6.14.0.tgz", - "integrity": "sha512-iuhEDel3Z3hF9Jfe44DPXR8l07bhjuFY3GMHIXbjnY9XcafbyDDwl2sN2vw2GjMPf5Nkoe+OFao7ffn9SXaKDg==", + "version": "8.15.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.15.1.tgz", + "integrity": "sha512-Cp4QzUQrvWCRJaQ8Lzv0mJzXVk4z2jlq8JNKMGaixC2Pz5L4l2p95TkuRvYbrEbe85NQsDKrAd4zalf7Ml6WiA==", "dependencies": { - "fast-redact": "^3.0.0", - "fast-safe-stringify": "^2.0.8", - "flatstr": "^1.0.12", - "pino-std-serializers": "^3.1.0", - "process-warning": "^1.0.0", + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", "quick-format-unescaped": "^4.0.3", - "sonic-boom": "^1.0.2" + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.1.0", + "thread-stream": "^2.0.0" }, "bin": { "pino": "bin.js" } }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, "node_modules/pino-std-serializers": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-3.2.0.tgz", - "integrity": "sha512-EqX4pwDPrt3MuOAAUBMU0Tk5kR/YcCM5fNPEzgCO2zJ5HfX0vbiH9HbJglnyeQsN96Kznae6MWD47pZB5avTrg==" + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/pino/node_modules/process-warning": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.2.0.tgz", + "integrity": "sha512-/1WZ8+VQjR6avWOgHeEPd7SDQmFQ1B5mC1eRXsCm5TarlNmx/wCsa5GEaxGm05BORRtyG/Ex/3xq3TuRvq57qg==" + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } }, "node_modules/process-warning": { "version": "1.0.0", @@ -1182,10 +1363,28 @@ "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" }, - "node_modules/react-fast-compare": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.1.tgz", - "integrity": "sha512-xTYf9zFim2pEif/Fw16dBiXpe0hoy5PxcD8+OwBnTtNLfIm3g6WxhKNurY+6OmdH1u6Ta/W/Vl6vjbYP1MFnDg==" + "node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } }, "node_modules/request": { "version": "2.88.2", @@ -1231,6 +1430,15 @@ "node": ">= 0.12" } }, + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "bin": { + "uuid": "bin/uuid" + } + }, "node_modules/require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", @@ -1322,6 +1530,14 @@ "ret": "~0.2.0" } }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", @@ -1333,9 +1549,9 @@ "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" }, "node_modules/semver": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.1.tgz", - "integrity": "sha512-Wvss5ivl8TMRZXXESstBA4uR5iXgEN/VC5/sOcuXdVLzcdkz4HWetIoRfG5gb5X+ij/G9rw9YoGn3QoQ8OCSpw==", + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", "dependencies": { "lru-cache": "^6.0.0" }, @@ -1376,18 +1592,25 @@ } }, "node_modules/sonic-boom": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-1.4.1.tgz", - "integrity": "sha512-LRHh/A8tpW7ru89lrlkU4AszXt1dbwSjVWguGrmlxE7tawVmDBlI1PILMkXAxJTwqhgsEeTHzj36D5CmHgQmNg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.3.0.tgz", + "integrity": "sha512-LYxp34KlZ1a2Jb8ZQgFCK3niIHzibdwtwNUWKg0qQRzsDoJ3Gfgkf8KdBTFU3SkejDEIlWwnSnpVdOZIhFMl/g==", "dependencies": { - "atomic-sleep": "^1.0.0", - "flatstr": "^1.0.12" + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" } }, "node_modules/sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==" }, "node_modules/sshpk": { "version": "1.17.0", @@ -1421,6 +1644,14 @@ "node": ">= 0.10.0" } }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, "node_modules/string-similarity": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/string-similarity/-/string-similarity-4.0.4.tgz", @@ -1428,9 +1659,9 @@ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info." }, "node_modules/tar": { - "version": "6.1.15", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", - "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.0.tgz", + "integrity": "sha512-/Wo7DcT0u5HUV486xg675HtjNd3BXZ6xDbzsCUZPt5iw8bTQ63bP0Raut3mvro9u+CUyq7YQd8Cx55fsZXxqLQ==", "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", @@ -1443,6 +1674,14 @@ "node": ">=10" } }, + "node_modules/thread-stream": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.0.tgz", + "integrity": "sha512-xZYtOtmnA63zj04Q+F9bdEay5r47bvpo1CaNqsKi7TpoJHcotUez8Fkfo2RJWpW91lnnaApdpRbVwCWsy+ifcw==", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/tiny-lru": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/tiny-lru/-/tiny-lru-8.0.2.tgz", @@ -1488,9 +1727,9 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" }, "node_modules/tslib": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.1.tgz", - "integrity": "sha512-KaI6gPil5m9vF7DKaoXxx1ia9fxS4qG5YveErRRVknPDXXriu5M8h48YRjB6h5ZUOKuAKlSJYb0GaDe8I39fRw==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tunnel-agent": { "version": "0.6.0", @@ -1509,23 +1748,21 @@ "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "node_modules/type-fest": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.10.0.tgz", - "integrity": "sha512-hmAPf1datm+gt3c2mvu0sJyhFy6lTkIGf0GzyaZWxRLnabQfPUqg6tF95RPg6sLxKI7nFLGdFxBcf2/7+GXI+A==", + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", "engines": { "node": ">=14.16" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" - }, - "peerDependencies": { - "typescript": ">=4.7.0" } }, "node_modules/typescript": { "version": "4.9.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1564,12 +1801,11 @@ } }, "node_modules/uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { - "uuid": "bin/uuid" + "uuid": "dist/bin/uuid" } }, "node_modules/verror": { @@ -1599,9 +1835,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" + "version": "3.6.19", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.19.tgz", + "integrity": "sha512-d67JP4dHSbm2TrpFj8AbO8DnL1JXL5J9u0Kq2xW6d0TFDbCA3Muhdt8orXC22utleTVj7Prqt82baN6RBvnEgw==" }, "node_modules/whatwg-url": { "version": "5.0.0", @@ -1618,9 +1854,9 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", - "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", "engines": { "node": ">=10.0.0" }, diff --git a/kac-api/package.json b/admission-webhook/package.json similarity index 81% rename from kac-api/package.json rename to admission-webhook/package.json index 12fce0e..f760246 100644 --- a/kac-api/package.json +++ b/admission-webhook/package.json @@ -1,5 +1,5 @@ { - "name": "kac2", + "name": "admission-webhook", "version": "1.0.0", "description": "", "main": "dist/index.js", @@ -13,10 +13,12 @@ "license": "ISC", "dependencies": { "@kubernetes/client-node": "^0.18.1", - "@monokle/validation": "^0.16.0", + "@monokle/parser": "0.2.0", + "@monokle/validation": "^0.30.1", "fastify": "^3.28.0", "isomorphic-fetch": "^3.0.0", "node-fetch": "^2.6.6", + "pino": "^8.15.1", "type-fest": "^3.10.0" }, "devDependencies": { diff --git a/admission-webhook/src/index.ts b/admission-webhook/src/index.ts new file mode 100644 index 0000000..cdad7e7 --- /dev/null +++ b/admission-webhook/src/index.ts @@ -0,0 +1,42 @@ +import pino from 'pino'; +import {getInformer} from './utils/get-informer.js'; +import {MonoklePolicy, MonoklePolicyBinding, PolicyManager} from './utils/policy-manager.js'; +import {ValidatorManager} from './utils/validator-manager.js'; +import {ValidationServer} from './utils/validation-server.js'; + +const LOG_LEVEL = (process.env.MONOKLE_LOG_LEVEL || 'warn').toLowerCase(); +const IGNORED_NAMESPACES = (process.env.MONOKLE_IGNORE_NAMESPACES || '').split(','); + +const logger = pino({ + name: 'Monokle', + level: LOG_LEVEL, +}); + +(async() => { + const policyInformer = await getInformer( + 'monokle.com', + 'v1alpha1', + 'policies', + (err: any) => { + logger.error({msg: 'Informer: Policies: Error', err: err.message, body: err.body}); + } + ); + + const bindingsInformer = await getInformer( + 'monokle.com', + 'v1alpha1', + 'policybindings', + (err: any) => { + logger.error({msg: 'Informer: Bindings: Error', err: err.message, body: err.body}); + } + ); + + const policyManager = new PolicyManager(policyInformer, bindingsInformer, logger); + const validatorManager = new ValidatorManager(policyManager, logger); + + await policyManager.start(); + + const server = new ValidationServer(validatorManager, IGNORED_NAMESPACES, logger); + + await server.start(); +})(); diff --git a/admission-webhook/src/utils/get-informer.ts b/admission-webhook/src/utils/get-informer.ts new file mode 100644 index 0000000..5fb4142 --- /dev/null +++ b/admission-webhook/src/utils/get-informer.ts @@ -0,0 +1,61 @@ +import k8s from '@kubernetes/client-node'; + +export type Informer = k8s.Informer & k8s.ObjectCache; + +export type InformerWrapper = { + informer: Informer, + start: () => Promise +} + +const ERROR_RESTART_INTERVAL = 500; + +export async function getInformer( + group: string, version: string, plural: string, onError?: k8s.ErrorCallback +): Promise> { + const informer = await createInformer(group, version, plural, onError); + const start = createInformerStarter(informer, onError); + + return {informer, start} +} + +function createInformerStarter(informer: Informer, onError?: k8s.ErrorCallback) { + return async () => { + let tries = 0; + let started = false; + + while (!started) { + try { + tries++; + await informer.start(); + started = true; + } catch (err: any) { + if (onError) { + onError(err); + } + + await new Promise((resolve) => setTimeout(resolve, ERROR_RESTART_INTERVAL)); + } + } + } +} + +async function createInformer(group: string, version: string, plural: string, onError?: k8s.ErrorCallback) { + const kc = new k8s.KubeConfig(); + kc.loadFromCluster(); + + const k8sApi = kc.makeApiClient(k8s.CustomObjectsApi) + const listFn = () => k8sApi.listClusterCustomObject(group, version, plural); + const informer = k8s.makeInformer(kc, `/apis/${group}/${version}/${plural}`, listFn as any); + + informer.on('error', (err) => { + if (onError) { + onError(err); + } + + setTimeout(async () => { + await informer.start(); + }, ERROR_RESTART_INTERVAL); + }); + + return informer; +} diff --git a/admission-webhook/src/utils/policy-manager.ts b/admission-webhook/src/utils/policy-manager.ts new file mode 100644 index 0000000..612ae25 --- /dev/null +++ b/admission-webhook/src/utils/policy-manager.ts @@ -0,0 +1,144 @@ +import {EventEmitter} from 'events'; +import pino from 'pino'; +import {KubernetesObject} from '@kubernetes/client-node'; +import {Config} from '@monokle/validation'; +import {InformerWrapper} from './get-informer.js'; +import {AdmissionRequestObject} from './validation-server.js'; +import {postprocess} from './policy-postprocessor.js'; + +export type MonoklePolicy = KubernetesObject & { + spec: Config +} + +export type MonoklePolicyBindingConfiguration = { + policyName: string + validationActions: ['Warn'] + matchResources?: { + namespaceSelector?: { + matchLabels?: Record + } + } +} + +export type MonoklePolicyBinding = KubernetesObject & { + spec: MonoklePolicyBindingConfiguration +} + +export type MonokleApplicablePolicy = { + policy: Config, + binding: MonoklePolicyBindingConfiguration +} + +export class PolicyManager extends EventEmitter{ + private _policies = new Map(); // Map + private _bindings = new Map(); // Map + + constructor( + private readonly _policyInformer: InformerWrapper, + private readonly _bindingInformer: InformerWrapper, + private readonly _logger: ReturnType, + ) { + super(); + + this._policyInformer.informer.on('add', this.onPolicy.bind(this)); + this._policyInformer.informer.on('update', this.onPolicy.bind(this)); + this._policyInformer.informer.on('delete', this.onPolicyRemoval.bind(this)); + + this._bindingInformer.informer.on('add', this.onBinding.bind(this)); + this._bindingInformer.informer.on('update', this.onBinding.bind(this)); + this._bindingInformer.informer.on('delete', this.onBindingRemoval.bind(this)); + } + + async start() { + await this._policyInformer.start(); + await this._bindingInformer.start(); + } + + getMatchingPolicies(resource: AdmissionRequestObject, namespace: string): MonokleApplicablePolicy[] { + this._logger.debug({policies: this._policies.size, bindings: this._bindings.size}); + + if (this._bindings.size === 0) { + return []; + } + + return Array.from(this._bindings.values()) + .map((binding) => { + const policy = this._policies.get(binding.spec.policyName); + + if (!policy) { + this._logger.error({msg: 'Binding is pointing to missing policy', binding}); + return null; + } + + if (binding.spec.matchResources && !this.isResourceMatching(binding, resource)) { + return null; + } + + return { + policy: policy.spec, + binding: binding.spec + } + }) + .filter((policy) => policy !== null) as MonokleApplicablePolicy[]; + } + + private onPolicy(rawPolicy: MonoklePolicy) { + const policy = postprocess(rawPolicy); + + this._logger.debug({msg: 'Policy updated', rawPolicy, policy}); + + this._policies.set(rawPolicy.metadata!.name!, policy); + + this.emit('policyUpdated', policy); + } + + private onPolicyRemoval(rawPolicy: MonoklePolicy) { + const policy = postprocess(rawPolicy); + + this._logger.debug({msg: 'Policy removed', rawPolicy, policy}); + + this._policies.delete(rawPolicy.metadata!.name!); + + this.emit('policyRemoved', policy); + } + + private onBinding(rawBinding: MonoklePolicyBinding) { + this._logger.debug({msg: 'Binding updated', rawBinding}); + + this._bindings.set(rawBinding.metadata!.name!, rawBinding); + + this.emit('bindingUpdated', rawBinding); + } + + private onBindingRemoval(rawBinding: MonoklePolicyBinding) { + this._logger.debug({msg: 'Binding removed', rawBinding}); + + this._bindings.delete(rawBinding.metadata!.name!); + + this.emit('bindingRemoved', rawBinding); + } + + private isResourceMatching(binding: MonoklePolicyBinding, resource: AdmissionRequestObject): boolean { + const namespaceMatchLabels = binding.spec.matchResources?.namespaceSelector?.matchLabels; + + this._logger.trace({ + msg: 'Checking if resource matches binding', + namespaceMatchLabels, + resourceMetadata: resource.metadata.labels + }); + + if (!namespaceMatchLabels) { + return true; + } + + for (const key of Object.keys(namespaceMatchLabels)) { + if (resource.metadata.labels?.[key] !== namespaceMatchLabels[key]) { + if (!(key === 'namespace' && resource.metadata.namespace === namespaceMatchLabels[key])) { + return false; + } + } + } + + return true; + } +} diff --git a/admission-webhook/src/utils/policy-postprocessor.ts b/admission-webhook/src/utils/policy-postprocessor.ts new file mode 100644 index 0000000..c4063bc --- /dev/null +++ b/admission-webhook/src/utils/policy-postprocessor.ts @@ -0,0 +1,30 @@ +import {Config} from '@monokle/validation'; +import {MonoklePolicy} from './policy-manager.js'; + +const PLUGIN_BLOCKLIST = [ + 'resource-links', +]; + +export function postprocess(policy: MonoklePolicy) { + const newPolicy = { ...policy }; + newPolicy.spec = blockPlugins(newPolicy.spec); + return newPolicy; +} + +function blockPlugins(policySpec: Config): Config { + if (policySpec.plugins === undefined) { + return policySpec; + } + + const newPlugins = { ...policySpec.plugins }; + for (const blockedPlugin of PLUGIN_BLOCKLIST) { + if (newPlugins[blockedPlugin] === true) { + newPlugins[blockedPlugin] = false; + } + } + + return { + ...policySpec, + plugins: newPlugins, + }; +} diff --git a/admission-webhook/src/utils/validation-server.ts b/admission-webhook/src/utils/validation-server.ts new file mode 100644 index 0000000..3eb5966 --- /dev/null +++ b/admission-webhook/src/utils/validation-server.ts @@ -0,0 +1,250 @@ +import fastify from 'fastify'; +import pino from 'pino'; +import path from 'path'; +import {readFileSync} from 'fs'; +import {Message, Resource, RuleLevel} from '@monokle/validation'; +import {V1ObjectMeta} from '@kubernetes/client-node'; +import {ValidatorManager} from './validator-manager.js'; + +export type ValidationServerOptions = { + port: number + host: string +}; + +export type AdmissionRequestObject = { + apiVersion: string + kind: string + metadata: V1ObjectMeta + spec: any + status: any +}; + +export type AdmissionRequest = { + apiVersion: string + kind: string + request: { + name: string + namespace: string + uid: string + object: AdmissionRequestObject + } +}; + +// See +// https://pkg.go.dev/k8s.io/api/admission/v1#AdmissionResponse +// https://kubernetes.io/docs/reference/config-api/apiserver-admission.v1/#admission-k8s-io-v1-AdmissionReview +export type AdmissionResponse = { + kind: string + apiVersion: string + response: { + uid: string + allowed: boolean + warnings?: string[] + status: { + message: string + } + } +}; + +export type Violation = { + ruleId: string + message: Message + level?: RuleLevel + actions: string[] +} + +export class ValidationServer { + private _server: ReturnType; + private _shouldValidate: boolean + + constructor( + private readonly _validators: ValidatorManager, + private readonly _ignoredNamespaces: string[], + private readonly _logger: ReturnType, + private readonly _options: ValidationServerOptions = { + port: 8443, + host: '0.0.0.0' + } + ) { + this._shouldValidate = false; + + this._server = fastify({ + https: { + key: readFileSync(path.join('/run/secrets/tls', 'tls.key')), + cert: readFileSync(path.join('/run/secrets/tls', 'tls.crt')) + } + }); + + this._initRouting(); + } + + get shouldValidate() { + return this._shouldValidate; + } + + set shouldValidate(value: boolean) { + this._shouldValidate = value; + } + + async start() { + return new Promise((resolve, reject) => { + this._server.listen({port: this._options.port, host: this._options.host}, (err, address) => { + if (err) { + reject(err); + } + + this._logger.info(`Server listening at ${address}`); + + this.shouldValidate = true; + + resolve(address); + }); + + }); + } + + async stop() { + this.shouldValidate = false; + + if (this._server) { + await this._server.close(); + } + } + + private _initRouting() { + this._server.post("/validate", async (req, _res): Promise => { + + this._logger.trace({requestBody: req.body}); + + const body = req.body as AdmissionRequest; + const namespace = body.request?.namespace; + + const response = { + kind: body?.kind || '', + apiVersion: body?.apiVersion || '', + response: { + uid: body?.request?.uid || "", + allowed: true, + status: { + message: "OK" + } + } + } + + if (!namespace || this._ignoredNamespaces.includes(namespace)) { + this._logger.error({msg: 'No namespace found or namespace ignored', namespace}); + return response; + } + + const resource = body.request?.object; + if (!resource) { + this._logger.error({msg: 'No resource found', metadata: body.request}); + return response; + } + + this._logger.debug({request: req}); + + const validators = this._validators.getMatchingValidators(resource, namespace); + + this._logger.debug({msg: 'Matching validators', count: validators.length}); + + if (validators.length === 0) { + return response; + } + + const resourceForValidation = this._createResourceForValidation(body); + const validationResponses = await Promise.all(validators.map(async (validator) => { + return { + result: await validator.validator.validate({ resources: [resourceForValidation] }), + policy: validator.policy + }; + } + )); + + const violations: Violation[] = []; + for (const validationResponse of validationResponses) { + const actions = validationResponse.policy.binding.validationActions; + + for (const result of validationResponse.result.runs) { + for (const item of result.results) { + violations.push({ + ruleId: item.ruleId, + message: item.message, + level: item.level, + actions: actions + }); + } + } + } + + const violationsByAction = violations.reduce((acc: Record, violation: Violation) => { + const actions = violation.actions; + + for (const action of actions) { + if (!acc[action]) { + acc[action] = []; + } + + acc[action].push(violation); + } + + return acc; + }, {}); + + const responseFull = this._handleViolationsByAction(violationsByAction, response); + + this._logger.debug({response}); + this._logger.trace({resourceForValidation, validationResponses}); + + return responseFull; + }); + } + + private _createResourceForValidation(admissionResource: AdmissionRequest): Resource { + const resource = { + id: admissionResource.request?.uid || '', + fileId: '', + filePath: '', + fileOffset: 0, + name: admissionResource.request?.name || '', + apiVersion: admissionResource.request?.object?.apiVersion || '', + kind: admissionResource.request?.object?.kind || '', + namespace: admissionResource.request?.namespace || '', + content: admissionResource.request?.object || {}, + text: '' + }; + + return resource; + } + + private _handleViolationsByAction(violationsByAction: Record, response: AdmissionResponse) { + for (const action of Object.keys(violationsByAction)) { + // 'Warn' action should be mapped to warnings, see: + // - https://kubernetes.io/docs/reference/access-authn-authz/validating-admission-policy/#validation-actions + // - https://kubernetes.io/blog/2020/09/03/warnings/ + if (action.toLowerCase() === 'warn') { + response = this._handleViolationsAsWarn(violationsByAction[action], response); + } + } + + return response; + } + + private _handleViolationsAsWarn(violations: Violation[], response: AdmissionResponse) { + const warnings = violations.filter((v) => v.level === 'warning').map((e) => `${e.ruleId}: ${e.message.text}`); + const errors = violations.filter((v) => v.level === 'error').map((e) => `${e.ruleId}: ${e.message.text}`); + + if (errors.length > 0 || warnings.length > 0) { + response.response.warnings = [ + `Monokle Admission Controller found:`, + `Errors: ${errors.length}`, + ...errors, + `Warnings: ${warnings.length}`, + ...warnings, + "You can use Monokle (https://monokle.io/) to validate and fix those errors easily!" + ]; + } + + return response; + } +} diff --git a/admission-webhook/src/utils/validator-manager.ts b/admission-webhook/src/utils/validator-manager.ts new file mode 100644 index 0000000..5eea015 --- /dev/null +++ b/admission-webhook/src/utils/validator-manager.ts @@ -0,0 +1,70 @@ +import pino from 'pino'; +import {AnnotationSuppressor, Config, DisabledFixer, FingerprintSuppressor, MonokleValidator, RemotePluginLoader, ResourceParser, SchemaLoader} from '@monokle/validation'; +import {MonokleApplicablePolicy, MonoklePolicy, PolicyManager} from './policy-manager.js'; +import {AdmissionRequestObject} from './validation-server.js'; + +export type MonokleApplicableValidator = { + validator: MonokleValidator, + policy: MonokleApplicablePolicy +} + +export class ValidatorManager { + private _validators = new Map(); // Map + + constructor( + private readonly _policyManager: PolicyManager, + private readonly _logger: ReturnType, + ) { + this._policyManager.on('policyUpdated', async (policy: MonoklePolicy) => { + await this.setupValidator(policy.metadata!.name!, policy.spec); + }); + + this._policyManager.on('policyRemoved', async (policy: MonoklePolicy) => { + await this._validators.delete(policy.metadata!.name!); + }); + } + + getMatchingValidators(resource: AdmissionRequestObject, namespace: string): MonokleApplicableValidator[] { + const matchingPolicies = this._policyManager.getMatchingPolicies(resource, namespace); + + if (matchingPolicies.length === 0) { + return []; + } + + return matchingPolicies.map((policy) => { + if (!this._validators.has(policy.binding.policyName)) { + // This should not happen and means there is a bug in other place in the code. Raise warning and skip. + // Do not create validator instance here to keep this function sync and to keep processing time low. + this._logger.warn({msg: 'ValidatorManager: Validator not found', policyName: policy.binding.policyName}); + return null; + } + + return { + validator: this._validators.get(policy.binding.policyName)!, + policy + } + }).filter((validator) => validator !== null) as MonokleApplicableValidator[]; + } + + private async setupValidator(policyName: string, policy: Config) { + if (this._validators.has(policyName)) { + await this._validators.get(policyName)!.preload(policy); + } else { + const validator = new MonokleValidator( + { + loader: new RemotePluginLoader(), + parser: new ResourceParser(), + schemaLoader: new SchemaLoader(), + suppressors: [new AnnotationSuppressor(), new FingerprintSuppressor()], + fixer: new DisabledFixer(), + } + ); + + // Run separately (instead of passing config to constructor) to make sure that validator + // is ready when 'setupValidator' function call fulfills. + await validator.preload(policy); + + this._validators.set(policyName, validator); + } + } +} diff --git a/kac-api/tsconfig.json b/admission-webhook/tsconfig.json similarity index 100% rename from kac-api/tsconfig.json rename to admission-webhook/tsconfig.json diff --git a/examples/policy-binding-sample-1.yaml b/examples/policy-binding-sample-1.yaml new file mode 100644 index 0000000..13136e0 --- /dev/null +++ b/examples/policy-binding-sample-1.yaml @@ -0,0 +1,7 @@ +apiVersion: monokle.com/v1alpha1 +kind: MonoklePolicyBinding +metadata: + name: policy-binding-sample-1 +spec: + policyName: "policy-sample-1" + validationActions: [Warn] \ No newline at end of file diff --git a/examples/policy-binding-sample-2.yaml b/examples/policy-binding-sample-2.yaml new file mode 100644 index 0000000..9a945ba --- /dev/null +++ b/examples/policy-binding-sample-2.yaml @@ -0,0 +1,11 @@ +apiVersion: monokle.com/v1alpha1 +kind: MonoklePolicyBinding +metadata: + name: policy-binding-sample-2 +spec: + policyName: "policy-sample-2" + validationActions: [Warn] + matchResources: + namespaceSelector: + matchLabels: + namespace: default \ No newline at end of file diff --git a/examples/policy-binding-sample-3.yaml b/examples/policy-binding-sample-3.yaml new file mode 100644 index 0000000..1f8a5e1 --- /dev/null +++ b/examples/policy-binding-sample-3.yaml @@ -0,0 +1,11 @@ +apiVersion: monokle.com/v1alpha1 +kind: MonoklePolicyBinding +metadata: + name: policy-binding-sample-3 +spec: + policyName: "policy-sample-2" + validationActions: [Warn] + matchResources: + namespaceSelector: + matchLabels: + namespace: nstest1 \ No newline at end of file diff --git a/examples/policy-sample-1.yaml b/examples/policy-sample-1.yaml new file mode 100644 index 0000000..7030db2 --- /dev/null +++ b/examples/policy-sample-1.yaml @@ -0,0 +1,21 @@ +apiVersion: monokle.com/v1alpha1 +kind: MonoklePolicy +metadata: + name: policy-sample-1 +spec: + plugins: + yaml-syntax: true + open-policy-agent: true + resource-links: true + kubernetes-schema: true + annotations: true + rules: + yaml-syntax/no-bad-alias: "warn" + yaml-syntax/no-bad-directive: false + open-policy-agent/no-last-image: "err" + open-policy-agent/cpu-limit: "err" + open-policy-agent/memory-limit: "err" + open-policy-agent/memory-request: "err" + settings: + kubernetes-schema: + schemaVersion: v1.28.2 \ No newline at end of file diff --git a/examples/policy-sample-2.yaml b/examples/policy-sample-2.yaml new file mode 100644 index 0000000..2c15f5f --- /dev/null +++ b/examples/policy-sample-2.yaml @@ -0,0 +1,8 @@ +apiVersion: monokle.com/v1alpha1 +kind: MonoklePolicy +metadata: + name: policy-sample-2 +spec: + plugins: + open-policy-agent: true + pod-security-standards: true \ No newline at end of file diff --git a/k8s/manifests/monokle-policy-binding-crd.yaml b/k8s/manifests/monokle-policy-binding-crd.yaml new file mode 100644 index 0000000..138ca6a --- /dev/null +++ b/k8s/manifests/monokle-policy-binding-crd.yaml @@ -0,0 +1,46 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: policybindings.monokle.com +spec: + group: monokle.com + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + required: + - policyName + - validationActions + properties: + policyName: + type: string + validationActions: + type: array + items: + type: string + enum: [Warn] + matchResources: + type: object + properties: + namespaceSelector: + type: object + properties: + matchLabels: + type: object + additionalProperties: + type: string + scope: Cluster + names: + plural: policybindings + singular: policybinding + kind: MonoklePolicyBinding + shortNames: + - mpb \ No newline at end of file diff --git a/k8s/manifests/monokle-policy-crd.yaml b/k8s/manifests/monokle-policy-crd.yaml new file mode 100644 index 0000000..709ddad --- /dev/null +++ b/k8s/manifests/monokle-policy-crd.yaml @@ -0,0 +1,50 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: policies.monokle.com +spec: + group: monokle.com + versions: + - name: v1alpha1 + served: true + storage: true + schema: + # For schema see: + # - maps/dicts - https://swagger.io/docs/specification/data-models/dictionaries/ + # - structural schema - https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema + # + # Object values as multitypes: + # Even though it's supported by OpenAPI spec, e.g. https://stackoverflow.com/a/46475776, + # Kubernetes requires "structural" definition # https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/#specifying-a-structural-schema + # which seems to be in opposite to it "does not set description, type, default, additionalProperties, nullable + # within an allOf, anyOf, oneOf or not, with the exception of the two pattern for x-kubernetes-int-or-string: true (see below)." + openAPIV3Schema: + type: object + required: + - spec + properties: + spec: + type: object + required: + - plugins + properties: + plugins: + type: object + additionalProperties: + type: boolean + rules: + type: object + additionalProperties: true + settings: + type: object + additionalProperties: + type: object + additionalProperties: + type: string + scope: Cluster + names: + plural: policies + singular: policy + kind: MonoklePolicy + shortNames: + - mp diff --git a/k8s/manifests/service-account.yaml b/k8s/manifests/service-account.yaml new file mode 100644 index 0000000..492b1b4 --- /dev/null +++ b/k8s/manifests/service-account.yaml @@ -0,0 +1,27 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: monokle-policies +--- +kind: ClusterRole +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: monokle-policies +rules: +- apiGroups: ["monokle.com"] + resources: ["policies", "policybindings"] + verbs: ["list", "watch"] + +--- +kind: ClusterRoleBinding +apiVersion: rbac.authorization.k8s.io/v1 +metadata: + name: monokle-policies +subjects: +- kind: ServiceAccount + name: monokle-policies + namespace: monokle-admission-controller +roleRef: + kind: ClusterRole + name: monokle-policies + apiGroup: rbac.authorization.k8s.io \ No newline at end of file diff --git a/skaffold.yaml b/k8s/skaffold.yaml similarity index 78% rename from skaffold.yaml rename to k8s/skaffold.yaml index 934df1c..db22fb8 100644 --- a/skaffold.yaml +++ b/k8s/skaffold.yaml @@ -4,12 +4,12 @@ metadata: name: webhook-server build: artifacts: - - image: kac-api + - image: admission-webhook docker: dockerfile: Dockerfile manifests: rawYaml: - - deployment.yaml + - ./k8s/manifests/deployment.yaml portForward: - resourceType: service resourceName: webhook-server diff --git a/deployment/deployment.yaml.template b/k8s/templates/deployment.yaml.template similarity index 66% rename from deployment/deployment.yaml.template rename to k8s/templates/deployment.yaml.template index 0eab556..253e8fb 100644 --- a/deployment/deployment.yaml.template +++ b/k8s/templates/deployment.yaml.template @@ -2,7 +2,7 @@ apiVersion: apps/v1 kind: Deployment metadata: name: webhook-server - namespace: webhook-demo + namespace: monokle-admission-controller labels: app: webhook-server spec: @@ -15,12 +15,12 @@ spec: labels: app: webhook-server spec: - #securityContext: + # securityContext: # runAsNonRoot: true - # runAsUser: 1234 + # runAsUser: 1000 containers: - name: server - image: kac-api + image: admission-webhook #imagePullPolicy: Always ports: - containerPort: 8443 @@ -29,16 +29,22 @@ spec: - name: webhook-tls-certs mountPath: /run/secrets/tls readOnly: true + env: + - name: MONOKLE_LOG_LEVEL + value: DEBUG + - name: MONOKLE_IGNORE_NAMESPACES + value: 'kube-node-lease,kube-public,kube-system,monokle-admission-controller' volumes: - name: webhook-tls-certs secret: secretName: webhook-server-tls + serviceAccountName: monokle-policies --- apiVersion: v1 kind: Service metadata: name: webhook-server - namespace: webhook-demo + namespace: monokle-admission-controller spec: selector: app: webhook-server diff --git a/deployment/webhook.yaml.template b/k8s/templates/webhook.yaml.template similarity index 56% rename from deployment/webhook.yaml.template rename to k8s/templates/webhook.yaml.template index 6940037..61157d6 100644 --- a/deployment/webhook.yaml.template +++ b/k8s/templates/webhook.yaml.template @@ -3,17 +3,18 @@ kind: ValidatingWebhookConfiguration metadata: name: demo-webhook webhooks: - - name: webhook-server.webhook-demo.svc + - name: webhook-server.monokle-admission-controller.svc sideEffects: None admissionReviewVersions: ["v1", "v1beta1"] clientConfig: service: name: webhook-server - namespace: webhook-demo + namespace: monokle-admission-controller path: "/validate" caBundle: ${CA_PEM_B64} rules: - - operations: [ "CREATE" ] - apiGroups: [""] - apiVersions: ["v1"] - resources: ["pods"] \ No newline at end of file + - operations: ["CREATE", "UPDATE"] + apiGroups: ["*"] + apiVersions: ["*"] + resources: ["*"] + scope: "*" \ No newline at end of file diff --git a/kac-api/src/index.ts b/kac-api/src/index.ts deleted file mode 100644 index a826742..0000000 --- a/kac-api/src/index.ts +++ /dev/null @@ -1,128 +0,0 @@ -import {readFileSync} from "fs"; -import path from "path"; -import fastify from "fastify"; -import {V1ValidatingWebhookConfiguration, V1ObjectMeta} from "@kubernetes/client-node"; -import {createDefaultMonokleValidator, Resource} from "@monokle/validation"; - -// TODO - some mismatch with types, this is not exactly the type which should be used -type AdmissionRequest = V1ValidatingWebhookConfiguration & { - request?: V1ObjectMeta & { - object?: Resource - } -} - -type AdmissionResponse = { - kind: string, - apiVersion: string, - response: { - uid: string, - allowed: boolean, - status: { - message: string - } - } -} - -const validator = createDefaultMonokleValidator(); - -const server = fastify({ - https: { - key: readFileSync(path.join('/run/secrets/tls', 'tls.key')), - cert: readFileSync(path.join('/run/secrets/tls', 'tls.crt')) - } -}); - -server.post("/validate", async (req, res): Promise => { - console.log('request', req.headers, req.body) - - const body = req.body as AdmissionRequest; - - const response = { - kind: body?.kind || '', - apiVersion: body?.apiVersion || '', - response: { - uid: body?.request?.uid || "", - allowed: true, - status: { - message: "OK" - } - } - } - - // Dev workaround - always return true for webhok server to not block hot-reload - if (body.request?.name?.startsWith('webhook-server-')) { - console.log('Allowing webhook server to pass', response); - - return response; - } - - await validator.preload({ - plugins: { - "kubernetes-schema": true, - "open-policy-agent": true, - }, - }); - - const resourceForValidation = createResourceForValidation(body); - const validationResponse = await validator.validate({ resources: [resourceForValidation] }); - - const warnings = []; - const errors = []; - for (const result of validationResponse.runs) { - for (const item of result.results) { - if (item.level === "warning") { - warnings.push(item); - } else if (item.level === "error") { - errors.push(item); - } - } - } - - if (errors.length > 0 || warnings.length > 0) { - const warningsList = warnings.map((e) => `${e.ruleId}: ${e.message.text}`).join("\n"); - const errorsList = errors.map((e) => `${e.ruleId}: ${e.message.text}`).join("\n"); - const message = []; - - if (errors.length > 0) { - message.push(`\n${errors.length} errors found:\n${errorsList}\n`); - } - - if (warnings.length > 0) { - message.push(`\n${warnings.length} warnings found:\n${warningsList}\n`); - } - - message.push("\nYou can use Monokle (https://monokle.io/) to validate and fix those errors easily!"); - - response.response.allowed = false; - response.response.status.message = message.join(""); - } - - console.log('response', resourceForValidation, validationResponse, response); - - return response; -}); - -server.listen({port: 8443, host: '0.0.0.0' }, (err, address) => { - if (err) { - console.error(err); - process.exit(1); - } - console.log(`Server listening at ${address}`); -}); - -function createResourceForValidation(admissionResource: AdmissionRequest): Resource { - const resource = { - id: admissionResource.request?.uid || '', - fileId: '', - filePath: '', - fileOffset: 0, - name: admissionResource.request?.name || '', - apiVersion: admissionResource.request?.object?.apiVersion || '', - kind: admissionResource.request?.object?.kind || '', - namespace: admissionResource.request?.namespace || '', - content: admissionResource.request?.object || {}, - text: '' - }; - - return resource; -} \ No newline at end of file diff --git a/deploy.sh b/scripts/deploy.sh similarity index 55% rename from deploy.sh rename to scripts/deploy.sh index e6082eb..3dd2316 100755 --- a/deploy.sh +++ b/scripts/deploy.sh @@ -16,41 +16,51 @@ # deploy.sh # -# Sets up the environment for the admission controller webhook demo in the active cluster. +# Sets up the environment for the admission controller webhook in the active cluster. set -euo pipefail -basedir="$(dirname "$0")/deployment" +basedir="$(dirname "$0")" keydir="$(mktemp -d)" +templdir="${basedir}/../k8s/templates" +resdir="${basedir}/../k8s/manifests" # Generate keys into a temporary directory. echo "Generating TLS keys ..." "${basedir}/generate-keys.sh" "$keydir" -# Create the `webhook-demo` namespace. This cannot be part of the YAML file as we first need to create the TLS secret, +# Create the `monokle-admission-controller` namespace. This cannot be part of the YAML file as we first need to create the TLS secret, # which would fail otherwise. echo "Creating Kubernetes objects ..." -kubectl create namespace webhook-demo +kubectl create namespace monokle-admission-controller + +# Create test namespaces +kubectl create namespace nstest1 +kubectl create namespace nstest2 # Create the TLS secret for the generated keys. -kubectl -n webhook-demo create secret tls webhook-server-tls \ +kubectl -n monokle-admission-controller create secret tls webhook-server-tls \ --cert "${keydir}/webhook-server-tls.crt" \ --key "${keydir}/webhook-server-tls.key" -rm -f webhook.yaml deployment.yaml +rm -f "${resdir}/webhook.yaml" "${resdir}/deployment.yaml" # Read the PEM-encoded CA certificate, base64 encode it, and replace the `${CA_PEM_B64}` placeholder in the YAML # template with it. Then, create the Kubernetes resources. ca_pem_b64="$(openssl base64 -A <"${keydir}/ca.crt")" -sed -e 's@${CA_PEM_B64}@'"$ca_pem_b64"'@g' <"${basedir}/webhook.yaml.template" > webhook.yaml -cp deployment/deployment.yaml.template deployment.yaml +sed -e 's@${CA_PEM_B64}@'"$ca_pem_b64"'@g' <"${templdir}/webhook.yaml.template" > "${resdir}/webhook.yaml" +cp "${templdir}/deployment.yaml.template" "${resdir}/deployment.yaml" + +# Cluster-wide +kubectl apply -f "${resdir}/monokle-policy-crd.yaml" +kubectl apply -f "${resdir}/monokle-policy-binding-crd.yaml" -skaffold run --namespace webhook-demo -# kubectl apply -f deployment.yaml -sleep 2 -kubectl apply -f webhook.yaml +# Namespaced +kubectl apply -f "${resdir}/service-account.yaml" -n monokle-admission-controller -# skaffold dev +skaffold run -n monokle-admission-controller -f k8s/skaffold.yaml +sleep 5 +kubectl apply -f "${resdir}/webhook.yaml" # Delete the key directory to prevent abuse (DO NOT USE THESE KEYS ANYWHERE ELSE). rm -rf "$keydir" diff --git a/deployment/generate-keys.sh b/scripts/generate-keys.sh similarity index 89% rename from deployment/generate-keys.sh rename to scripts/generate-keys.sh index 7df6494..7ecb23d 100755 --- a/deployment/generate-keys.sh +++ b/scripts/generate-keys.sh @@ -17,7 +17,7 @@ # generate-keys.sh # # Generate a (self-signed) CA certificate and a certificate and private key to be used by the webhook demo server. -# The certificate will be issued for the Common Name (CN) of `webhook-server.webhook-demo.svc`, which is the +# The certificate will be issued for the Common Name (CN) of `webhook-server.monokle-admission-controller.svc`, which is the # cluster-internal DNS name for the service. # # NOTE: THIS SCRIPT EXISTS FOR DEMO PURPOSES ONLY. DO NOT USE IT FOR YOUR PRODUCTION WORKLOADS. @@ -35,14 +35,14 @@ req_extensions = v3_req distinguished_name = req_distinguished_name prompt = no [req_distinguished_name] -CN = webhook-server.webhook-demo.svc +CN = webhook-server.monokle-admission-controller.svc [ v3_req ] basicConstraints = CA:FALSE keyUsage = nonRepudiation, digitalSignature, keyEncipherment extendedKeyUsage = clientAuth, serverAuth subjectAltName = @alt_names [alt_names] -DNS.1 = webhook-server.webhook-demo.svc +DNS.1 = webhook-server.monokle-admission-controller.svc EOF @@ -51,5 +51,5 @@ openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=Admission Co # Generate the private key for the webhook server openssl genrsa -out webhook-server-tls.key 2048 # Generate a Certificate Signing Request (CSR) for the private key, and sign it with the private key of the CA. -openssl req -new -key webhook-server-tls.key -subj "/CN=webhook-server.webhook-demo.svc" -config server.conf \ +openssl req -new -key webhook-server-tls.key -subj "/CN=webhook-server.monokle-admission-controller.svc" -config server.conf \ | openssl x509 -req -CA ca.crt -CAkey ca.key -CAcreateserial -out webhook-server-tls.crt -extensions v3_req -extfile server.conf