Kube-apiserver 认证鉴权插件Authenticator和Authorizer

欧阳学真
2023-12-01

Kube-apiserver 认证鉴权插件Authenticator和Authorizer

原文链接:https://note.youdao.com/ynoteshare1/index.html?id=9d0b804336ce5f4009d35848bc3acded&type=note

一、初始化入口

cmd/kube-apiserver/app/server.go中的BuildAuthenticator函数初始化kube-apiserver的认证插件,这个函数又调用了pkg/kubeapiserver/authenticator/Config.New()添加了很多authenticator。

 

cmd/kube-apiserver/app/server.go中的BuildAuthorizer函数初始化kube-apiserver的鉴权插件,这个函数又调用了pkg/kubeapiserver/authorizer/Config.New()初始化各种authorizers

 

二、Authenticator列表

 

1.requestHeaderAuthenticator:

请求头authenticator,实际是上是x509.Verifier(staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go),用CA证书进行认证。用户信息是从请求头指定的Key中获取的,如X-Remote-User,X-Remote-Group

 

2.basicAuth:

basicAuthFile里保存的用户名密码,请求头中 Authorization:Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ== 获取用户名密码进行对比

 

3.CertAuth(ClientCA ) :

staging/src/k8s.io/apiserver/pkg/authentication/request/x509/x509.go.Authenticator, 使用ClientCAFile校验证书,用户信息是x509.CommonNameUserConversion函数从客户端证书中获取的(User:CommonName,Groups:Organization)

 

 

4.tokenAuthenticators:

包含多种token authenticator的数组:

a. TokenAuthFile:本地token file保存的token

b. LegacyServiceAccountAuthenticator: jwt格式token,JWT payload中的private claim是 pkg/serviceaccount/legacy.go.legacyPrivateClaims 结构。jwt token格式为 header.payload.signature,header和 payload都是base64编码,signature是把前两部分用privateKey加密的,服务端解析时用publicKey解密

c. ServiceAccountAuthenticator:也是jwt token, JWT payload中的private claim是pkg/serviceaccount/claims.go.validator字段

d. BootstrapTokenAuthenticator:格式为(token-id).(token-secret),用token-secret和bootstrap-token-<token-id> secret中保存的token对比

e. oidcAuth: 也是一种jwt格式token,可以从issuerUrl获取token_endpoint、jwks_uri,keyfile可以从jwks_uri获取

f. 调用webhook插件解析token

 

二、Authorizer列表

 

1. AlwaysAllow 全部通过

2.AlwaysDeny 全部拒绝

3.Node

对来自kubelet的请求鉴权

// NodeAuthorizer authorizes requests from kubelets, with the following logic:
// 1. If a request is not from a node (NodeIdentity() returns isNode=false), reject
// 2. If a specific node cannot be identified (NodeIdentity() returns nodeName=""), reject
// 3. If a request is for a secret, configmap, persistent volume or persistent volume claim, reject unless the verb is get, and the requested object is related to the requesting node:
//    node <- configmap
//    node <- pod
//    node <- pod <- secret
//    node <- pod <- configmap
//    node <- pod <- pvc
//    node <- pod <- pvc <- pv
//    node <- pod <- pvc <- pv <- secret
// 4. For other resources, authorize all nodes uniformly using statically defined rules

 

4.ABAC

基于属性的鉴权,--authorization-policy-file=SOME_FILENAME 指定策略文件

 

举例:Kubelet可以读取任何pod:

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "kubelet", "namespace": "*", "resource": "pods", "readonly": true}}

 

鉴权就是遍历每一个Policy,执行下面的matches函数,先检查user和group是否匹配,再检查verb动作是否匹配,最后检查资源是否匹配。

// pkg/auth/authorizer/abac/abac.go
func matches(p abac.Policy, a authorizer.Attributes) bool {
   if subjectMatches(p, a.GetUser()) {
      if verbMatches(p, a) {
         // Resource and non-resource requests are mutually exclusive, at most one will match a policy
         if resourceMatches(p, a) {
            return true
         }
         if nonResourceMatches(p, a) {
            return true
         }
      }
   }
   return false
}

 

更多示例见:http://docs.kubernetes.org.cn/87.html

 

5.RBAC

基于角色的鉴权,通过ClusterRoleBinding、RoleBinding找到和该用户(或者用户组)绑定的ClusterRole、Role,ClusterRole和Role就是用户拥有的角色,在ClusterRole和Role中找到具体的rules,这些rules就是用户可以通过鉴权的白名单。然后调用visitor函数检查该rule和请求的Verb、APIGroup、Resource等是否匹配。

// pkg/registry/rbac/validation/rule.go
func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
   // 查询所有的clusterRoleBindings
   if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil {
      if !visitor(nil, nil, err) {
         return
      }
   } else {
      sourceDescriber := &clusterRoleBindingDescriber{}
      for _, clusterRoleBinding := range clusterRoleBindings {
          // 遍历clusterRoleBindings,检查是否该用户或者用户组有绑定关系,没有的跳过
         subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "")
         if !applies {
            continue
         }
         // 找到clusterRoleBinding绑定的clusterRole,从clusterRole中获取该角色拥有的权限规则rules
         rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
         if err != nil { 
            if !visitor(nil, nil, err) {
               return
            }
            continue
         }
         sourceDescriber.binding = clusterRoleBinding
         sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
         // 调用visitor函数检查每个rule和请求的Verb、APIGroup、Resource等是否匹配。
         for i := range rules {
            if !visitor(sourceDescriber, &rules[i], nil) {
               return
            }
         }
      }
   }
    // roleBinding是和namespace关联的,请求中带有namespace时,查询所有的roleBindings,
    // 逻辑和clusterRoleBindings类似,找到roleBinding对应的Role,获取Role中的鉴权规则rules,调用visitor函数做校验
   if len(namespace) > 0 {
      if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil {
         if !visitor(nil, nil, err) {
            return
         }
      } else {
         sourceDescriber := &roleBindingDescriber{}
         for _, roleBinding := range roleBindings {
            subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace)
            if !applies {
               continue
            }
            rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace)
            if err != nil {
               if !visitor(nil, nil, err) {
                  return
               }
               continue
            }
            sourceDescriber.binding = roleBinding
            sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
            for i := range rules {
               if !visitor(sourceDescriber, &rules[i], nil) {
                  return
               }
            }
         }
      }
   }
}

 

visit函数是authorizingVisitor.visit,authorizingVisitor初始的时候保存了请求相关的属性requestAttributes,调用RuleAllows检查rule是否允许requestAttributes,RuleAllows中就是分别检查 Verb、APIGroup、Resource、ResourceName是否match。

// plugin/pkg/auth/authorizer/rbac/rbac.go
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
   if rule != nil && RuleAllows(v.requestAttributes, rule) {
      v.allowed = true
      v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
      return false
   }
   if err != nil {
      v.errors = append(v.errors, err)
   }
   return true
}

func RuleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool {
   if requestAttributes.IsResourceRequest() { //是关于Resource的请求
      combinedResource := requestAttributes.GetResource()
      if len(requestAttributes.GetSubresource()) > 0 {
         combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource()
      }
    // 分别检查 Verb、APIGroup、Resource、ResourceName是否match
      return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
         rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
         rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
         rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName())
   }

   return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
      rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
}

 

6.Webhook

调用webhook插件进行鉴权,把请求相关的所有信息封装到SubjectAccessReview对象中,作为请求体调用webhook插件。

代码逻辑可以看下面的代码注释,注释中包含了调用webhook的请求体和返回体样例。逻辑也比较简单,先构造请求体,填充User和Resource信息,然后看是否有缓存,又缓存就取缓存结果,否则就调用subjectAccessReview.Create函数,subjectAccessReview.Create函数就是调用Post请求把SubjectAccessReview作为请求体请求webhook插件。从请求结果中获取鉴权结果。

// Authorize makes a REST request to the remote service describing the attempted action as a JSON
// serialized api.authorization.v1beta1.SubjectAccessReview object. An example request body is
// provided below.
// 调用webhook 请求体样例
//     {
//       "apiVersion": "authorization.k8s.io/v1beta1",
//       "kind": "SubjectAccessReview",
//       "spec": {
//         "resourceAttributes": {
//           "namespace": "kittensandponies",
//           "verb": "GET",
//           "group": "group3",
//           "resource": "pods"
//         },
//         "user": "jane",
//         "group": [
//           "group1",
//           "group2"
//         ]
//       }
//     }
//
// The remote service is expected to fill the SubjectAccessReviewStatus field to either allow or
// disallow access. A permissive response would return:
//   webhook返回成功样例
//     {
//       "apiVersion": "authorization.k8s.io/v1beta1",
//       "kind": "SubjectAccessReview",
//       "status": {
//         "allowed": true
//       }
//     }
//
// To disallow access, the remote service would return:
//   webhook返回失败样例
//     {
//       "apiVersion": "authorization.k8s.io/v1beta1",
//       "kind": "SubjectAccessReview",
//       "status": {
//         "allowed": false,
//         "reason": "user does not have read access to the namespace"
//       }
//     }
//
// TODO(mikedanese): We should eventually support failing closed when we
// encounter an error. We are failing open now to preserve backwards compatible
// behavior.
func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
   r := &authorization.SubjectAccessReview{}
   // 填充请求体User信息
   if user := attr.GetUser(); user != nil {
      r.Spec = authorization.SubjectAccessReviewSpec{
         User:   user.GetName(),
         UID:    user.GetUID(),
         Groups: user.GetGroups(),
         Extra:  convertToSARExtra(user.GetExtra()),
      }
   }
    // 填充请求体Resource信息
   if attr.IsResourceRequest() {
      r.Spec.ResourceAttributes = &authorization.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 = &authorization.NonResourceAttributes{
         Path: attr.GetPath(),
         Verb: attr.GetVerb(),
      }
   }
   key, err := json.Marshal(r.Spec)
   if err != nil {
      return w.decisionOnError, "", err
   }
   // 如果有缓存就取缓存结果
   if entry, ok := w.responseCache.Get(string(key)); ok {
      r.Status = entry.(authorization.SubjectAccessReviewStatus)
   } else {
      var (
         result *authorization.SubjectAccessReview
         err    error
      )
      webhook.WithExponentialBackoff(w.initialBackoff, func() error {
          // 没有缓存就调用webhook插件获取鉴权结果,见下面的func (t *subjectAccessReviewClient) Create()函数
         result, err = w.subjectAccessReview.Create(r)
         return err
      })
      if err != nil {
         // An error here indicates bad configuration or an outage. Log for debugging.
         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)
         }
      }
   }
   // webhook插件返回的鉴权结果
   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
   }

}
// 就是一个Post请求,把SubjectAccessReview作为请求体,调用webhook插件
func (t *subjectAccessReviewClient) Create(subjectAccessReview *authorization.SubjectAccessReview) (*authorization.SubjectAccessReview, error) {
   result := &authorization.SubjectAccessReview{}
   err := t.w.RestClient.Post().Body(subjectAccessReview).Do().Into(result)
   return result, err
}

 

 类似资料: