diff --git a/test/images/webhook/Dockerfile b/test/images/webhook/Dockerfile new file mode 100644 index 00000000000..2a43f424b21 --- /dev/null +++ b/test/images/webhook/Dockerfile @@ -0,0 +1,18 @@ +# Copyright 2017 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +FROM alpine:latest + +ADD webhook /webhook +ENTRYPOINT ["/webhook"] diff --git a/test/images/webhook/Makefile b/test/images/webhook/Makefile new file mode 100644 index 00000000000..7f706cbaf13 --- /dev/null +++ b/test/images/webhook/Makefile @@ -0,0 +1,19 @@ +# Copyright 2017 The Kubernetes Authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +build: + CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o webhook . + docker build --no-cache -t gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v1 . +push: + gcloud docker --push gcr.io/kubernetes-e2e-test-images/k8s-sample-admission-webhook-amd64:1.8v1 . diff --git a/test/images/webhook/README.md b/test/images/webhook/README.md new file mode 100644 index 00000000000..14895d90997 --- /dev/null +++ b/test/images/webhook/README.md @@ -0,0 +1,51 @@ +# Kubernetes External Admission Webhook Example + +The example shows how to build and deploy an external webhook that only admits +pods creation and update if the container images have the "grc.io" prefix. + +## Prerequisites + +Please use a Kubernetes release at least as new as v1.8.0 or v1.9.0-alpha.1, +because the generated server cert/key only works with Kubernetes release that +contains this [change](https://github.com/kubernetes/kubernetes/pull/50476). +Please checkout the `pre-v1.8` tag for an example that works with older +clusters. + +Please enable the admission webhook feature +([doc](https://kubernetes.io/docs/admin/extensible-admission-controllers/#enable-external-admission-webhooks)). + +## Build the code + +```bash +make build +``` + +## Deploy the code + +```bash +make deploy-only +``` + +The Makefile assumes your cluster is created by the +[hack/local-up-cluster.sh](https://github.com/kubernetes/kubernetes/blob/master/hack/local-up-cluster.sh). +Please modify the Makefile accordingly if your cluster is created differently. + +## Explanation on the CAs/Certs/Keys + +The apiserver initiates a tls connection with the webhook, so the apiserver is +the tls client, and the webhook is the tls server. + +The webhook proves its identity by the `serverCert` in the certs.go. The server +cert is signed by the CA in certs.go. To let the apiserver trust the `caCert`, +the webhook registers itself with the apiserver via the +`admissionregistration/v1alpha1/externalAdmissionHook` API, with +`clientConfig.caBundle=caCert`. + +For maximum protection, this example webhook requires and verifies the client +(i.e., the apiserver in this case) cert. The cert presented by the apiserver is +signed by a client CA, whose cert is stored in the configmap +`extension-apiserver-authentication` in the `kube-system` namespace. See the +`getAPIServerCert` function for more information. Usually you don't need to +worry about setting up this CA cert. It's taken care of when the cluster is +created. You can disable the client cert verification by setting the +`tls.Config.ClientAuth` to `tls.NoClientCert` in `config.go`. diff --git a/test/images/webhook/config.go b/test/images/webhook/config.go new file mode 100644 index 00000000000..c3f736eaa06 --- /dev/null +++ b/test/images/webhook/config.go @@ -0,0 +1,51 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "crypto/tls" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/rest" + + "github.com/golang/glog" +) + +// Get a clientset with in-cluster config. +func getClient() *kubernetes.Clientset { + config, err := rest.InClusterConfig() + if err != nil { + glog.Fatal(err) + } + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + glog.Fatal(err) + } + return clientset +} + +func configTLS(config Config, clientset *kubernetes.Clientset) *tls.Config { + sCert, err := tls.LoadX509KeyPair(config.CertFile, config.KeyFile) + if err != nil { + glog.Fatal(err) + } + return &tls.Config{ + Certificates: []tls.Certificate{sCert}, + // TODO: uses mutual tls after we agree on what cert the apiserver should use. + // ClientAuth: tls.RequireAndVerifyClientCert, + } +} diff --git a/test/images/webhook/main.go b/test/images/webhook/main.go new file mode 100644 index 00000000000..f6e6500e5f3 --- /dev/null +++ b/test/images/webhook/main.go @@ -0,0 +1,130 @@ +/* +Copyright 2017 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "encoding/json" + "flag" + "io/ioutil" + "net/http" + "strings" + + "github.com/golang/glog" + "k8s.io/api/admission/v1alpha1" + "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// Config contains the server (the webhook) cert and key. +type Config struct { + CertFile string + KeyFile string +} + +func (c *Config) addFlags() { + flag.StringVar(&c.CertFile, "tls-cert-file", c.CertFile, ""+ + "File containing the default x509 Certificate for HTTPS. (CA cert, if any, concatenated "+ + "after server cert).") + flag.StringVar(&c.KeyFile, "tls-private-key-file", c.KeyFile, ""+ + "File containing the default x509 private key matching --tls-cert-file.") +} + +// only allow pods to pull images from specific registry. +func admit(data []byte) *v1alpha1.AdmissionReviewStatus { + ar := v1alpha1.AdmissionReview{} + if err := json.Unmarshal(data, &ar); err != nil { + glog.Error(err) + return nil + } + podResource := metav1.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"} + if ar.Spec.Resource != podResource { + glog.Errorf("expect resource to be %s", podResource) + return nil + } + + raw := ar.Spec.Object.Raw + pod := v1.Pod{} + if err := json.Unmarshal(raw, &pod); err != nil { + glog.Error(err) + return nil + } + reviewStatus := v1alpha1.AdmissionReviewStatus{} + reviewStatus.Allowed = true + // Note: the apiserver encodes the api.Pod. Decoding it as a v1.Pod will + // lose the metadata. So the following check on labels will not work + // until we let the apiserver encodes the versioned object. + for k, v := range pod.Labels { + if k == "webhook-e2e-test" && v == "webhook-disallow" { + reviewStatus.Allowed = false + reviewStatus.Result = &metav1.Status{ + Reason: "the pod contains unwanted label", + } + } + } + for _, container := range pod.Spec.Containers { + if strings.Contains(container.Name, "webhook-disallow") { + reviewStatus.Allowed = false + reviewStatus.Result = &metav1.Status{ + Message: "the pod contains unwanted container name", + } + } + } + return &reviewStatus +} + +func serve(w http.ResponseWriter, r *http.Request) { + var body []byte + if r.Body != nil { + if data, err := ioutil.ReadAll(r.Body); err == nil { + body = data + } + } + + // verify the content type is accurate + contentType := r.Header.Get("Content-Type") + if contentType != "application/json" { + glog.Errorf("contentType=%s, expect application/json", contentType) + return + } + + reviewStatus := admit(body) + ar := v1alpha1.AdmissionReview{ + Status: *reviewStatus, + } + + resp, err := json.Marshal(ar) + if err != nil { + glog.Error(err) + } + if _, err := w.Write(resp); err != nil { + glog.Error(err) + } +} + +func main() { + var config Config + config.addFlags() + flag.Parse() + + http.HandleFunc("/", serve) + clientset := getClient() + server := &http.Server{ + Addr: ":443", + TLSConfig: configTLS(config, clientset), + } + server.ListenAndServeTLS("", "") +} diff --git a/test/images/webhook/webhook b/test/images/webhook/webhook new file mode 100755 index 00000000000..22a214bcf83 Binary files /dev/null and b/test/images/webhook/webhook differ