apiserver 处理请求的过程

1. 概述

k8s 的 apiserver 作为所有组件通信的枢纽,其重要性不言而喻。apiserver 可以对外提供基于 HTTP 的服务,那么一个请求从发出到处理,具体要经过哪些步骤呢?下面会根据代码将整个过程简单的叙述一遍,让大家可以对这个过程由大概的印象。

因为 apiserver 的代码结构并不简单,因此会尽量少的贴代码。以下分析基于 k8s 1.18

2. 请求的处理链

// 构建请求的处理链
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
   handler := genericapifilters.WithAuthorization(apiHandler, c.Authorization.Authorizer, c.Serializer)
   if c.FlowControl != nil {
      handler = genericfilters.WithPriorityAndFairness(handler, c.LongRunningFunc, c.FlowControl)
   } else {
      handler = genericfilters.WithMaxInFlightLimit(handler, c.MaxRequestsInFlight, c.MaxMutatingRequestsInFlight, c.LongRunningFunc)
   }
   handler = genericapifilters.WithImpersonation(handler, c.Authorization.Authorizer, c.Serializer)
   handler = genericapifilters.WithAudit(handler, c.AuditBackend, c.AuditPolicyChecker, c.LongRunningFunc)
   failedHandler := genericapifilters.Unauthorized(c.Serializer, c.Authentication.SupportsBasicAuth)
   failedHandler = genericapifilters.WithFailedAuthenticationAudit(failedHandler, c.AuditBackend, c.AuditPolicyChecker)
   handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences)
   handler = genericfilters.WithCORS(handler, c.CorsAllowedOriginList, nil, nil, nil, "true")
   handler = genericfilters.WithTimeoutForNonLongRunningRequests(handler, c.LongRunningFunc, c.RequestTimeout)
   handler = genericfilters.WithWaitGroup(handler, c.LongRunningFunc, c.HandlerChainWaitGroup)
   handler = genericapifilters.WithRequestInfo(handler, c.RequestInfoResolver)
   if c.SecureServing != nil && !c.SecureServing.DisableHTTP2 && c.GoawayChance > 0 {
      handler = genericfilters.WithProbabilisticGoaway(handler, c.GoawayChance)
   }
   handler = genericfilters.WithPanicRecovery(handler)
   return handler
}

这个请求的处理链是从后向前执行的。因此请求经过的 handler 为:

  • PanicRecovery
  • ProbabilisticGoaway
  • RequestInfo
  • WaitGroup
  • TimeoutForNonLongRunningRequests
  • CORS
  • Authentication
  • failedHandler: FailedAuthenticationAudit
  • failedHandler: Unauthorized
  • Audit
  • Impersonation
  • PriorityAndFairness / MaxInFlightLimit
  • Authorization

之后传递到 director,由 director 分到 gorestfulContainer 或 nonGoRestfulMux。gorestfulContainer 是 apiserver 主要部分。

director := director{
   name:               name,
   goRestfulContainer: gorestfulContainer,
   nonGoRestfulMux:    nonGoRestfulMux,
}

PanicRecovery

runtime.HandleCrash 防止 panic,并打了日志记录 panic 的请求详情

ProbabilisticGoaway

因为 client 和 apiserver 是使用 http2 长连接的。这样即使 apiserver 有负载均衡,部分 client 的请求也会一直命中到同一个 apiserver 上。goaway 会配置一个很小的几率,在 apiserver 收到请求后响应 GOWAY 给 client,这样 client 就会新建一个 tcp 连接负载均衡到不同的 apiserver 上。这个几率的取值范围是 0~0.02

相关的 PR:https://github.com/kubernetes/kubernetes/pull/88567

RequestInfo

RequestInfo 会根据 HTTP 请求进行解析处理。得到以下的信息:

// RequestInfo holds information parsed from the http.Request
type RequestInfo struct {
    // IsResourceRequest indicates whether or not the request is for an API resource or subresource
    IsResourceRequest bool
    // Path is the URL path of the request
    Path string
    // Verb is the kube verb associated with the request for API requests, not the http verb.  This includes things like list and watch.
    // for non-resource requests, this is the lowercase http verb
    Verb string

    APIPrefix  string
    APIGroup   string
    APIVersion string
    Namespace  string
    // Resource is the name of the resource being requested.  This is not the kind.  For example: pods
    Resource string
    // Subresource is the name of the subresource being requested.  This is a different resource, scoped to the parent resource, but it may have a different kind.
    // For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
    // (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
    Subresource string
    // Name is empty for some verbs, but if the request directly indicates a name (not in body content) then this field is filled in.
    Name string
    // Parts are the path parts for the request, always starting with /{resource}/{name}
    Parts []string
}

WaitGroup

waitgroup 用来处理短连接退出的。

如何判断是不是一个长连接呢?这里是通过请求的动作或者 subresource 来判断的。watch 和 proxy 这两个动作是在 requestinfo 上通过请求的 path 来判断的。

serverConfig.LongRunningFunc = filters.BasicLongRunningRequestCheck(
  sets.NewString("watch", "proxy"),
  sets.NewString("attach", "exec", "proxy", "log", "portforward"),
)

// BasicLongRunningRequestCheck returns true if the given request has one of the specified verbs or one of the specified subresources, or is a profiler request.
func BasicLongRunningRequestCheck(longRunningVerbs, longRunningSubresources sets.String) apirequest.LongRunningRequestCheck {
    return func(r *http.Request, requestInfo *apirequest.RequestInfo) bool {
        if longRunningVerbs.Has(requestInfo.Verb) {
            return true
        }
        if requestInfo.IsResourceRequest && longRunningSubresources.Has(requestInfo.Subresource) {
            return true
        }
        if !requestInfo.IsResourceRequest && strings.HasPrefix(requestInfo.Path, "/debug/pprof/") {
            return true
        }
        return false
    }
}

这样之后的 handler 全部退出后,这个 waitgroup 的 handler 才会 done。这样就能实现优雅退出了。

TimeoutForNonLongRunningRequests

对于非长连接的请求,使用 ctx 的 cancel 来在超时后取消请求。

CORS

设置一些跨域的响应头

Authentication

开始认证用户。认证成功会从请求中移除 Authorization。然后将请求交给下一个 handler,否则将请求交给下一个 failed handler。

处理的方式有很多中。包括:

  • Requestheader,负责从请求中取出 X-Remote-User,X-Remote-Group,X-Remote-Extra
  • X509 证书校验,
  • BearerToken
  • WebSocket
  • Anonymous: 在允许匿名的情况下

还有一部分是以插件的形式提供了认证:

  • bootstrap token

  • Basic auth

  • password
  • OIDC
  • Webhook

如果有一个认证成功的话,就认为认证成功。并且如果用户是 system:anonymous 或 用户组中包含 system:unauthenticatedsystem:authenticated。就直接返回,否则修改用户信息并返回:

r.User = &user.DefaultInfo{
        Name:   r.User.GetName(),
        UID:    r.User.GetUID(),
        Groups: append(r.User.GetGroups(), user.AllAuthenticated),
        Extra:  r.User.GetExtra(),
    }

注意到,user 现在已经属于 system:authenticated。也就是认证过了。

FailedAuthenticationAudit

这个只会在认证失败后才会执行。主要是提供了审计的功能。

Unauthorized

未授权的处理,在 FailedAuthenticationAudit 之后调用

Audit

提供请求的审计功能

Impersonation

impersonation 是一个将当前用户扮演为另外一个用户的特性,这个特性有助于管理员来测试不同用户的权限是否配置正确等等。取得 header 的 key 是:

  • Impersonate-User:用户
  • Impersonate-Group:组
  • Impersonate-Extra-:额外信息

用户分为 service account 和 user。根据格式区分,service account 的格式是 namespace/name,否则就是当作 user 对待。

Service account 最终的格式是: system:serviceaccount:namespace:name

PriorityAndFairness / MaxInFlightLimit

如果设置了流控,就使用 PriorityAndFairness,否则使用 MaxInFlightLimit。

PriorityAndFairness:会对请求做优先级的排序。同优先级的请求会有公平性相关的控制。

MaxInFlightLimit:在给定时间内进行中不可变请求的最大数量。当超过该值时,服务将拒绝所有请求。0 值表示没有限制。(默认值 400)

参考资料:https://kubernetes.io/zh/docs/concepts/cluster-administration/flow-control/

Authorization

// AttributesRecord implements Attributes interface.
type AttributesRecord struct {
   User            user.Info
   Verb            string
   Namespace       string
   APIGroup        string
   APIVersion      string
   Resource        string
   Subresource     string
   Name            string
   ResourceRequest bool
   Path            string
}

鉴权的时候会从 context 中取出上面这个结构体需要的信息,然后进行认证。支持的认证方式有:

  • Always allow
  • Always deny
  • Path: 允许部分路径总是可以被访问

其他的一些常用的认证方式主要是通过插件提供:

  • Webhook
  • RBAC
  • Node

其中 Node 专门为 kubelet 设计的,节点鉴权器允许 kubelet 执行 API 操作。包括:

读取操作:

  • services
  • endpoints
  • nodes
  • pods
  • secrets、configmaps、pvcs 以及绑定到 kubelet 节点的与 pod 相关的持久卷

写入操作:

  • 节点和节点状态(启用 NodeRestriction 准入插件以限制 kubelet 只能修改自己的节点)
  • Pod 和 Pod 状态 (启用 NodeRestriction 准入插件以限制 kubelet 只能修改绑定到自身的 Pod)
  • 事件

鉴权相关操作:

  • 对于基于 TLS 的启动引导过程时使用的 certificationsigningrequests API 的读/写权限
  • 为委派的身份验证/授权检查创建 tokenreviews 和 subjectaccessreviews 的能力

在将来的版本中,节点鉴权器可能会添加或删除权限,以确保 kubelet 具有正确操作所需的最小权限集。

为了获得节点鉴权器的授权,kubelet 必须使用一个凭证以表示它在 system:nodes 组中,用户名为 system:node:<nodeName>。 上述的组名和用户名格式要与 kubelet TLS 启动引导过程中为每个 kubelet 创建的标识相匹配。

director

director 的 ServeHTTP 方法定义如下,也就是会根据定义的 webservice 匹配规则进行转发。否则就调用 nonGoRestfulMux 进行处理。

func (d director) ServeHTTP(w http.ResponseWriter, req *http.Request) {
    path := req.URL.Path

    // check to see if our webservices want to claim this path
    for _, ws := range d.goRestfulContainer.RegisteredWebServices() { q 
        switch {
        case ws.RootPath() == "/apis":
            // if we are exactly /apis or /apis/, then we need special handling in loop.
            // normally these are passed to the nonGoRestfulMux, but if discovery is enabled, it will go directly.
            // We can't rely on a prefix match since /apis matches everything (see the big comment on Director above)
            if path == "/apis" || path == "/apis/" {
                klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
                // don't use servemux here because gorestful servemuxes get messed up when removing webservices
                // TODO fix gorestful, remove TPRs, or stop using gorestful
                d.goRestfulContainer.Dispatch(w, req)
                return
            }

        case strings.HasPrefix(path, ws.RootPath()):
            // ensure an exact match or a path boundary match
            if len(path) == len(ws.RootPath()) || path[len(ws.RootPath())] == '/' {
                klog.V(5).Infof("%v: %v %q satisfied by gorestful with webservice %v", d.name, req.Method, path, ws.RootPath())
                // don't use servemux here because gorestful servemuxes get messed up when removing webservices
                // TODO fix gorestful, remove TPRs, or stop using gorestful
                d.goRestfulContainer.Dispatch(w, req)
                return
            }
        }
    }

    // if we didn't find a match, then we just skip gorestful altogether
    klog.V(5).Infof("%v: %v %q satisfied by nonGoRestful", d.name, req.Method, path)
    d.nonGoRestfulMux.ServeHTTP(w, req)
}

admission webhook

在请求真正被处理前,还差最后一步,就是我们的 admission webhook。admission 的调用是在具体的 REST 的处理代码中,在 create, update 和 delete 时,会先调用 mutate,然后再调用 validating。k8s 本身就内置了很多的 admission,以插件的形式提供,具体如下:

  • AlwaysAdmit
  • AlwaysPullImages
  • LimitPodHardAntiAffinityTopology
  • CertificateApproval/CertificateSigning/CertificateSubjectRestriction
  • DefaultIngressClass
  • DefaultTolerationSeconds
  • ExtendedResourceToleration
  • OwnerReferencesPermissionEnforcement
  • ImagePolicyWebhook
  • LimitRanger
  • NamespaceAutoProvision
  • NamespaceExists
  • NodeRestriction
  • TaintNodesByCondition
  • PodNodeSelector
  • PodPreset
  • PodTolerationRestriction
  • Priority
  • ResourceQuota
  • RuntimeClass
  • PodSecurityPolicy
  • SecurityContextDeny
  • ServiceAccount
  • PersistentVolumeLabel
  • PersistentVolumeClaimResize
  • DefaultStorageClass
  • StorageObjectInUseProtection

3. 如何阅读 apiserver 的相关代码

我看的是仓库是 https://github.com/kubernetes/kubernetes。apiserver 的代码主要分散在以下几个位置:

  • cmd/kube-apiserver: apiserver main 函数入口。主要封装了很多的启动参数。
  • pkg/kubeapiserver: 提供了 kube-apiserver 和 federation-apiserve 共用的代码,但是不属于 generic API server。
  • plugin/pkg: 这下面都是和认证,鉴权以及准入控制相关的插件代码
  • staging/src/apiserver: 这里面是 apiserver 的核心代码。其下面的 pkg/server 是服务的启动入口。

kubernetes 中的认证和授权

一、概述

kubernetes 中有两种用户类型:服务账户(service account)普通用户(user)。这两种用户类型对应了两种使用场景。

服务账户提供给在集群中运行的 pod,当这些 pod 要和 apiserver 通信时,就是使用 serviceaccount 来认证和授权。服务账户是存储在 k8s 集群中的,基于 RBAC,可以和角色进行绑定,从而拥有特定资源的特定权限。

普通用户是非 pod 的场景下用来做认证和授权。比如像 k8s 的一些关键组件: scheduler, kubelet 和 controller manager,包括使用 kubectl 和 k8s 集群做交互。

二、服务账户

2.1 自动化

即使我们不在 namespace 下创建服务账户,也不为 pod 绑定任何的服务账户,pod 的 serviceAccount 字段也会被设置为 default。任何 namespace 下都会有这样的服务账户。我们可以查看这个名为 default 的服务账户,它会对应一个 secret。secret 中记录了 ca.crt,namespace 和 token 这三个值。

这整个过程由三个组件完成:

  • 服务账户准入控制器(Service account admission controller)
  • Token 控制器(Token controller)
  • 服务账户控制器(Service account controller)

其中,服务账户控制器负责在每个 namespace 下维护默认的服务账户。这样当新的 namespace 被创建后,就会自动创建一个 default 服务账户。即使删除了该服务账户,也会立刻被重新自动创建出来。

token 控制器负责以下几项工作:

  • 检测服务账户的创建,并且创建相应的 Secret 以支持 API 访问。
  • 检测服务账户的删除,并且删除所有相应的服务账户 Token Secret。
  • 检测 Secret 的增加,保证相应的服务账户存在,如有需要,为 Secret 增加 token。
  • 检测 Secret 的删除,如有需要,从相应的服务账户中移除引用。

至于为什么需要为服务账户创建 token 并生成 secret,将在后面提到。

现在我们知道了服务账号和其 token 的自动化过程。那服务账户准入控制器又是什么作用呢?我们将目光投到 pod 创建的过程中,大多数时候我们都不会为 pod 指定服务账户。那么 pod 在创建成功后,关联的 default 服务账户是怎么回事呢?

这里就是服务账户准入控制器的作用了。它通过 admission controller 的机制来对 pod 进行修改。在 pod 被创建或更新时,会执行以下操作:

  • 如果该 pod 没有 ServiceAccount 设置,将其 ServiceAccount 设为 default

  • 保证 pod 所关联的 ServiceAccount 存在,否则拒绝该 pod。

  • 如果 pod 不包含 ImagePullSecrets 设置,那么 将 ServiceAccount 中的 ImagePullSecrets 信息添加到 pod 中。

  • 将一个包含用于 API 访问的 token 的 volume 添加到 pod 中。

  • 将挂载于 /var/run/secrets/kubernetes.io/serviceaccountvolumeSource 添加到 pod 下的每个容器中。

2.2 认证

假设我们的 pod 已经设置好了服务账户,现在要和 apiserver 通信,那么 apiserver 是怎么认证这个服务账户是有效的呢?

我们在上面提到,token 控制器会为服务账户创建 secret,secret 中的 token 就是服务账户的校验信息,具体来说是通过 JWT 来认证的。这个 token 中会存储服务账户所在的命名空间,名称,UID 和 secret 的名称等信息。如果你对 token 内容感兴趣的话,可以将 token 值用 base64 解码,粘贴到这里看看 token 中的内容。然后也可以将私钥和公钥粘贴上去验证签名是否正确。具体内容如下:

{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "default",
  "kubernetes.io/serviceaccount/secret.name": "default-token-894m5",
  "kubernetes.io/serviceaccount/service-account.name": "default",
  "kubernetes.io/serviceaccount/service-account.uid": "df5a8a9c-14d4-44c7-a55f-0100f51fc848",
  "sub": "system:serviceaccount:default:default"
}

这个 token 在生成的过程中,使用了服务账户专属的私钥进行签名,这个私钥是在 contrller-manager 启动时,通过--service-account-private-key-file传进去的。同样的,为了在 apiserver 中对这个 token 进行校验,需要在 apiserver 启动时通过参数--service-account-key-file传入对应的公钥。

2.3 授权

认证的问题解决了,那么 apiserver 怎么知道该请求的服务账户是否有权限操作当前的资源呢?在 k8s 中,常用的授权策略就是 RBAC 了。我们通过将服务账户和角色关联,就可以让服务账户有指定资源的相关操作权限了。

三、普通用户

3.1 认证

不同于服务账户的是,k8s 本身并不存储普通用户的任何信息,那么 apiserver 是如何认证普通用户的呢?

在创建 k8s 集群时,一般都会有一个根证书负责签发集群中所需的其他证书。那么可以认为,如果一个普通用户可以提供由根证书签发的证书,他就是一个合法的用户。其中,证书的 common name 就是用户名,orgnization 是用户组。比如我们本地集群上的 controller-manager 的证书信息如下:

$ cfssl certinfo -cert controller-manager.pem
{
  "subject": {
    "common_name": "system:kube-controller-manager",
    "names": [
      "system:kube-controller-manager"
    ]
  },
  "issuer": {
    "common_name": "kubernetes",
    "names": [
      "kubernetes"
    ]
  },
  "serial_number": "7884702304157281003",
  "not_before": "2020-10-09T05:51:52Z",
  "not_after": "2021-10-09T05:51:54Z",
  "sigalg": "SHA256WithRSA",
  "authority_key_id": "11:F5:D7:48:AE:2E:7F:59:DD:4C:C4:A8:97:D2:C0:21:98:C6:3A:A7",
  "subject_key_id": "",
  "pem": "-----BEGIN CERTIFICATE-----\nMIIDCDCCAfCgAwIBAgIIbWwW+H7/quswDQYJKoZIhvcNAQELBQAwFTETMBEGA1UE\nAxMKa3ViZXJuZXRlczAeFw0yMDEwMDkwNTUxNTJaFw0yMTEwMDkwNTUxNTRaMCkx\nJzAlBgNVBAMTHnN5c3RlbTprdWJlLWNvbnRyb2xsZXItbWFuYWdlcjCCASIwDQYJ\nKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMPCkXDAttbJHnoLuhGFPr/28ag8NoI5\nY0e00uv3ltyHlakfCeOV48eBgpMka3BdUxFOTHI5wtumlU3iymdDvTnKkLc75v6p\nQ0Hfx0DYz8ykDcHQ04hIsgyXecaHl+hfy90bYAbF8V43MjA0X2VmIyLxS6wXgeM6\n8d/jc1X8Ggpw53ow7L4XiCMlXDPwzLlVUThYHRN+PA5EdADZHAzgXjsyg379/ori\nbS/NZtmizzfHGWugrfwBGPL187mp1xN1tyjR+7obtsQYpgZ0Emz74fWNlike2I69\ntlBDWYC5ddsbHtDu4h/H5guwFtZ3+VVLogyw3CntPvoV840Ro5jxmtMCAwEAAaNI\nMEYwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMCMB8GA1UdIwQY\nMBaAFBH110iuLn9Z3UzEqJfSwCGYxjqnMA0GCSqGSIb3DQEBCwUAA4IBAQBvUxh0\n+TDJn19qJPWXu5MGrRs1Efn+KCgSVMDcak9MfnG3kzCZ94SKw5PRYGQ6fzuUsgwT\nkbGJ3o4PR/BkZ9R2UUHa2prydQTHN+Qb/DuF3kVYTRbWxTN3br8Tp1uqiQVOLPe0\nrfRelwVR6y39O5Wc3VQCnQKM/ih4k2LKGwinq2sO7HN6pjwoKfapaOb050vrGOTu\n5RmX+SWs7CeWzITjC3sLfFyP/lh8zK7TINOKRx1/QBHlCnX4wnsXpOIe4Jf4ol1b\nKKGcicAcSrj252oOIxspAW8a7vX4DjVGRTSneQen5wbHeZbkeMyuvAVs2a73x94d\nfTH4K9+zxCLAVZFs\n-----END CERTIFICATE-----\n"
}

其中,subject 是证书申请人的信息,issuer 是签发人的信息。这里可以知道,controller-manager 使用的是 system:kube-controller-manager 这个用户。

3.2 授权

跟服务账户的授权一样,普通用户也可以通过 RBAC 的机制来绑定角色,然后拥有某些资源的某些权限。比如 controller-manager 的 ClusterRoleBinding 信息如下:

$ kubectl get clusterrolebinding  system:kube-controller-manager -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  annotations:
    rbac.authorization.kubernetes.io/autoupdate: "true"
  creationTimestamp: "2020-09-22T12:33:24Z"
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
  name: system:kube-controller-manager
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: system:kube-controller-manager
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: User
  name: system:kube-controller-manager

也就是绑定到了 system:kube-controller-manager 这个集群角色,如果你感兴趣的话,还可以继续看这个角色绑定了哪些资源极其操作权限。

3.3 实践

因为普通用户需要我们自己为其签发证书,然后授权。下面用一个简单的例子来走一遍。

首先创建一个证书签发请求的 json 文件:

{
    "CN": "jiang",
    "key": {
        "algo": "ecdsa",
        "size": 256
    },
    "names": [
        {
            "O": "dev"
        }
    ]
}

CN 是 common name 的简写。也就是我们要设置的用户名。O 是 orgnization 的简写,可以理解为用户组。接下来用 k8s 的根证书签发,需要 ca.crt 和 ca.key。这两个文件可以在 master 节点上的 /etc/kubernetes/certs或其他地方找到。

cfssl gencert -ca ca.crt -ca-key=ca.key jiang.json | cfssljson -bare jiang

这条命令会生成三个新的文件:

  • jiang-key.pem:私钥
  • jiang.pem:证书
  • jiang.csr: 证书签发请求文件

下面我们用 jiang 的私钥和证书生成 kubeconfig 中的用户:

kubectl config set-credentials jiang --client-key=./jiang-key.pem --client-certificate=./jiang.pem --embed-certs

接下来生成新的 context,指定 k8s 集群要使用的用户:

kubectl config set-context k8s-jiang --user=jiang --cluster=k8s

为 jiang 这个用户创建角色和绑定。这里只允许 jiang 这个用户读取 default namespace 下的 pod。

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
    name: pod-reader
    namespace: default
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

---

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
    name: read-pods
    namespace: default
subjects:
- kind: User
  name: jiang
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

这样就完成了对 jiang 这个用户的认证和授权了。使用下面命令切换到这个用户:

kubectl config use-context k8s-jiang