美文网首页
K8s自定义apiserver的认证鉴权流程笔记

K8s自定义apiserver的认证鉴权流程笔记

作者: Teddy_b | 来源:发表于2023-06-15 15:30 被阅读0次

    概要

    K8s自定义apiserver是扩展kube-apiserver的其中一种方式,通过这种方式,可以很轻松的将自定义资源注册到kube-apiserver中,然后就能通过kubectl开始对自定义资源愉快的CRUD了

    总体流程

    image.png

    按照K8s官方文档给出的流程图中,可以总结三步:

    • 请求首先会在kube-apiserver中进行认证和鉴权

    • kube-apiserver认证和鉴权通过后,使用新的证书对、并将原始请求中的用户和组信息设置到新请求的Header中,然后将请求代理到自定义的apiserver

      • 新的证书对、以及设置的Header信息来自kube-apiserver的启动参数
        --requestheader-allowed-names='' \
                 --requestheader-extra-headers-prefix=X-Remote-Extra- \
                 --requestheader-group-headers=X-Remote-Group \
                 --requestheader-username-headers=X-Remote-User \
                --requestheader-client-ca-file=/etc/kubernetes/ssl/front-proxy-ca.pem
                 --proxy-client-cert-file=/etc/kubernetes/ssl/front-proxy-client.pem \
                 --proxy-client-key-file=/etc/kubernetes/ssl/front-proxy-client-key.pem \
        
      • 自定义apiserver的请求地址通过ApiService资源定义
        apiVersion: apiregistration.k8s.io/v1
        kind: APIService
        metadata:
            name: v1alpha1.cluster.karmada.io
            labels:
                  app: karmada-aggregated-apiserver
                  apiserver: "true"
        spec:
            insecureSkipTLSVerify: true
            group: cluster.karmada.io
            groupPriorityMinimum: 2000
            service:
                  name: karmada-aggregated-apiserver
                  namespace: karmada-system
            version: v1alpha1
            versionPriority: 10
        
    • 自定义apiserver在对kube-apiserver代理过来的请求进行认证和授权

    通过curl来描述这个过程:

    - 首先正常通过证书对来访问kube-apiserver:
    - curl -k --cert /path/to/cert --key /path/to/key https://2.2.2.2:6443/apis/cluster.karmada.io/v1alhpa1/clusters
    
    - kube-apiserver收到上述请求后,首先会对请求进行认证和授权,
    - 通过后,从证书中提取出用户和组信息(证书的CN作为用户名,O作为用户组)
    
    - kube-apiserver发起到自定义apiserver的新请求,
    - curl -cacert requestheader-client-ca-file  --cert proxy-client-cert-file --key proxy-client-key-file  
      -H 'X-Remote-Group: Organization' -H 'X-Remote-User: Common Name' 
      https://karamda-cluster.default.svc.cluster.local:443/apis/cluster.karmada.io/v1alhpa1/clusters
    
    - 自定义apiserver收到该请求后,也需要对该请求进行认证和授权
    

    这里我们主要关注最后一步:自定义apiserver是怎么对kube-apiserver代理过来的请求进行认证和授权?

    代码实现

    以开源项目karmada为例,它需要往kube-apiserver中注册cluster这种自定义资源,其GVR用路径表示为cluster.karmada.io/v1alhpa1/clusters,我们看下它是怎么完成第三步的

    自定义apiserver的配置

    自定义apiserver的配置在新建时会使用默认的请求处理链DefaultBuildHandlerChain

    func NewConfig(codecs serializer.CodecFactory) *Config {
        defaultHealthChecks := []healthz.HealthChecker{healthz.PingHealthz, healthz.LogHealthz}
        var id string
        if utilfeature.DefaultFeatureGate.Enabled(genericfeatures.APIServerIdentity) {
            hostname, err := os.Hostname()
            if err != nil {
                klog.Fatalf("error getting hostname for apiserver identity: %v", err)
            }
    
            hash := sha256.Sum256([]byte(hostname))
            id = "kube-apiserver-" + strings.ToLower(base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(hash[:16]))
        }
        lifecycleSignals := newLifecycleSignals()
    
        return &Config{
            Serializer:                  codecs,
                    // 默认的请求处理链
            BuildHandlerChainFunc:       DefaultBuildHandlerChain,
        }
    }
    

    自定义apiserver请求处理链

    在默认的请求处理链中,我们可以看到认证和授权都在其中

    func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
        ...
              // 对请求授权
        handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
        ...
             // 对请求认证
        handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
        ...
        return handler
    }
    

    认证过程

    认证的过程主要是使用认证器对请求进行认证,然后获取到用户和组信息,并将用户和组信息设置到上下文中,用于后续的授权

    func withAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler, apiAuds authenticator.Audiences, metrics recordMetrics) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
            
                    // 使用认证器对请求进行认证,认证成功后会返回认证的用户及其组信息
            resp, ok, err := auth.AuthenticateRequest(req)
            
                    // 认证完成后将请求Header中的Authorization删除
            req.Header.Del("Authorization")
               
                    // 将认证完成的用户和组信息设置到上下文中,用于后续的授权
            req = req.WithContext(genericapirequest.WithUser(req.Context(), resp.User))
            handler.ServeHTTP(w, req)
        })
    }
    
    认证器的创建

    认证器的创建过程在生成配置的流程里

    func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.SecurityDefinitions, error) {
        authenticators := []authenticator.Request{}
        securityDefinitions := spec.SecurityDefinitions{}
    
        // 创建一个基于请求Header参数的认证器
        if c.RequestHeaderConfig != nil {
            requestHeaderAuthenticator := headerrequest.NewDynamicVerifyOptionsSecure(
                c.RequestHeaderConfig.CAContentProvider.VerifyOptions,
                c.RequestHeaderConfig.AllowedClientNames,
                c.RequestHeaderConfig.UsernameHeaders,
                c.RequestHeaderConfig.GroupHeaders,
                c.RequestHeaderConfig.ExtraHeaderPrefixes,
            )
            authenticators = append(authenticators, requestHeaderAuthenticator)
        }
    
        // 创建一个基于证书的认证器
        if c.ClientCertificateCAContentProvider != nil {
            authenticators = append(authenticators, x509.NewDynamic(c.ClientCertificateCAContentProvider.VerifyOptions, x509.CommonNameUserConversion))
        }
    
            // 创建一个基于Token的认证器
        if c.TokenAccessReviewClient != nil {
            if c.WebhookRetryBackoff == nil {
                return nil, nil, errors.New("retry backoff parameters for delegating authentication webhook has not been specified")
            }
            tokenAuth, err := webhooktoken.NewFromInterface(c.TokenAccessReviewClient, c.APIAudiences, *c.WebhookRetryBackoff, c.TokenAccessReviewTimeout, webhooktoken.AuthenticatorMetrics{
                RecordRequestTotal:   RecordRequestTotal,
                RecordRequestLatency: RecordRequestLatency,
            })
            if err != nil {
                return nil, nil, err
            }
            cachingTokenAuth := cache.New(tokenAuth, false, c.CacheTTL, c.CacheTTL)
            authenticators = append(authenticators, bearertoken.New(cachingTokenAuth), websocket.NewProtocolAuthenticator(cachingTokenAuth))
    
            securityDefinitions["BearerToken"] = &spec.SecurityScheme{
                SecuritySchemeProps: spec.SecuritySchemeProps{
                    Type:        "apiKey",
                    Name:        "authorization",
                    In:          "header",
                    Description: "Bearer Token authentication",
                },
            }
        }
    
            // 将上面的认证器通过数组连接起来,只要有一个认证通过就行了
        authenticator := group.NewAuthenticatedGroupAdder(unionauth.New(authenticators...))
        
        return authenticator, &securityDefinitions, nil
    }
    
    基于Header的认证器

    由于这个认证器在认证器数组的第一个,所以请求会先被这个认证器进行认证
    首先对kube-apiserver代理过来的请求证书进行认证,对证书进行认证只需要有根证书就可以了

    func (a *Verifier) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
        if req.TLS == nil || len(req.TLS.PeerCertificates) == 0 {
            return nil, false, nil
        }
    
        // Use intermediates, if provided
        optsCopy, ok := a.verifyOptionsFn()
        // if there are intentionally no verify options, then we cannot authenticate this request
        if !ok {
            return nil, false, nil
        }
        if optsCopy.Intermediates == nil && len(req.TLS.PeerCertificates) > 1 {
            optsCopy.Intermediates = x509.NewCertPool()
            for _, intermediate := range req.TLS.PeerCertificates[1:] {
                optsCopy.Intermediates.AddCert(intermediate)
            }
        }
    
        if _, err := req.TLS.PeerCertificates[0].Verify(optsCopy); err != nil {
            return nil, false, err
        }
        if err := a.verifySubject(req.TLS.PeerCertificates[0].Subject); err != nil {
            return nil, false, err
        }
        return a.auth.AuthenticateRequest(req)
    }
    

    然后从请求Header中提取相关的Header信息,从Header中获取到的用户和组信息后返回成功,说明这个认证器已经对请求进行了认证,后续的认证器就不需要再进行认证了

    func (a *requestHeaderAuthRequestHandler) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
        name := headerValue(req.Header, a.nameHeaders.Value())
        if len(name) == 0 {
            return nil, false, nil
        }
        groups := allHeaderValues(req.Header, a.groupHeaders.Value())
        extra := newExtra(req.Header, a.extraHeaderPrefixes.Value())
    
        // clear headers used for authentication
        for _, headerName := range a.nameHeaders.Value() {
            req.Header.Del(headerName)
        }
        for _, headerName := range a.groupHeaders.Value() {
            req.Header.Del(headerName)
        }
        for k := range extra {
            for _, prefix := range a.extraHeaderPrefixes.Value() {
                req.Header.Del(prefix + k)
            }
        }
    
        return &authenticator.Response{
            User: &user.DefaultInfo{
                Name:   name,
                Groups: groups,
                Extra:  extra,
            },
        }, true, nil
    }
    

    上面两步中涉及到的一个关键信息是,自定义apiserver是怎么知道根证书和相关Header信息的呢?它的启动参数里是没有指定这些信息的,但是我们可以看到它的启动参数是有指定--authentication-kubeconfig=/etc/kubeconfig,指定了一个指向kube-apiserver的kubeconfig文件,一般通过挂载方式获取该文件。

    再创建认证器的时候,可以看到它通过该kubeconfig文件创建了kube-apiserver的请求客户端,然后查询集群中kube-system这个ns下的extension-apiserver-authentication这个ConfigMap

    func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.AuthenticationInfo, servingInfo *server.SecureServingInfo, openAPIConfig *openapicommon.Config) error {
        // 由于启动参数里指定了--authentication-kubeconfig=/etc/kubeconfig,因此这里可以直接根据该kubeconfig创建kube-apiserver的客户端
        client, err := s.getClient()
        if err != nil {
            return fmt.Errorf("failed to get delegated authentication kubeconfig: %v", err)
        }
    
            // 查询集群中kube-system这个ns下的extension-apiserver-authentication这个ConfigMap中key=client-ca-file的data
            // 这个ConfigMap是K8s启动kube-apiserver的时候默认会创建的,里面记录了集群的根证书
        clientCAProvider, err = dynamiccertificates.NewDynamicCAFromConfigMapController("client-ca", "kube-system", "extension-apiserver-authentication", "client-ca-file", client)
    
            // 仍然是上面这个ConfigMap,这次读取的是这些key:requestheader-client-ca-file、requestheader-username-headers、
           // requestheader-group-headers、requestheader-extra-headers-prefix、requestheader-allowed-names
        requestHeaderConfig, err = s.createRequestHeaderConfig(client)
    
        // 新建一个认证器
        authenticator, securityDefinitions, err := cfg.New()
        
        return nil
    }
    

    这个ConfigMap的内容一般如下,可以看到它记录了根证书、相关Header信息

    apiVersion: v1
    data:
      client-ca-file: |
        -----BEGIN CERTIFICATE-----
        MIIFzjCCAxxxxxxxxxxxxxxxxxxxJnzbek
        bE4=
        -----END CERTIFICATE-----
      requestheader-allowed-names: '[]'
      requestheader-client-ca-file: |
        -----BEGIN CERTIFICATE-----
        MIIxxxxxxxxxxxxxxxxxxxxxxHn0PI=
        -----END CERTIFICATE-----
      requestheader-extra-headers-prefix: '["X-Remote-Extra-"]'
      requestheader-group-headers: '["X-Remote-Group"]'
      requestheader-username-headers: '["X-Remote-User"]'
    kind: ConfigMap
    metadata:
    ...
    

    至此,基于Header的认证器的认证流程就比较清晰了,总结一下:

    • 根据启动参数--authentication-kubeconfig=/etc/kubeconfig创建到kube-apiserver的请求客户端
    • 读取集群中kube-system这个ns下的extension-apiserver-authentication这个ConfigMap,获取根证书、相关Header信息
    • 使用根证书对kube-apiserver代理过来的请求进行认证
    • 再根据相关Header信息提取请求中携带的用户和组信息,完成认证
    • 将认证完成的用户和组信息设置到上下文中,用于后续的授权

    授权过程

    认证请求完成后,成功拿到了请求的用户和组信息,同样的自定义apiserver的启动参数中有--authorization-kubeconfig=/etc/kubeconfig,指向的仍然是kube-apiserver的kubeconfig文件

    func (s *DelegatingAuthorizationOptions) ApplyTo(c *server.AuthorizationInfo) error {
            // 由于启动参数里指定了--authorization-kubeconfig=/etc/kubeconfig,因此这里可以直接根据该kubeconfig创建kube-apiserver的客户端
        client, err := s.getClient()
        
            // AlwaysAllowGroups对应的用户组为{"system:masters"}的,可以理解为特权用户组,往授权器中添加一个特权授权器
        if len(s.AlwaysAllowGroups) > 0 {
            authorizers = append(authorizers, authorizerfactory.NewPrivilegedGroups(s.AlwaysAllowGroups...))
        }
    
            // AlwaysAllowPaths对应的请求路径是{"/healthz", "/readyz", "/livez"}的,可以理解为这些请求路径是不需要授权的,也添加一个特殊路径授权器
            if len(s.AlwaysAllowPaths) > 0 {
            a, err := path.NewAuthorizer(s.AlwaysAllowPaths)
            if err != nil {
                return nil, err
            }
            authorizers = append(authorizers, a)
        }
    
            // 不是特权组,也不是特殊路径的,走正常的授权逻辑,通过调用kube-apiserver的Authorization API
            // 发送SubjectAccessReview请求来完成授权,也添加一个授权器
            cfg := authorizerfactory.DelegatingAuthorizerConfig{
                SubjectAccessReviewClient: client.AuthorizationV1(),
                AllowCacheTTL:             s.AllowCacheTTL,
                DenyCacheTTL:              s.DenyCacheTTL,
                WebhookRetryBackoff:       s.WebhookRetryBackoff,
            }
            delegatedAuthorizer, err := cfg.New()
            if err != nil {
                return nil, err
            }
            authorizers = append(authorizers, delegatedAuthorizer)
    
            // 最终将这些授权器级联起来形成一个数组,从前往后一次进行授权,只要有一个完成了授权就可以了
        return union.New(authorizers...), nil
    }
    
    特权组的授权方式

    这种授权方式就比较简单了,由于认证完成后已经将用户和组信息记录到了请求上下文中,这里只需要从上下文中获取用户组信息,判断其组信息是否为{"system:masters"}即可

    func (r *privilegedGroupAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (authorizer.Decision, string, error) {
        
        for _, attr_group := range attr.GetUser().GetGroups() {
            for _, priv_group := range r.groups {
                if priv_group == attr_group {
                    return authorizer.DecisionAllow, "", nil
                }
            }
        }
        return authorizer.DecisionNoOpinion, "", nil
    }
    
    特殊路径的授权方式

    这种授权方式也比较简单,只需要判断请求路径是否匹配{"/healthz", "/readyz", "/livez"}之一即可

    func NewAuthorizer(alwaysAllowPaths []string) (authorizer.Authorizer, error) {
          ...
        return authorizer.AuthorizerFunc(func(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
                   // 请求路径里是否包括了资源信息,即/api/v1/nodes 或者 /apis/apps/v1/deployments这种
            if a.IsResourceRequest() {
                return authorizer.DecisionNoOpinion, "", nil
            }
    
            pth := strings.TrimPrefix(a.GetPath(), "/")
            if paths.Has(pth) {
                return authorizer.DecisionAllow, "", nil
            }
    
            for _, prefix := range prefixes {
                if strings.HasPrefix(pth, prefix) {
                    return authorizer.DecisionAllow, "", nil
                }
            }
    
            return authorizer.DecisionNoOpinion, "", nil
        }), nil
    }
    
    SubjectAccessReview授权方式

    这种授权方式需要和kube-apiserver交互,根据用户和组信息、请求路径、请求方法,构建一个SubjectAccessReview资源,由kube-apiserver去权限这个用户和组是否有权限访问这个请求路径

    func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
        r := &authorizationv1.SubjectAccessReview{}
        // 将用户、组信息写到SubjectAccessReview对象中
             if user := attr.GetUser(); user != nil {
            r.Spec = authorizationv1.SubjectAccessReviewSpec{
                User:   user.GetName(),
                UID:    user.GetUID(),
                Groups: user.GetGroups(),
                Extra:  convertToSARExtra(user.GetExtra()),
            }
        }
    
            // 将请求方法、请求路径、请求资源等写到SubjectAccessReview对象中
        if attr.IsResourceRequest() {
            r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
                Namespace:   attr.GetNamespace(),
                Verb:        attr.GetVerb(),
                Group:       attr.GetAPIGroup(),
                Version:     attr.GetAPIVersion(),
                Resource:    attr.GetResource(),
                Subresource: attr.GetSubresource(),
                Name:        attr.GetName(),
            }
        } else {
            r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
                Path: attr.GetPath(),
                Verb: attr.GetVerb(),
            }
        }
        key, err := json.Marshal(r.Spec)
             // 先尝试从缓存中获取
        if entry, ok := w.responseCache.Get(string(key)); ok {
            r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
        } else {
                    // 以失败BackOff的方式提交SubjectAccessReview资源请求,从status中获取授权结果
            var result *authorizationv1.SubjectAccessReview
            // WithExponentialBackoff will return SAR create error (sarErr) if any.
            if err := webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error {
                
                result, statusCode, sarErr = w.subjectAccessReview.Create(ctx, r, metav1.CreateOptions{})
                return sarErr
            }, webhook.DefaultShouldRetry); err != nil {
                klog.Errorf("Failed to make webhook authorizer request: %v", err)
                return w.decisionOnError, "", err
            }
    
            r.Status = result.Status
                    // 将结果缓存起来
            if shouldCache(attr) {
                if r.Status.Allowed {
                    w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
                } else {
                    w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
                }
            }
        }
    
            // 从Status中获取授权结果
        switch {
        case r.Status.Denied && r.Status.Allowed:
            return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
        case r.Status.Denied:
            return authorizer.DecisionDeny, r.Status.Reason, nil
        case r.Status.Allowed:
            return authorizer.DecisionAllow, r.Status.Reason, nil
        default:
            return authorizer.DecisionNoOpinion, r.Status.Reason, nil
        }
    
    }
    

    参考

    相关文章

      网友评论

          本文标题:K8s自定义apiserver的认证鉴权流程笔记

          本文链接:https://www.haomeiwen.com/subject/gdhkydtx.html