快速了解Kubernetes中的ImagePolicyWebhook

介绍

在本文中,我们将探讨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

K8S中文社区微信公众号

评论 抢沙发

登录后评论

立即登录