忿忿的泥巴坨 发表于 2022-8-25 19:17:39

深入解析Kubernetes admission webhooks

BACKGROUND

admission controllers的特点:

[*]可定制性:准入功能可针对不同的场景进行调整。
[*]可预防性:审计则是为了检测问题,而准入控制器可以预防问题发生
[*]可扩展性:在kubernetes自有的验证机制外,增加了另外的防线,弥补了RBAC仅能对资源提供安全保证。
下图,显示了用户操作资源的流程,可以看出 admission controllers 作用是在通过身份验证资源持久化之前起到拦截作用。在准入控制器的加入会使kubernetes增加了更高级的安全功能。
https://img2022.cnblogs.com/blog/1380340/202207/1380340-20220711212041601-34320373.png
图:Kubernetes API 请求的请求处理步骤图Source:https://kubernetes.io/blog/2019/03/21/a-guide-to-kubernetes-admission-controllers/这里找到一个大佬博客画的图,通过两张图可以很清晰的了解到admission webhook流程,与官方给出的不一样的地方在于,这里清楚地定位了kubernetes admission webhook 处于准入控制中,RBAC之后,push 之前。
https://img2022.cnblogs.com/blog/1380340/202207/1380340-20220711212051482-1849156450.png
图:Kubernetes API 请求的请求处理步骤图(详细)Source:https://www.armosec.io/blog/kubernetes-admission-controller/两种控制器有什么区别?

根据官方提供的说法是
Mutating controllers may modify related objects to the requests they admit; validating controllers may not
从结构图中也可以看出,validating是在持久化之前,而 Mutating是在结构验证前,根据这些特性我们可以使用 Mutating 修改这个资源对象内容(如增加验证的信息),在 validating 中验证是否合法。
composition of admission controllers

kubernetes中的admission controllers 由两部分组成:

[*]内置在APIServer中的准入控制器 build-in list
[*]特殊的控制器;也是内置在APIServer中,但提供一些自定义的功能

[*]MutatingAdmission
[*]ValidatingAdmission

Mutating 控制器可以修改他们处理的资源对象,Validating 控制器不会。当在任何一个阶段中的任何控制器拒绝这个了请求,则会立即拒绝整个请求,并将错误返回。
admission webhook

由于准入控制器是内置在 kube-apiserver 中的,这种情况下就限制了admission controller的可扩展性。在这种背景下,kubernetes提供了一种可扩展的准入控制器 extensible admission controllers,这种行为叫做动态准入控制 Dynamic Admission Control,而提供这个功能的就是 admission webhook 。
admission webhook通俗来讲就是 HTTP 回调,通过定义一个http server,接收准入请求并处理。用户可以通过kubernetes提供的两种类型的 admission webhook,validating admission webhook 和 mutating admission webhook。来完成自定义的准入策略的处理。
webhook 就是
注:从上面的流程图也可以看出,admission webhook 也是有顺序的。首先调用mutating webhook,然后会调用validating webhook。
如何使用准入控制器

使用条件:kubernetes v1.16 使用 admissionregistration.k8s.io/v1 ;kubernetes v1.9 使用 admissionregistration.k8s.io/v1beta1。
如何在集群中开启准入控制器? :查看kube-apiserver 的启动参数 --enable-admission-plugins ;通过该参数来配置要启动的准入控制器,如 --enable-admission-plugins=NodeRestriction 多个准入控制器以 , 分割,顺序无关紧要。 反之使用 --disable-admission-plugins 参数可以关闭相应的准入控制器(Refer to apiserver opts)。
通过 kubectl 命令可以看到,当前kubernetes集群所支持准入控制器的版本
$ kubectl api-versions | grep admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1
admissionregistration.k8s.io/v1beta1webhook工作原理

通过上面的学习,已经了解到了两种webhook的工作原理如下所示:
mutating webhook,会在持久化前拦截在 MutatingWebhookConfiguration 中定义的规则匹配的请求。MutatingAdmissionWebhook 通过向 mutating webhook 服务器发送准入请求来执行验证。
validaing webhook,会在持久化前拦截在 ValidatingWebhookConfiguration 中定义的规则匹配的请求。ValidatingAdmissionWebhook 通过将准入请求发送到 validating webhook server来执行验证。
那么接下来将从源码中看这个在这个工作流程中,究竟做了些什么?
资源类型

对于 1.9 版本之后,也就是 v1 版本 ,admission 被定义在 k8s.io\api\admissionregistration\v1\types.go ,大同小异,因为本地只有1.18集群,所以以这个讲解。
对于 Validating Webhook 来讲实现主要都在webhook中
type ValidatingWebhookConfiguration struct {
    // 每个api必须包含下列的metadata,这个是kubernetes规范,可以在注释中的url看到相关文档
        metav1.TypeMeta `json:",inline"`
        metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
        // Webhooks在这里被表示为[]ValidatingWebhook,表示我们可以注册多个
        // +optional
        // +patchMergeKey=name
        // +patchStrategy=merge
        Webhooks []ValidatingWebhook `json:"webhooks,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,2,rep,name=Webhooks"`
}webhook,则是对这种类型的webhook提供的操作、资源等。对于这部分不做过多的注释了,因为这里本身为kubernetes API资源,官网有很详细的例子与说明。这里更多字段的意思的可以参考官方 doc
type ValidatingWebhook struct {
        //admission webhook的名词,Required
        Name string `json:"name" protobuf:"bytes,1,opt,name=name"`

        // ClientConfig 定义了与webhook通讯的方式 Required
        ClientConfig WebhookClientConfig `json:"clientConfig" protobuf:"bytes,2,opt,name=clientConfig"`

        // rule表示了webhook对于哪些资源及子资源的操作进行关注
        Rules []RuleWithOperations `json:"rules,omitempty" protobuf:"bytes,3,rep,name=rules"`

        // FailurePolicy 对于无法识别的value将如何处理,allowed/Ignore optional
        FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"`

        // matchPolicy 定义了如何使用“rules”列表来匹配传入的请求。
        MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,9,opt,name=matchPolicy,casttype=MatchPolicyType"`
        NamespaceSelector *metav1.LabelSelector `json:"namespaceSelector,omitempty" protobuf:"bytes,5,opt,name=namespaceSelector"`
        SideEffects *SideEffectClass `json:"sideEffects" protobuf:"bytes,6,opt,name=sideEffects,casttype=SideEffectClass"`
        AdmissionReviewVersions []string `json:"admissionReviewVersions" protobuf:"bytes,8,rep,name=admissionReviewVersions"`
}到这里了解了一个webhook资源的定义,那么这个如何使用呢?通过 Find Usages 找到一个 k8s.io/apiserver/pkg/admission/plugin/webhook/accessors.go 在使用它。这里没有注释,但在结构上可以看出,包含客户端与一系列选择器组成
type mutatingWebhookAccessor struct {
        *v1.MutatingWebhook
        uid               string
        configurationName string

        initObjectSelector sync.Once
        objectSelector   labels.Selector
        objectSelectorErrerror

        initNamespaceSelector sync.Once
        namespaceSelector   labels.Selector
        namespaceSelectorErrerror

        initClient sync.Once
        client   *rest.RESTClient
        clientErrerror
}accessor 因为包含了整个webhookconfig定义的一些动作(这里个人这么觉得)。
accessor.go 下面 有一个 GetRESTClient 方法 ,通过这里可以看出,这里做的就是使用根据 accessor 构造一个客户端。
func (m *mutatingWebhookAccessor) GetRESTClient(clientManager *webhookutil.ClientManager) (*rest.RESTClient, error) {
        m.initClient.Do(func() {
                m.client, m.clientErr = clientManager.HookClient(hookClientConfigForWebhook(m))
        })
        return m.client, m.clientErr
}到这步骤已经没必要往下看了,因已经知道这里是请求webhook前的步骤了,下面就是何时请求了。
k8s.io\apiserver\pkg\admission\plugin\webhook\validating\dispatcher.go 下面有两个方法,Dispatch去请求我们自己定义的webhook
func (d *validatingDispatcher) Dispatch(ctx context.Context, attr admission.Attributes, o admission.ObjectInterfaces, hooks []webhook.WebhookAccessor) error {
        var relevantHooks []*generic.WebhookInvocation
        // Construct all the versions we need to call our webhooks
        versionedAttrs := map*generic.VersionedAttributes{}
        for _, hook := range hooks {
                invocation, statusError := d.plugin.ShouldCallHook(hook, attr, o)
                if statusError != nil {
                        return statusError
                }
                if invocation == nil {
                        continue
                }
                relevantHooks = append(relevantHooks, invocation)
                // If we already have this version, continue
                if _, ok := versionedAttrs; ok {
                        continue
                }
                versionedAttr, err := generic.NewVersionedAttributes(attr, invocation.Kind, o)
                if err != nil {
                        return apierrors.NewInternalError(err)
                }
                versionedAttrs = versionedAttr
        }

        if len(relevantHooks) == 0 {
                // no matching hooks
                return nil
        }

        // Check if the request has already timed out before spawning remote calls
        select {
        case <-ctx.Done():
                // parent context is canceled or timed out, no point in continuing
                return apierrors.NewTimeoutError("request did not complete within requested timeout", 0)
        default:
        }

        wg := sync.WaitGroup{}
        errCh := make(chan error, len(relevantHooks))
        wg.Add(len(relevantHooks))
    // 循环所有相关的注册的hook
        for i := range relevantHooks {
                go func(invocation *generic.WebhookInvocation) {
                        defer wg.Done()
            // invacation 中有一个 Accessor,Accessor注册了一个相关的webhookconfig
            // 也就是我们 kubectl -f 注册进来的那个webhook的相关配置
                        hook, ok := invocation.Webhook.GetValidatingWebhook()
                        if !ok {
                                utilruntime.HandleError(fmt.Errorf("validating webhook dispatch requires v1.ValidatingWebhook, but got %T", hook))
                                return
                        }
                        versionedAttr := versionedAttrs
                        t := time.Now()
            // 调用了callHook去请求我们自定义的webhook
                        err := d.callHook(ctx, hook, invocation, versionedAttr)
                        ignoreClientCallFailures := hook.FailurePolicy != nil && *hook.FailurePolicy == v1.Ignore
                        rejected := false
                        if err != nil {
                                switch err := err.(type) {
                                case *webhookutil.ErrCallingWebhook:
                                        if !ignoreClientCallFailures {
                                                rejected = true
                                                admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionCallingWebhookError, 0)
                                        }
                                case *webhookutil.ErrWebhookRejection:
                                        rejected = true
                                        admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionNoError, int(err.Status.ErrStatus.Code))
                                default:
                                        rejected = true
                                        admissionmetrics.Metrics.ObserveWebhookRejection(hook.Name, "validating", string(versionedAttr.Attributes.GetOperation()), admissionmetrics.WebhookRejectionAPIServerInternalError, 0)
                                }
                        }
                        admissionmetrics.Metrics.ObserveWebhook(time.Since(t), rejected, versionedAttr.Attributes, "validating", hook.Name)
                        if err == nil {
                                return
                        }

                        if callErr, ok := err.(*webhookutil.ErrCallingWebhook); ok {
                                if ignoreClientCallFailures {
                                        klog.Warningf("Failed calling webhook, failing open %v: %v", hook.Name, callErr)
                                        utilruntime.HandleError(callErr)
                                        return
                                }

                                klog.Warningf("Failed calling webhook, failing closed %v: %v", hook.Name, err)
                                errCh <- apierrors.NewInternalError(err)
                                return
                        }

                        if rejectionErr, ok := err.(*webhookutil.ErrWebhookRejection); ok {
                                err = rejectionErr.Status
                        }
                        klog.Warningf("rejected by webhook %q: %#v", hook.Name, err)
                        errCh <- err
                }(relevantHooks)
        }
        wg.Wait()
        close(errCh)

        var errs []error
        for e := range errCh {
                errs = append(errs, e)
        }
        if len(errs) == 0 {
                return nil
        }
        if len(errs) > 1 {
                for i := 1; i < len(errs); i++ {
                        // TODO: merge status errors; until then, just return the first one.
                        utilruntime.HandleError(errs)
                }
        }
        return errs
}MutatingAdmission

apiVersion: admissionregistration.k8s.io/v1kind: ValidatingWebhookConfigurationmetadata:name: "valipod-policy.example.com"webhooks:- name: "valipod-policy.example.com"rules:    - apiGroups:   ["apps"] # 拦截资源的Group "" 表示 core。"*" 表示所有。      apiVersions: ["v1"] # 拦截资源的版本      operations:["CREATE"] # 什么请求下拦截      resources:   ["deployments"]# 拦截什么资源      scope:       "Namespaced" # 生效的范围,cluster还是namespace "*"表示没有范围限制。clientConfig: # 我们部署的webhook服务,    url: "https://10.0.0.1:81/validate" # 这里是外部模式    #      service: # service是在cluster-in模式下    #      namespace: "default"    #      name: "admission-webhook"    #      port: 81 # 服务的端口    #      path: "/mutate" # path是对应用于验证的接口    # caBundle是提供给 admission webhook CA证书    caBundle: "Ci0tLS0tQk...
页: [1]
查看完整版本: 深入解析Kubernetes admission webhooks