Kubernetes权限管理概述

对于一个多用户系统而言,权限管理始终是无法忽略的一环,系统规模越大,权限管理就越重要。

Git 系统为例,你有一个代码仓库,你是这个仓库的管理员,你们组的同学具有提交代码的权限,你们部门的同学具有只读权限,公司的其他人全部无法查看,这就是一个典型的权限管理场景。

通常在校验权限中分为两个流程,认证鉴权

即先判断你是谁:认证,然后再鉴权,判断你有没有这个资源的权限。

毫无疑问,复杂的Kubernetes也具有一套复杂的权限管理系统,如果要用一句话简单介绍一下的话:

客户端经过认证,确认了身份之后,再经过鉴权,就可以访问资源了。

k8s 权限管理机制概览:

image-20210318210936820

下文我们会逐一详细介绍各个环节。

认证:Authentication

权限管理系统的第一步肯定是需要确认访问者的身份,如果访问者的身份是伪造的的话,那么后面的一切也就无从谈起。

k8s 支持非常多种途径来证明访问者的身份,比如最常见的kubectl 中的客户端证书等等,同时访问者的身份也可能有很多种,如:外部用户、服务账号、用户组。

虽然从 API Server 的角度来说,你得先认证,然后才有身份,但是从访问者的角度而言,他是先有身份然后再去接受 API Server 认证的,所以我们按照访问者的角度来看,先介绍一下 k8s 中都有哪些身份。

身份类型

User

User,外部用户是 k8s 中非常常见的一种访问者身份,通常用于从 k8s 之外来访问集群中的资源,不过硬要在集群内部访问也不是不可以,毕竟从 API Server 提供的 HTTP 协议角度而言,都是外部客户端,只是 IP 地址不一样罢了。

因为这种资源的定位就是外部用户,所以 k8s 是不会存储 User 信息的,我们可以通过 Keystone 或者 Google Accounts 这种外部应用来管理。

至于用户的创建和删除等生命周期管理,以X509客户端证书认证的方式为例,k8s 假设拿到集群 CA 私钥的人都是可信的,只需要用 CA 签发一张证书即可,证书里的正式名称(Common Name)写的什么用户名就是什么。

不过虽然 User 这个字符串的拼接规则是在外部,但是对应“字符串”所拥有的权限还是需要在集群中申明的。

Group

同外部用户,Group 也是一种外部的概念,在X509客户端证书认证的方式中,Group 名字就是证书的组织名(Orgnization),也是想写什么就是什么。

主要方便快速地为一组用户授予权限。

Service Account

相对于外部用户 User 而言,Service Account 则是集群内部的用户,我们可以使用 k8s api 来查看和管理这种用户。

增:kubectl create serviceaccount my-test-sa
删:kubectl delete serviceaccounts my-test-sa
改:kubectl edit serviceaccounts my-test-sa
查:kubectl get serviceaccounts

k8s 服务账号机制主要是为了方便 POD 访问集群资源而建立的,每一个 ServiceAccount 都会对应一个或多个 Secret,Secret 中就放着该账号对应的密钥,通常是一个 JWT Token。

在一个 POD 上绑定一个 ServiceAccount,那么运行时对应的密钥就会挂载到这个 POD 中,然后我们在 POD 中读取出来就可以通过使用其他 JWT Token 一样的方式,添加一个 HTTP头部:Authorization: Bearer xxx,来让 API Server 可以识别这个请求的身份,这样就免去了把密钥写在代码或者镜像里这种不安全的行为了。

同时,当一个 POD 上没有申明任何的服务账号时,k8s 也会挂载一个默认的 ServiceAccount 供 POD 使用。

  1. 能方便地挂载 POD 上
  2. 在 k8s 集群内部管理

这两点是 ServiceAccount 最显著的特征,不过除此之外 ServiceAccount 和 User 是没有任何本质区别的API Server 在判断用户是否拥有某个资源的权限时,ServiceAccount的实现方式就是加了:system:serviceaccount: +NameSpace 前缀的 User。

我们可以用集群公钥签一张证书,CommonName 就按照集群内已有的 ServiceAccount 来拼接,比如:system:serviceaccount:kube-system:kube-user,然后我们直接使用客户端证书的方式认证访问就会发现我们拼出来的这个外部“User”拥有对应 ServiceAccount 的所有权限。

Anonymous

没有身份也是一种身份。

当一个请求没有携带任何的认证信息时,它会自动获得用户名:system:anonymous和用户组system:unauthenticated,我们可以配置分配特定的权限给这种匿名用户,适用于想要公开一些不敏感的资源等场景。

认证方式

有了身份之后,我们接下来要做的事情就是向系统证明我们的身份。

认证这种行为,其实说到底就是把自己的用户名传递给API Server,同时有一个别人不知道的独一无二的标识,能证明这个用户名确实是你的。

这个标识可能是很多种东西,比如证书认证中的客户端私钥,BasicAuth 中的密码等等,Kubernetes 支持非常丰富的认证方式,甚至当内置的认证方式无法满足你的场景时还可以通过 Webhook 来自定义,甚至你还可以通过直接修改 k8s API Server 代码,添加一个:authentication.authenticator.Request接口的实现然后注册上去即可实现代码级别的自定义认证。

在 HTTP 请求到达 API Server 时,API Server 会遍历所有启用的认证方式,只要有一个认证成功就跳出进入下一个逻辑,认证顺序如下:

  1. 基本认证
  2. X509 客户端证书
  3. 静态令牌
  4. ServiceAccount令牌
  5. BootstrapToken
  6. OIDC
  7. Webhook

所以理论上顺序越靠后的认证方式性能是越差的,而且由于是 O(n) 的时间复杂度,每次认证都会遍历所有的认证方式,所以我们可以尽量关闭一些用不到的认证方式来提高性能。

HTTP BasicAuth

HTTP BasicAuth 是一种非常简单的认证方式,通过 HTTP 头部:Authorization 传输客户端用户名和密码的base64值来提供给服务端进行认证。

这种方式虽然很方便,风险比较大,明文的密码机制很容易泄露,也不好管理。

k8s 对于这种机制也支持的比较克制,在 API Server 启动时可以通过参数–basic-auth-file指定基本认证的用户名密码路径,API Server 起来之后就会把这些信息载入,之后如果要修改的话必须重启 API Server

轻易重启API Server 对于线上集群而言是一件比较危险的事情,无法随意添加修改用户,这也就限制了这种认证方式的使用场景,一定程度上也避免了某些人图方便而滥用这种机制的情况。

X509客户端证书

X509证书即我们平常所熟知的 HTTPS 证书,不过平常我们访问 HTTPS 网站的时候只是单向验证了服务端的身份没有被篡改。

k8s 支持我们使用客户端证书的方式,来证明我们自己客户端的身份

用集群的CA私钥签署一张证书,证书的 CommonName 填上用户名,Organization 填用户组,这样一张客户端证书就签好了。

只要管理员手中的私钥没有泄露,那么持有CA签署过的证书同时持有对应私钥的用户,就是可信的,我们只需要在跟 API Server 进行 TLS 握手时提供我们的证书,即可证明我们的身份,比如使用 curl 的话我们可以这样来带上:

# client.crt 客户端证书
# client.key 客户端私钥
# 127.0.0.1:60002 API Server 地址
curl -k --cert  ./client.crt  --key ./client.key https://127.0.0.1:60002

不过签发证书的操作还是比较繁琐的,同时证书一旦签发出去就再也无法回收了,最多也只能把这个证书对应用户的权限撤销,适合用于签发一些低频的系统账号。

ServiceAccount 令牌

ServiceAccount 是我们上面提到的一种特殊的身份,一种以 system:serviceaccount开头的用户,而 ServiceAccount 令牌则是这种机制使用的认证方式。创建 ServiceAccount 时会同时创建一个 Secret,里面存着的就是对应的 ServiceAccount令牌,在 ServiceAccount 被绑定到 POD 后,这个令牌也会自动挂载到 POD 内供其使用。

ServiceAccount令牌也是一种 BerearToken,对于 API Server 从 HTTP 协议角度的来说,k8s 集群内外是没有区别的,所以我们其实也可以把 ServiceAccount令牌拿到集群外用,比如配到 kubeconfig 里。

一个 ServcieAccount令牌实例:

Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImhZeXFZbG1QSlFKQjBKRkdLd0t6cWw0SG8tSVQ1QzJLbjFvMnJBYTRrZzgifQ.eyJpc3MiOiJrdWJlcm5ldGVzL3NlcnZpY2VhY2NvdW50Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9uYW1lc3BhY2UiOiJ0ZXN0LXRlcyIsImt1YmVybmV0ZXMuaW8vc2VydmljZWFjY291bnQvc2VjcmV0Lm5hbWUiOiJkZWZhdWx0LXRva2VuLXpibW03Iiwia3ViZXJuZXRlcy5pby9zZXJ2aWNlYWNjb3VudC9zZXJ2aWNlLWFjY291bnQubmFtZSI6ImRlZmF1bHQiLCJrdWJlcm5ldGVzLmlvL3NlcnZpY2VhY2NvdW50L3NlcnZpY2UtYWNjb3VudC51aWQiOiJiOWRhY2RlNi05MzkzLTRkYjUtOTk4Yi03NzU3N2QzZGYxNTMiLCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdC10ZXM6ZGVmYXVsdCJ9.mAHVlldUxlJD4dUOp2LTG37H3Jp0P4neYmv7A-bL-lrhFJ9XznLah4UuYIrgutCJ4clXcZnlsvrzDEnoWXSrK4bi62ztJuFneaJb0oczekU8YUligBWBpAHwfgYNXEDAWEVDrPayggF0TZdKfpHhQ0iOisvUO8XEo24fP1VYWtsr8SibyFJ-qOAuMUMi2C54KHY8-NAhbpkRM0d7awH-hU2Zkg-oYrK8U-RbNkWEIgUfxKByal0SXzF-KH24bO0_8XWSazAqi3vpElK2a5h4j7_qNv88UjX21gZtzpkOTXkZsDtiPKihfpXyWRHpo877-AUuC1Ct0ln4o7AbQpADEg

ServiceAccount令牌是一种JWT Token,所以我们可以直接通过 JWT 解析的工具来查看里面的内容。

静态令牌文件

和 BasicAuth 一样,通过 --token-auth-file指定静态令牌文件地址,每次修改文件也一样需要重启才能生效,只不过认证方式由 BasicAuth 改成了 Bearer Token。

Bootstrap 引导令牌认证

Bootstrap令牌是最近才出来的一种认证机制,在 1.18版本之后才支持。

和上面两种令牌一样,也是通过 HTTP 头部Authorization传递该令牌,形如:

# 07401b 是 ID
# f395accd246ae52d 是密钥
Authorization: Bearer 07401b.f395accd246ae52d

密钥存在kube-system命名空间下的 Secrte 里,名字形如"bootstrap-token-<token id>"格式,同时可以设置过期时间,一个例子如下:

apiVersion: v1
kind: Secret
metadata:
  # name 必须是 "bootstrap-token-<token id>" 格式的
  name: bootstrap-token-07401b
  namespace: kube-system

# type 必须是 'bootstrap.kubernetes.io/token'
type: bootstrap.kubernetes.io/token
stringData:
  # 供人阅读的描述,可选。
  description: "The default bootstrap token generated by 'kubeadm init'."

  # 令牌 ID 和秘密信息,必需。
  token-id: 07401b
  token-secret: base64(f395accd246ae52d)

  # 可选的过期时间字段
  expiration: "2017-03-10T03:22:11Z"
  # 允许的用法
  usage-bootstrap-authentication: "true"
  usage-bootstrap-signing: "true"

  # 令牌要认证为的额外组,必须以 "system:bootstrappers:" 开头
  auth-extra-groups: system:bootstrappers:worker,system:bootstrappers:ingress

Bootstrap token一般会绑定一个 system:node-bootstrapper的角色,该角色只有证书签署对象(certificatesigningrequests.certificates.k8s.io)相关的权限:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: 'system:node-bootstrapper'
  labels:
    kubernetes.io/bootstrapping: rbac-defaults
rules:
  - verbs:
      - create
      - get
      - list
      - watch
    apiGroups:
      - certificates.k8s.io
    resources:
      - certificatesigningrequests

然后一个组件使用该 token连到 apiserver 上后会创建一个证书签署对象,管理员在内部approve了之后会下发一组证书给组件,之后组件就能使用安全性更好的客户端证书的校验方式和 apiserver协同工作了。

最典型的一个应用场景就是新的节点接入集群上面的kubelet要和 apiserver通信通常就是通过这种认证方式。

OIDC令牌

OAuth 应该是我们经常见到的一种授权协议,用于在用户许可的情况下将系统的资源权限授予给第三方平台。

OAuth 本质上是个授权协议,但是也可以当做认证协议来用,授予查看身份的权限就算是认证了。而 OpenID Connect(OIDC) 则是集 OpenID 和 OAuth 协议精华于一身的既完全兼容 OAuth 协议,同时吸收了 OpenID 在身份认证上的建树。

而 Kubernetes 也集成了 OIDC 的功能,使得 Kubernetes 可以接入第三方登录系统,来实现自定义认证,比如我们可以配置通过 Github、Google 等第三方应用来登录 k8s,就像很多网站除了直接注册都还支持使用微信、QQ来登录一样。

而具体第三方系统中内部的身份虽然是五花八门的,但是都需要通过 OIDC 这种标准的协议来和 k8s 交互,不过 k8s 内部也有一套身份体系,比如我们既可以把邮箱当做 k8s 用户名也可以把 OIDC 中的用户名当做 k8s 用户名,这就需要一个地方能配置这种内外字段的映射关系,具体可以通过:

  • --oidc-groups-prefix指定统一的 k8s group 前缀
  • --oidc-groups-claim指定用哪个 OIDC 字段当做 k8s group
  • --oidc-username-prefix指定统一的 k8s 用户前缀
  • --oidc-username-claim指定用哪个字段当做 k8s 用户

不过 k8s 本身并不能直接提供 OIDC 令牌认证的服务,需要再搭配一个令牌管理插件来使用,比如:CoreOS dexKeycloak、CloudFoundry UAA 等。

Webhook令牌

如果想要彻底搭建自己的认证系统,Webhook令牌绝对是最合适的选择。

k8s 支持在提交 Token 时将 Token 转发至指定的 Webhook 回调地址,然后根据约定好的回调响应来判断此次认证的结果。

资源:Resources

API Server 完成了认证,确认了“你是谁”之后,下一步就是鉴权了,但是在此之前,API Server 还需要知道“你想要什么”,才能判断能不能给你你想要的东西。具体就是你想要访问的资源,再具体一点,就是你发起的 HTTP 请求中的字段。

一个资源具体包含以下核心属性,权限分配时也是根据这些属性来进行配置的。

以访问 deployments coredns 资源的 URL为例:

http://127.0.0.1:8001/apis/extensions/v1beta1/namespaces/kube-system/deployments/coredns

对应资源属性到 HTTP 协议字段位置映射如下图:

image-20210318012750159

资源类型+资源名称

资源类型包括常见的 PODServiceDeploymentSecret 等,同时一个资源可能还包含子资源,比如 POD 的日志这种资源类型:”pod/logs”,

同时 k8s 也是支持针对某一个具体的资源来进行授权的,所以资源名称也是必要的参考属性。

API组

资源所在的 API 组,用于给一类资源进行权限配置,空白代表核心API组。

常见的 API 组有:

  • rbac.authorization.k8s.io
  • storage.k8s.io
  • apps 等等

当前集群所支持的所有资源及其API组可以通过 kubectl api-resources命令查询。

动作

要对资源执行的动作,比如增删改查等。

通用的 k8s 动作如下:

动作 对应的HTTP请求方法
create POST
get GET、HEAD
update PUT
patch PATCH
delete DELETE
list GET
watch GET
deletecollection DELETE

Namespace

很多类型的资源是分 namespace 的,所以我们也可以对其分 namespace 进行权限授予。

请求路径

适用于非 RESTFul 风格的 API 鉴权使用。

鉴权:Authorization

API Server 知道了客户端是谁,以及它想要什么之后,下一步 API Server 就需要判断是否允它的请求,而这个判断的依据,就是“鉴权机制”。

鉴权机制的实现原理无非就是一种对象代表用户,一种对象代表资源,然后在一个地方存着用户到资源的联系集,每次要鉴权的时候就查一下这个联系集看有没有当前用户到资源的规则在。

而具体不同鉴权机制之间,则主要是这个联系如何建立以及存在哪里会有一些区别。

属性鉴权:ABAC

Attribute-based access control基于属性的权限控制,通过一个静态文件通过 JSONL的格式来声明权限授予,需要直接写死给哪个用户或者组分配什么权限,不太灵活,已经不推荐使用了,一个例子如下:

用户 test-admin 具有 test namespace 下所有资源的权限:

{"apiVersion": "abac.authorization.kubernetes.io/v1beta1", "kind": "Policy", "spec": {"user": "test-admin", "namespace": "test", "resource": "*", "apiGroup": "*"}}

角色鉴权:RABC

Role-based access control基于角色的权限控制,RABC 是一种非常经典的权限管理模型,它在 ABAC的基础上将要被授权的属性(或者叫资源)抽象出了一层角色,用户不再直接与资源建立联系,而是先将一系列资源绑定到角色上,用户再与角色建立联系。

举个例子,假设我有两台机器,给小明和小红开了一个机器管理员的权限,如果是 ABAC 模式的话,如果新加了一台机器,那么我需要分别给小明和小红都加一下权限,如果机器和人一多,那就会非常的乱。

而如果我们在机器资源之上抽象出来一层机器管理的角色,那么下次再新加机器只需要将这台机器加入到这个角色下即可,拥有机器管理员身份的所有人就都能自动获得这台机器的权限了,这就是 RABC用户和资源不再直接耦合,更加灵活方便。

ABAC:

image-20210318203921086

RBAC:

image-20210318204053595

角色:Role

在 k8s 中,角色主要通过 Role 对象来申明资源的集合,Role 通过 Rule 对象来申明具体的权限,一条 Rule 即我们上文提到的资源,*是通配符。

一个 Role 的例子如下,声明了一个 default命名空间的只读角色:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  # Role 是分命名空间的
  namespace: default
  name: default-viewer
rules:
  - # 资源API组
    apiGroups: ["*"]
    # 资源类型
    resources: ["*"]
    # 动作
    verbs: 
      - get
      - list
      - watch
    # 可以申明具体某一个资源的权限,不填代表拥有所有权限
    resourceNames: []

另外存在全局版本的 ClusterRole可以声明整个集群范围内的权限。

角色绑定:RoleBinding

有了角色之后,我们还需要完成角色到用户联系集的建立,在 k8s 中可以通过 RoleBinding来实现绑定,对应的全局集群版本为ClusterRolebinding,另外 RoleBinding 也是可以引用ClusterRole的,但是会自动在 ClusterRole 上加上当前命名空间的限制。

我们可以通过如下的RoleBinding对象申明来将我们上文创建的角色绑定到小明和小红这两个用户上:

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: default-viewers
  namespace: default
subjects:
# 可以同时将角色绑定到多个主体上
- kind: User
  name: xiaoming # 不区分大小写
  apiGroup: rbac.authorization.k8s.io
- kind: User
  name: xiaohongs
  apiGroup: rbac.authorization.k8s.io
roleRef:
  # 要绑定的角色
  kind: Role # Role 或 ClusterRole
  name: default-viewer
  apiGroup: rbac.authorization.k8s.io

Node

Node鉴权方式其实算是一种 RABC 的变种,它的提出可以参考kubernetes issue40476,具体实现于 pr46076,主要是出于安全考虑,早期一台机器上的 kubelet 拥有任意修改其他 node 的资源的权限,存在一定的风险,需要限制 node 即 kubelet 只能访问它这个节点上的资源。

理论上最好的方式肯定是创建一个 Role 叫 system:node,然后该角色只具有该节点的权限,这样就能直接复用 RABC 机制了,但是很遗憾,我们在上文资源那一节提到了一个资源都有哪些属性,很明显是无法直接配置出来这种角色的,所以只能单独写一种鉴权规则到代码里给 kubelet 等内部组件鉴权使用。

自定义

和认证方式一样,鉴权机制也是支持自定义的:Webhook 或者自己改代码实现authorization/authorizer/Authorizer接口即可。


参考文档:

  1. 鉴权概述 https://kubernetes.io/zh/docs/reference/access-authn-authz/authorization/
  2. 使用 RBAC 鉴权 https://kubernetes.io/zh/docs/reference/access-authn-authz/rbac/
  3. 用户认证 https://kubernetes.io/zh/docs/reference/access-authn-authz/authentication/
  4. OIDC https://openid.net/connect/
  5. X509定义 https://tools.ietf.org/html/rfc5280
  6. Kubernetes API Server 源代码 https://github.com/kubernetes/kubernetes/tree/master/staging/src/k8s.io/apiserver/pkg