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

2023-06-15  本文已影响0人  Teddy_b

概要

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

总体流程

image.png

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

通过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的认证器的认证流程就比较清晰了,总结一下:

授权过程

认证请求完成后,成功拿到了请求的用户和组信息,同样的自定义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
    }

}

参考

上一篇下一篇

猜你喜欢

热点阅读