From b3bae73cbd90b53139a4b7f697aafd4fe4ee25e1 Mon Sep 17 00:00:00 2001 From: yinwenqin Date: Thu, 4 Feb 2021 12:04:21 +0800 Subject: [PATCH] apiserver p4 --- .DS_Store | Bin 12292 -> 12292 bytes README.md | 2 - ...-APIServer-P4-APIServer的鉴权机制.md | 655 ++++++++++++++++++ apiServer/README.md | 4 +- package-lock.json | 3 - 5 files changed, 657 insertions(+), 7 deletions(-) create mode 100644 apiServer/Kubernetes源码学习-APIServer-P4-APIServer的鉴权机制.md delete mode 100644 package-lock.json diff --git a/.DS_Store b/.DS_Store index 04a0783543a054fcef1ca6e040d7907e25e769b4..57f010a35e27abea1bfb0167c924450a49169f7a 100644 GIT binary patch delta 20 bcmZokXi3 `pkg/auth/authorizer/abac/abac.go:117` + +```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 + // 资源类型与规则匹配(包含namespace/APIGroup/Resource) + if resourceMatches(p, a) { + return true + } + // 针对非资源对象的操作匹配(请求路径匹配) + if nonResourceMatches(p, a) { + return true + } + } + } + return false +} + +``` + + + +### RBAC鉴权器 + +#### 简介 + +基于角色(Role)的访问控制(RBAC)是一种基于组织中用户的角色来调节控制对 计算机或网络资源的访问的方法。 + +RBAC 鉴权机制使用 `rbac.authorization.k8s.io` [API 组](https://kubernetes.io/zh/docs/concepts/overview/kubernetes-api/#api-groups) 来驱动鉴权决定,允许你通过 Kubernetes API 动态配置策略。 + +通过创建`Role` 或` ClusterRole`来描述具体的资源授权策略,再通过创建RoleBinding/ClusterRoleBinding将策略绑定到用户/群组/服务上。 + +RBAC模式的详细描述和使用样例请参考我之前的文章: + +[k8s(十四)、RBAC权限控制](https://blog.upweto.top/2018/12/06/k8s(%E5%8D%81%E5%9B%9B)%E3%80%81RBAC%E6%9D%83%E9%99%90%E6%8E%A7%E5%88%B6.html) + + + +#### 代码实现 + +`plugin/pkg/auth/authorizer/rbac/rbac.go:74` + +```go +func (r *RBACAuthorizer) Authorize(requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) { + ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes} + // 规则解析器解析请求的属性,返回鉴权结果,判断匹配用的是ruleCheckingVisitor.visit方法 + r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit) + if ruleCheckingVisitor.allowed { + return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil + } + + ... + + reason := "" + if len(ruleCheckingVisitor.errors) > 0 { + reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors)) + } + return authorizer.DecisionNoOpinion, reason, nil +} +``` + +对比规则和请求属性,返回true or false的visit方法: + +`plugin/pkg/auth/authorizer/rbac/rbac.go:62` + +```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()) + // 返回false是为了提前break鉴权过程 + return false + } + if err != nil { + v.errors = append(v.errors, err) + } + return true +} +``` + + + +获取规则并调用visit方法的是`VisitRulesFor`接口方法,找一下`VisitRulesFor`方法: + +`pkg/registry/rbac/validation/rule.go:178` + +```go + +func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) { + // 先拿到所有的ClusterRoleBinding对象,ClusterRoleBinding资源是cluster级别的,不区分命名空间 + if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil { + if !visitor(nil, nil, err) { + return + } + } else { + sourceDescriber := &clusterRoleBindingDescriber{} + for _, clusterRoleBinding := range clusterRoleBindings { + // clusterRoleBinding.Subjects指定的绑定的用户对象,对比请求的所属用户,不匹配则continue + subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "") + if !applies { + continue + } + rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "") + if err != nil { + if !visitor(nil, nil, err) { + return + } + continue + } + sourceDescriber.binding = clusterRoleBinding + sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex] + for i := range rules { + // visit方法返回false是代表鉴权成功了,提前break鉴权过程 + if !visitor(sourceDescriber, &rules[i], nil) { + return + } + } + } + } + + // 如果指定了namespace,再取命名空间级别的roleBinding资源对象,重复一次上面的过程 + 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 + } + } + } + } + } +} + +``` + + + +#### 过程总结 + +参考代码片中的注释,rbac鉴权过程如下: + +- 1.取到所有的clusterRoleBinding/roleBindings资源对象,遍历它们对比请求用户 +- 2.对比roleBindings/clusterRoleBinding指向的用户(主体)与请求用户,相同则选中,不相同continue +- 3.对比规则与请求属性,符合则提前结束鉴权 + + + +### Node鉴权器 + +上面有提过,node鉴权器是专为kubelet组件设计的,按照kubeadm集群的默认配置,它是排序在第一位的鉴权器,为什么把它放在后面再讲呢,因为node鉴权器本质上也是利用了rbac鉴权器,是通过为`system:node`这个内置用户授权来实现的,来看一下。 + +#### 默认Node规则生成 + +`plugin/pkg/auth/authorizer/rbac/bootstrappolicy/policy.go:97` + +```go +func NodeRules() []rbacv1.PolicyRule { + nodePolicyRules := []rbacv1.PolicyRule{ + // Needed to check API access. These creates are non-mutating + rbacv1helpers.NewRule("create").Groups(authenticationGroup).Resources("tokenreviews").RuleOrDie(), + rbacv1helpers.NewRule("create").Groups(authorizationGroup).Resources("subjectaccessreviews", "localsubjectaccessreviews").RuleOrDie(), + + // Needed to build serviceLister, to populate env vars for services + rbacv1helpers.NewRule(Read...).Groups(legacyGroup).Resources("services").RuleOrDie(), + + // Nodes can register Node API objects and report status. + // Use the NodeRestriction admission plugin to limit a node to creating/updating its own API object. + rbacv1helpers.NewRule("create", "get", "list", "watch").Groups(legacyGroup).Resources("nodes").RuleOrDie(), + rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("nodes/status").RuleOrDie(), + rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("nodes").RuleOrDie(), + + // TODO: restrict to the bound node as creator in the NodeRestrictions admission plugin + rbacv1helpers.NewRule("create", "update", "patch").Groups(legacyGroup).Resources("events").RuleOrDie(), + + // TODO: restrict to pods scheduled on the bound node once field selectors are supported by list/watch authorization + rbacv1helpers.NewRule(Read...).Groups(legacyGroup).Resources("pods").RuleOrDie(), + + // Needed for the node to create/delete mirror pods. + // Use the NodeRestriction admission plugin to limit a node to creating/deleting mirror pods bound to itself. + rbacv1helpers.NewRule("create", "delete").Groups(legacyGroup).Resources("pods").RuleOrDie(), + // Needed for the node to report status of pods it is running. + // Use the NodeRestriction admission plugin to limit a node to updating status of pods bound to itself. + rbacv1helpers.NewRule("update", "patch").Groups(legacyGroup).Resources("pods/status").RuleOrDie(), + // Needed for the node to create pod evictions. + // Use the NodeRestriction admission plugin to limit a node to creating evictions for pods bound to itself. + rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("pods/eviction").RuleOrDie(), + + // Needed for imagepullsecrets, rbd/ceph and secret volumes, and secrets in envs + // Needed for configmap volume and envs + // Use the Node authorization mode to limit a node to get secrets/configmaps referenced by pods bound to itself. + rbacv1helpers.NewRule("get", "list", "watch").Groups(legacyGroup).Resources("secrets", "configmaps").RuleOrDie(), + // Needed for persistent volumes + // Use the Node authorization mode to limit a node to get pv/pvc objects referenced by pods bound to itself. + rbacv1helpers.NewRule("get").Groups(legacyGroup).Resources("persistentvolumeclaims", "persistentvolumes").RuleOrDie(), + + // TODO: add to the Node authorizer and restrict to endpoints referenced by pods or PVs bound to the node + // Needed for glusterfs volumes + rbacv1helpers.NewRule("get").Groups(legacyGroup).Resources("endpoints").RuleOrDie(), + // Used to create a certificatesigningrequest for a node-specific client certificate, and watch + // for it to be signed. This allows the kubelet to rotate it's own certificate. + rbacv1helpers.NewRule("create", "get", "list", "watch").Groups(certificatesGroup).Resources("certificatesigningrequests").RuleOrDie(), + } + + if utilfeature.DefaultFeatureGate.Enabled(features.ExpandPersistentVolumes) { + // Use the Node authorization mode to limit a node to update status of pvc objects referenced by pods bound to itself. + // Use the NodeRestriction admission plugin to limit a node to just update the status stanza. + pvcStatusPolicyRule := rbacv1helpers.NewRule("get", "update", "patch").Groups(legacyGroup).Resources("persistentvolumeclaims/status").RuleOrDie() + nodePolicyRules = append(nodePolicyRules, pvcStatusPolicyRule) + } + + if utilfeature.DefaultFeatureGate.Enabled(features.TokenRequest) { + // Use the Node authorization to limit a node to create tokens for service accounts running on that node + // Use the NodeRestriction admission plugin to limit a node to create tokens bound to pods on that node + tokenRequestRule := rbacv1helpers.NewRule("create").Groups(legacyGroup).Resources("serviceaccounts/token").RuleOrDie() + nodePolicyRules = append(nodePolicyRules, tokenRequestRule) + } + + // CSI + if utilfeature.DefaultFeatureGate.Enabled(features.CSIPersistentVolume) { + volAttachRule := rbacv1helpers.NewRule("get").Groups(storageGroup).Resources("volumeattachments").RuleOrDie() + nodePolicyRules = append(nodePolicyRules, volAttachRule) + if utilfeature.DefaultFeatureGate.Enabled(features.CSIDriverRegistry) { + csiDriverRule := rbacv1helpers.NewRule("get", "watch", "list").Groups("storage.k8s.io").Resources("csidrivers").RuleOrDie() + nodePolicyRules = append(nodePolicyRules, csiDriverRule) + } + } + if utilfeature.DefaultFeatureGate.Enabled(features.KubeletPluginsWatcher) && + utilfeature.DefaultFeatureGate.Enabled(features.CSINodeInfo) { + csiNodeInfoRule := rbacv1helpers.NewRule("get", "create", "update", "patch", "delete").Groups("storage.k8s.io").Resources("csinodes").RuleOrDie() + nodePolicyRules = append(nodePolicyRules, csiNodeInfoRule) + } + + // Node leases + if utilfeature.DefaultFeatureGate.Enabled(features.NodeLease) { + nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "create", "update", "patch", "delete").Groups("coordination.k8s.io").Resources("leases").RuleOrDie()) + } + + // RuntimeClass + if utilfeature.DefaultFeatureGate.Enabled(features.RuntimeClass) { + nodePolicyRules = append(nodePolicyRules, rbacv1helpers.NewRule("get", "list", "watch").Groups("node.k8s.io").Resources("runtimeclasses").RuleOrDie()) + } + return nodePolicyRules +} +``` + +这里初始化了kubelet工作所需要的资源的权限,如(node/pod/cm/secret/pvc等) + + + +#### Authorize代码实现 + +`plugin/pkg/auth/authorizer/node/node_authorizer.go:80` + +```go +func (r *NodeAuthorizer) Authorize(attrs authorizer.Attributes) (authorizer.Decision, string, error) { + // 判断是不是node发起的请求(所属的group是不是system:node) + nodeName, isNode := r.identifier.NodeIdentity(attrs.GetUser()) + if !isNode { + // reject requests from non-nodes + return authorizer.DecisionNoOpinion, "", nil + } + if len(nodeName) == 0 { + // reject requests from unidentifiable nodes + klog.V(2).Infof("NODE DENY: unknown node for user %q", attrs.GetUser().GetName()) + return authorizer.DecisionNoOpinion, fmt.Sprintf("unknown node for user %q", attrs.GetUser().GetName()), nil + } + + // subdivide access to specific resources + if attrs.IsResourceRequest() { + // 根据请求属性(路径)获取资源类型,不同类型资源不同的方式处理 + requestResource := schema.GroupResource{Group: attrs.GetAPIGroup(), Resource: attrs.GetResource()} + switch requestResource { + case secretResource: + return r.authorizeReadNamespacedObject(nodeName, secretVertexType, attrs) + case configMapResource: + return r.authorizeReadNamespacedObject(nodeName, configMapVertexType, attrs) + case pvcResource: + if r.features.Enabled(features.ExpandPersistentVolumes) { + if attrs.GetSubresource() == "status" { + return r.authorizeStatusUpdate(nodeName, pvcVertexType, attrs) + } + } + return r.authorizeGet(nodeName, pvcVertexType, attrs) + case pvResource: + return r.authorizeGet(nodeName, pvVertexType, attrs) + case vaResource: + if r.features.Enabled(features.CSIPersistentVolume) { + return r.authorizeGet(nodeName, vaVertexType, attrs) + } + return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.CSIPersistentVolume), nil + case svcAcctResource: + if r.features.Enabled(features.TokenRequest) { + return r.authorizeCreateToken(nodeName, serviceAccountVertexType, attrs) + } + return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.TokenRequest), nil + case leaseResource: + if r.features.Enabled(features.NodeLease) { + return r.authorizeLease(nodeName, attrs) + } + return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gate %s", features.NodeLease), nil + case csiNodeResource: + if r.features.Enabled(features.KubeletPluginsWatcher) && r.features.Enabled(features.CSINodeInfo) { + return r.authorizeCSINode(nodeName, attrs) + } + return authorizer.DecisionNoOpinion, fmt.Sprintf("disabled by feature gates %s and %s", features.KubeletPluginsWatcher, features.CSINodeInfo), nil + } + + } + + // Access to other resources is not subdivided, so just evaluate against the statically defined node rules + if rbac.RulesAllow(attrs, r.nodeRules...) { + return authorizer.DecisionAllow, "", nil + } + return authorizer.DecisionNoOpinion, "", nil +} +``` + + + +### WebHook鉴权器 + +#### 简介 + +与上一篇中的WebHook认证器类似,WebHook鉴权器也是依赖于集群外部的鉴权服务器,将鉴权请求POST发送给外部的鉴权服务器。 + +`Webhook` 模式需要一个 HTTP 配置文件,通过 `--authorization-webhook-config-file=SOME_FILENAME` 的参数声明。 + +配置文件的格式使用 [kubeconfig](https://kubernetes.io/zh/docs/tasks/access-application-cluster/configure-access-multiple-clusters/)。在文件中,"users" 代表着 API 服务器的 webhook,而 "cluster" 代表着远程服务。 + +使用 HTTPS 客户端认证的配置例子: + +```yaml +# Kubernetes API 版本 +apiVersion: v1 +# API 对象种类 +kind: Config +# clusters 代表远程服务。 +clusters: + - name: name-of-remote-authz-service + cluster: + # 对远程服务进行身份认证的 CA。 + certificate-authority: /path/to/ca.pem + # 远程服务的查询 URL。必须使用 'https'。 + server: https://authz.example.com/authorize + +# users 代表 API 服务器的 webhook 配置 +users: + - name: name-of-api-server + user: + client-certificate: /path/to/cert.pem # webhook plugin 使用 cert + client-key: /path/to/key.pem # cert 所对应的 key + +# kubeconfig 文件必须有 context。需要提供一个给 API 服务器。 +current-context: webhook +contexts: +- context: + cluster: name-of-remote-authz-service + user: name-of-api-server + name: webhook +``` + +> 摘自官方文档[Webhook 模式](https://kubernetes.io/zh/docs/reference/access-authn-authz/webhook/) + +#### 代码实现 + +`vendor/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go:152` + +```go +func (w *WebhookAuthorizer) Authorize(attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) { + r := &authorization.SubjectAccessReview{} + if user := attr.GetUser(); user != nil { + r.Spec = authorization.SubjectAccessReviewSpec{ + User: user.GetName(), + UID: user.GetUID(), + Groups: user.GetGroups(), + Extra: convertToSARExtra(user.GetExtra()), + } + } + + 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(), + } + } + // 将请求的主体/资源/操作等字段放在一个json里 + key, err := json.Marshal(r.Spec) + if err != nil { + return w.decisionOnError, "", err + } + // 从本地的缓存里取,有则不发起远端post请求了 + 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 { + // 缓存里没有,则发起post请求给远端鉴权服务器 + 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 + // 长度不超过10000则缓存结果 + 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) + } + } + } + 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 + } + +} +``` + + + +## 总结 + +鉴权的流程与认证的流程大体类似,但也有所不同,例如认证器链的执行顺序是无序的,而鉴权器链的执行顺序是有序的(按参数指定的顺序)。另外鉴权器的数量没有认证器那么多,因此相对容易理解一些。 + diff --git a/apiServer/README.md b/apiServer/README.md index 7e74c8b..b138769 100644 --- a/apiServer/README.md +++ b/apiServer/README.md @@ -6,9 +6,9 @@ - [APIServer的启动流程](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P2-启动流程.md) -- [APIServer的认证机制](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P3-APIServer的认证控制.md) +- [APIServer的认证机制](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P3-APIServer的认证机制.md) -- [APIServer的授权机制](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P4-APIServer的授权控制.md) +- [APIServer的鉴权机制](https://github.com/yinwenqin/kubeSourceCodeNote/blob/master/apiServer/Kubernetes源码学习-APIServer-P4-APIServer的鉴权机制.md) diff --git a/package-lock.json b/package-lock.json deleted file mode 100644 index 48e341a..0000000 --- a/package-lock.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "lockfileVersion": 1 -}