介绍
在本文中,我们将探讨webhook如何在kubernetes中工作,更具体地说是关于ImagePolicyWebhook。因为有关它的kubernetes文档有点含糊不清,也没有真正的示例。
本文基于ImagePolicyWebhook 或 ValidatingAdmissionWebhook,实现准入控制器将拒绝所有使用带有latest标签的镜像的Pod。
准入控制器(Admission Control)在授权后对请求做进一步的验证或添加默认参数。不同于授权和认证只关心请求的用户和操作,准入控制还处理请求的内容,并且仅对创建、更新、删除或连接(如代理)等有效,而对读操作无效。
对比:ImagePolicyWebhook 和 ValidatingAdmissionWebhook
ImagePolicyWebhook是准入控制器,仅评估镜像,你需要解析请求做逻辑和以允许或集群中否认镜像的响应。
ImagePolicyWebhook: 通过 webhook 决定 image 策略,需要同时配置 –admission-control-config-file,配置文件格式见 这里。
ImagePolicyWebhook优势:
- 如果无法连接Webhook端点,可以指示API服务器拒绝镜像,这非常方便,但是它也会带来问题,例如核心Pod无法调度。
ImagePolicyWebhook劣势:
- 配置涉及更多内容,并且需要访问主节点或apiserver配置,文档尚不明确,可能难以进行更改,更新等。
- 部署并不是那么简单,你需要使用systemd进行部署或将其作为docker容器在主机中运行,更新dns等。
ValidatingAdmissionWebhook使用 Webhook 验证请求,这些 Webhook 并行调用,并且任何一个调用拒绝都会导致请求失败 。
ValidatingAdmissionWebhook优势:
- 由于该服务作为Pod运行,因此部署更容易。
- 一切都可以成为kubernetes资源。
- 需要较少的人工干预和访问主机。
- 如果pod或服务不可用,则将允许所有镜像,这在某些情况下会带来安全风险,因此,如果要使用此路径,请确保使其高度可用,这实际上可以通过指定failurePolicytoFail来配置的Ignore(Fail是默认设置)。
ValidatingAdmissionWebhook劣势:
- 具有RBAC权限的任何人都可以更新/更改配置,因为它只是kubernetes的一个资源。
准备工作
构建
如果你打算将其用作普通服务:
$ go get github.com/kainlite/kube-image-bouncer
你还可以使用此Docker镜像:
$ docker pull kainlite/kube-image-bouncer
证书
如果你想了解更多信息,我们可以依靠kubernetes CA生成我们需要的证书。具体可以参考 管理群集中的TLS证书 。
创建一个CSR:
$ cat <<EOF | cfssl genkey - | cfssljson -bare server { "hosts": [ "image-bouncer-webhook.default.svc", "image-bouncer-webhook.default.svc.cluster.local", "image-bouncer-webhook.default.pod.cluster.local", "192.0.2.24", "10.0.34.2" ], "CN": "system:node:image-bouncer-webhook.default.pod.cluster.local", "key": { "algo": "ecdsa", "size": 256 }, "names": [ { "O": "system:nodes" } ] } EOF
然后将其应用于集群
$ cat <<EOF | kubectl apply -f - apiVersion: certificates.k8s.io/v1 kind: CertificateSigningRequest metadata: name: image-bouncer-webhook.default spec: request: $(cat server.csr | base64 | tr -d '\n') signerName: kubernetes.io/kubelet-serving usages: - digital signature - key encipherment - server auth EOF
批准并获取证书供以后使用
$ kubectl get csr image-bouncer-webhook.default -o jsonpath='{.status.certificate}' | base64 --decode > server.crt
ImagePolicyWebhook 方式
有两种方法来部署webhook控制器,要使其正常工作,你将需要按照以下说明创建证书。但是首先我们需要注意一个细节,将此证书添加到主服务器中的主机文件中运行:
我们使用的名称必须与证书中的名称匹配,因为它可以在kuberntes外部运行,甚至可以在外部使用,我们只是使用主机条目来伪造它
$ echo "127.0.0.1 image-bouncer-webhook.default.svc" >> /etc/hosts
另外,在apiserver中,你需要使用以下设置进行更新:
--admission-control-config-file=/etc/kubernetes/kube-image-bouncer/admission_configuration.json --enable-admission-plugins=ImagePolicyWebhook
如果你使用这个方法,则无需创建validating-webhook-configuration.yaml资源,也无需使用 deployment即可在集群中运行。
创建一个名为/etc/kubernetes/kube-image-bouncer/admission_configuration.json准入控制配置文件,其内容如下:
{ "imagePolicy": { "kubeConfigFile": "/etc/kubernetes/kube-image-bouncer/kube-image-bouncer.yml", "allowTTL": 50, "denyTTL": 50, "retryBackoff": 500, "defaultAllow": false } }
如果想要允许默认镜像,请调整defaultAllow。
创建具有以下内容的kubeconfig文件/etc/kubernetes/kube-image-bouncer/kube-image-bouncer.yml:
apiVersion: v1 kind: Config clusters: - cluster: certificate-authority: /etc/kubernetes/kube-image-bouncer/pki/server.crt server: https://image-bouncer-webhook.default.svc:1323/image_policy name: bouncer_webhook contexts: - context: cluster: bouncer_webhook user: api-server name: bouncer_validator current-context: bouncer_validator preferences: {} users: - name: api-server user: client-certificate: /etc/kubernetes/pki/apiserver.crt client-key: /etc/kubernetes/pki/apiserver.key
此配置文件指示API服务器连接地址为https://image-bouncer-webhook.default.svc:1323的Webhook服务器并使用其/image_policy端点,我们可以重用apiserver的证书以及已经生成的kube-image-bouncer证书。
请注意,你需要与证书放在同一个文件夹中,这样才能起作用:
$ docker run --rm -v `pwd`/server-key.pem:/certs/server-key.pem:ro -v `pwd`/server.crt:/certs/server.crt:ro -p 1323:1323 --network host kainlite/kube-image-bouncer -k /certs/server-key.pem -c /certs/server.crt
ValidatingAdmissionWebhook 方式
如果你要使用ValidatingAdmissionWebhook ,你要做的就是生成证书,其他一切都可以使用kubectl完成。首先你必须创建一个名为tls的secret文件(用来保存webhook证书和密钥) 。我们在上一步中刚刚生成了此 secret :
$ kubectl create secret tls tls-image-bouncer-webhook \ --key server-key.pem \ --cert server.pem
然后为创建一个名为image-bouncer-webhook的 deployment 文件:
$ kubectl apply -f kubernetes/image-bouncer-webhook.yaml
最后创建ValidatingWebhookConfiguration,确保使用我们的webhook端点 :
$ cat <<EOF | kubectl apply -f - apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: image-bouncer-webook webhooks: - name: image-bouncer-webhook.default.svc rules: - apiGroups: - "" apiVersions: - v1 operations: - CREATE resources: - pods failurePolicy: Ignore sideEffects: None admissionReviewVersions: ["v1", "v1beta1"] clientConfig: caBundle: $(kubectl get csr image-bouncer-webhook.default -o jsonpath='{.status.certificate}') service: name: image-bouncer-webhook namespace: default EOF
更改可能需要一点时间才能体现出来,因此请等待几秒钟,然后尝试一下。推出 helm chart后,这将实现自动化。
测试
ImagePolicyWebhook和ValidatingAdmissionWebhook都可以用相同的方式工作,并且你将看到类似的错误消息,例如:
Error creating: pods "nginx-latest-sdsmb" is forbidden: image policy webhook backend denied one or more images: Images using latest tag are not allowed
要么
Warning FailedCreate 23s (x15 over 43s) replication-controller Error creating: admission webhook "image-bouncer-webhook.default.svc" denied the request: Images using latest tag are not allowed
创建一个名为nginx-versioned 的RC来验证发布是否仍然有效:
$ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: ReplicationController metadata: name: nginx-versioned spec: replicas: 1 selector: app: nginx-versioned template: metadata: name: nginx-versioned labels: app: nginx-versioned spec: containers: - name: nginx-versioned image: nginx:1.13.8 ports: - containerPort: 80 EOF
检查副本控制器是否运行:
$ kubectl get rc NAME DESIRED CURRENT READY AGE nginx-versioned 1 1 0 2h
现在验证我们的webhook控制器,是否可以拒绝带有latest标签镜像的pod:
$ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: ReplicationController metadata: name: nginx-latest spec: replicas: 1 selector: app: nginx-latest template: metadata: name: nginx-latest labels: app: nginx-latest spec: containers: - name: nginx-latest image: nginx ports: - containerPort: 80 EOF
检查了Pod,RC应该显示类似以下输出的内容。你也可以使用命令kubectl get events –sort-by='{.lastTimestamp}’进行检查:
$ kubectl describe rc nginx-latest Name: nginx-latest Namespace: default Selector: app=nginx-latest Labels: app=nginx-latest Annotations: <none> Replicas: 0 current / 1 desired Pods Status: 0 Running / 0 Waiting / 0 Succeeded / 0 Failed Pod Template: Labels: app=nginx-latest Containers: nginx-latest: Image: nginx Port: 80/TCP Host Port: 0/TCP Environment: <none> Mounts: <none> Volumes: <none> Conditions: Type Status Reason ---- ------ ------ ReplicaFailure True FailedCreate Events: Type Reason Age From Message ---- ------ ---- ---- ------- Warning FailedCreate 23s (x15 over 43s) replication-controller Error creating: admission webhook "image-bouncer-webhook.default.svc" denied the request: Images using latest tag are not allowed
调试
如果你使用的是准入控制器路径,则查看apiserver日志总是很有用的,因为它将在其中记录失败的原因以及镜像的日志。
W0107 17:39:00.619560 1 dispatcher.go:142] rejected by webhook "image-bouncer-webhook.default.svc": &errors.StatusError{ErrStatus:v1.Status{TypeMeta:v1.TypeMeta{Kind:"", APIVersion:""}, ListMeta:v1.ListMeta{ SelfLink:"", ResourceVersion:"", Continue:"", RemainingItemCount:(*int64)(nil)}, Status:"Failure", Message:"admission webhook \"image-bouncer-webhook.default.svc\" denied the request: Images using latest tag are not allowed", Reason:"", Details:(*v1.StatusDetails)(nil), Code:400}}
kube-image-bouncer:
echo: http: TLS handshake error from 127.0.0.1:49414: remote error: tls: bad certificate method=POST, uri=/image_policy?timeout=30s, status=200 method=POST, uri=/image_policy?timeout=30s, status=200 method=POST, uri=/image_policy?timeout=30s, status=200
错误来自手动测试,其他错误是来自apiserver的成功请求。
深入源码分析
让我们通过源码,简要地介绍一下创建准入控制器或Webhook的关键部分:
一下是main.go的一部分,有两个POST方法,并进行其他一些验证,并转换为准入控制者审核请求。
app.Action = func(c *cli.Context) error { e := echo.New() e.POST("/image_policy", handlers.PostImagePolicy()) e.POST("/", handlers.PostValidatingAdmission()) e.Use(middleware.LoggerWithConfig(middleware.LoggerConfig{ Format: "method=${method}, uri=${uri}, status=${status}\n", })) if debug { e.Logger.SetLevel(log.DEBUG) } if whitelist != "" { handlers.RegistryWhitelist = strings.Split(whitelist, ",") fmt.Printf( "Accepting only images from these registries: %+v\n", handlers.RegistryWhitelist) fmt.Println("WARN: this feature is implemented only by the ValidatingAdmissionWebhook code") } else { fmt.Println("WARN: accepting images from ALL registries") } var err error if cert != "" && key != "" { err = e.StartTLS(fmt.Sprintf(":%d", port), cert, key) } else { err = e.Start(fmt.Sprintf(":%d", port)) } if err != nil { return cli.NewExitError(err, 1) } return nil } app.Run(os.Args)
以下是源码handlers/validating_admission.go的一部分,基本上它会解析并验证是否应允许该镜像,然后将Allowed设置为true或false的标志返还给AdmissionReponse。如果你想更多地了解此处使用的不同类型,可以参考 v1beta1.Admission Documentation
func PostValidatingAdmission() echo.HandlerFunc { return func(c echo.Context) error { var admissionReview v1beta1.AdmissionReview err := c.Bind(&admissionReview) if err != nil { c.Logger().Errorf("Something went wrong while unmarshalling admission review: %+v", err) return c.JSON(http.StatusBadRequest, err) } c.Logger().Debugf("admission review: %+v", admissionReview) pod := v1.Pod{} if err := json.Unmarshal(admissionReview.Request.Object.Raw, &pod); err != nil { c.Logger().Errorf("Something went wrong while unmarshalling pod object: %+v", err) return c.JSON(http.StatusBadRequest, err) } c.Logger().Debugf("pod: %+v", pod) admissionReview.Response = &v1beta1.AdmissionResponse{ Allowed: true, UID: admissionReview.Request.UID, } images := []string{} for _, container := range pod.Spec.Containers { images = append(images, container.Image) usingLatest, err := rules.IsUsingLatestTag(container.Image) if err != nil { c.Logger().Errorf("Error while parsing image name: %+v", err) return c.JSON(http.StatusInternalServerError, "error while parsing image name") } if usingLatest { admissionReview.Response.Allowed = false admissionReview.Response.Result = &metav1.Status{ Message: "Images using latest tag are not allowed", } break } if len(RegistryWhitelist) > 0 { validRegistry, err := rules.IsFromWhiteListedRegistry( container.Image, RegistryWhitelist) if err != nil { c.Logger().Errorf("Error while looking for image registry: %+v", err) return c.JSON( http.StatusInternalServerError, "error while looking for image registry") } if !validRegistry { admissionReview.Response.Allowed = false admissionReview.Response.Result = &metav1.Status{ Message: "Images from a non whitelisted registry", } break } } } if admissionReview.Response.Allowed { c.Logger().Debugf("All images accepted: %v", images) } else { c.Logger().Infof("Rejected images: %v", images) } c.Logger().Debugf("admission response: %+v", admissionReview.Response) return c.JSON(http.StatusOK, admissionReview) } }
本文案例仓库地址。
译文链接: https://dzone.com/articles/kubernetes-image-policy-webhook-explained
登录后评论
立即登录 注册