聊一聊 rancher 的认证和鉴权(二)

1,347 阅读11分钟

上一篇有提到 k8s 在认证上是达不到企业级应用的标准的。比如,k8s 如何对接到 OIDC,基于 k8s 的平台如何实现单点登录?这些都是需要平台自身实现的。

在鉴权上同理,简单的 RBAC 机制很可能满足不了用户需求,也需要平台建设自己的鉴权体系。

本篇将讲述两部分内容:第一部分是 rancher 的认证和鉴权体系,第二部分是对上篇没有讲的进阶知识的补充。

rancher 的认证体系

rancher 为什么要做用户管理

首先明确一下,任何直接服务于用户的系统都必须有账号。账号才是系统的直接使用者。所以 rancher 作为一个企业级 k8s 管理平台,必然会建设自己的账号体系。如果没有账号的话,用户压根就没法登陆,总不能像 k8s 一样,管理员手动去创建 x509 证书吧……

rancher 的认证方式

通过官方文档可以发现,rancher 可以可对接许多公有的 IDentity Provider,比如 google OAuth、github、Azure AD 等,也可以对接私有的 OpenLDAP 服务器。在使用过程中,管理员可以在安全-认证中配置 IDP,之后登陆时,用户就可以选择除了 local authentication 之外的认证方式。

不管使用哪种认证方式,rancher 在 IDP 进行了认证行为之后,会在本地保存这个用户的账号,并把 IDP 中的账号和本地账号进行一次 mapping。之后还会给用户返回一个 token,这个 token 后面再说。在 k8s 集群上部署 rancher 之后,k8s 集群上会出现若干 rancher 的 CRD,其中一个叫做 users.management.cattle.io,rancher 就是这样使用 CRD 进行用户管理。如果配置了认证方式,比如配置了 openldap 认证方式,那么当一个用户通过 openldap 方式登录到系统中时,管理员可以看到一个新建的 user CR。

那这个 mapping 是怎么做的呢?user 中记录了一个 principleIDs 的字段,分别标志了不同的认证域内的用户主体信息。比如

principalIds": [
  "local://u-xxxxxxx",
  "openldap_user://CN=xxx,OU=xxx,DC=xxx"
],

即这个用户在 local IDP 的账号的 ID 是 u-xxxxxx,在 openldap 服务器上的 distinguish name 是 CN=xxx,OU=xxx,DC=xxx(DN 是 openldap 协议中唯一标志一个用户的属性)。这样就完成了账号的映射。

要注意,除了 local authentication 外,rancher 最多只能同时对接一种外部认证方式。这个也非常好理解,正如之前强调过多次,身份是唯一的,账号不是。如果对接了多个 openldap 服务器,那么假设用户在 IDP1 中的 DN 和在 IDP2 中的 DN 可能是不同的,那么在 rancher 看来,这就是两个用户。故只能对接一个 IDP。如果要有同时对接多个 IDP 的需求,那么必须在多个 IDP 之间就做好用户的同步、合并、账号映射,但是这样一来,又只需要对接一个合并后的 IDP 就可以了。

丑陋的 rancher token

rancher 除了管理了用户信息之外,还保存了用户认证时,需要用到的 token,保存在 tokens.management.cattle.io 这个 CRD 里。用户登录之后,会给他生成一个 token。真正发给 rancher API 的请求将首先携带这个 token,向服务端请求一个 session。之后就是 cookie 那一套了。rancher 内部对这个 token 只是看存不存在,过没过期而已。我认为这个 token 是丑陋的。

为什么说它是丑陋的呢?因为它完全是个随机字符串。在 jwt 已经如此普及的时候,rancher 依然选择了用随机字符串来生成 token,着实让人有点意外。这意味着 rancher 需要自行管理 token 的有效期、token refresh,还要定期清理过期的 token。我没想明白这有啥好处。

rancher 发起的请求在 k8s 上的认证

先说结论:rancher 巧妙地利用代管账号+身份伪装(impersonation)进行。

rancher 会在每个纳管的 k8s 集群上部署一个 cluster-agent,该 agent 使用的 service account 被授予了 cluster-admin 的权限。在一开始将隧道的时候我们已经讲解过了,从 rancher 发往 k8s 的请求都通过这个 agent 进行,且转发给 agent 的时候,携带了用户在 rancher 上的账号信息,也就是 user CR 的 metadata name。

agent在 k8s 上认证时,实际使用的是 service account 的身份完成的认证,却伪装自己是 rancher 普通用户。之后在鉴权环节,k8s 看到的身份就不再是 service account 的身份,而是 rancher 账号的身份了。这就是 k8s 的 impersonation 的作用。

rancher 的鉴权

rancher 鉴权模式

rancher 大致上沿用了 k8s 的 RBAC 机制,但拓展出了更丰富的角色层级。

rancher 将角色分为三个层级

  • global role:全局角色,适用于所有的集群和项目,往往可以对管理员授予这样的权限
  • cluster role:集群级别的权限,该角色授权给用户时,必须指定一个集群
  • project role:项目级别的权限,该角色授权给用户时,必须指定一个项目

注:rancher 用项目(project)表示一组用户的工作空间的隔离。一个 project 必须在一个集群内,包含若干个命名空间,而一个命名空间最多属于一个项目。project 是对 namespace 语义的简单拓展。

rancher 的鉴权模式是服务于这样的用户隔离模型的。将角色拆解的更细,更易用。比如:

  • 全局管理员不需要被单独授予每个集群的权限
  • 管理员不需要为每个用户新建的 namespace 给用户手动授权

鉴权体系与 k8s 的对接

但注意,rancher 只是定义了 role 和 roleBinding,但并不在 rancher 做真正的鉴权。真正的鉴权发生在 k8s 侧。

rancher 通过 k8s operator 的方式将 rancher role + rancher role-binding 映射为 k8s role/cluster-role + k8s role-binding/cluster-role-binding,保证了用户权限的最终一致性。

在真正转发请求时,rancher 会做一些必要的 validation,通过之后,再把请求转到 agent 侧。上面认证中已经说过,通过 impersonation,agent 伪装自己是普通用户,之后 k8s 的 RBAC 机制完成了真正的鉴权。

拓展:OAuth、OIDC、SSO、IAM

在 OAuth 之前:user-agent 是什么

用户进行操作时,并不是直接对 app 进行的操作,而必须通过浏览器、CLI 等等工具进行。所以这里就会引入一层用户代理,代替用户真正与后端服务打交道。这就是 user-agent。

像我们在使用 kubectl 的时候,在 APIServer 的日志往往能看到一个 http header,例如 User-Agent: kubectl v1.21.5。就说明这个用户是用 kubectl CLI 发起的。

OAuth 的简单介绍

OAuth 是一个授权协议,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容。举个简单的例子,用户需要授权一个 app 读取另一个 app 中的数据。不可能让这个 app1 掌握用户在 app2 中的账号,故 app2 需要一种授权机制,允许 app1 在用户允许之后,读取用户的数据。

OAuth 2.0 的协议可以在这里看,大致读者需要知道 user-agent 与 authorization server 交互以获得一个 access token,拿着这个 access token,就可以向 app2 获取用户信息。

这里需要读者理解一个概念:OAuth 是一种授权协议,不是认证协议。因为协议的任何地方,都没有提到过认证服务器,没有认证行为。在实际应用中,authorization server 可以额外的承担认证功能,比如 github OAuth 是需要用户输入 github 账号的用户名、密码的。但 OAuth 没有对认证做任何限制,故本质上它是一种授权,不是认证。

OIDC 的意义

在 OAuth 的讨论中,我们涉及到的主题主要有 authorization server 和两个 app。考虑进一步的场景:我们的服务是分布式的,app1 向我请求 app2 的用户数据的权限,而访问用户数据时,app2 还要额外访问 app3。

如果仅仅使用 OAuth,我们只能获得一个特定的 access token,是一个标准的 jwt。假设我们没有对 access token 的字段做任何的定制,问题来了:app2 向 app3 访问的时候,如何证明自己的身份

这也是上面说到的,OAuth 是一个授权协议,不是一个认证协议。access token 只包含授权信息,不包含用户真实的身份信息。

解决方案就是 OIDC。这里不再赘述 OIDC 的协议和实现,读者可以自行搜索。OIDC 的核心在于:它完全兼容 OAuth,在 OAuth 的基础上解决了认证;它使用 id token,而非 access token,而 id token 中包含了用户的身份信息。

现在有一个很关键的问题:既然 jwt 允许自定义字段,为什么还要 id token,而不是在 access token 中直接嵌入身份信息呢?因为

  1. access token 只关于授权,为什么要随意添加其他非标准化的字段?
  2. access token 的受众是 user-agent,是 API 层面的 token;而 id token 关乎用户,其受众是用户本身,是 client 使用的。两个 token 的理念是不同的
  3. OIDC 解决了认证问题,它不是 OAuth 3.0。假设没有 OAuth,可能就不会有 access token 和 id token 了,这是技术变革且保持兼容性的结果

id token 的有效性如何验证?值得注意的是,id token 一样是一个有效的 jwt,故使用 jwt 的验证签名的方式就好了。

SSO 没那么神秘

SSO:single sign-on,即单点登录。大部分科技公司的内部系统都会做单点登录,方便员工使用。SSO 可以基于多种协议构建,比如 SAML、OIDC 等等。SSO 并不神秘。

基于 OIDC 可以高效的完成 SSO。它的原理也很简单,假设我们已经有了一个 authorization server,它对接了一个 IDP,可以签发有效的 id token。用户登录 app1 或者 app2 时,只要系统判断用户没有认证过,就重定向到 authorization server。用户通过认证,agent 再携带这个 id token 给各个 app 就好。

这里对各个 app 的要求就是大家都彼此信任 id token,拥有验证签名的能力即可。剩下的无非就是一些业务逻辑,比如跳转、回调等等。

IAM 才是最大的挑战

在认证和鉴权领域,最难的不是各种 jwt、OIDC 协议,也不是各种略微钻牛角尖的术语,而是身份的管理。在公司发展过程中,员工的身份信息可能随时发生变更,比如组织架构调整、离职、入职等等。当我们做了一套 SSO,互相传递 id token,各个 app 建设好账号体系与认证系统后,问题来了:如何在分布式系统的各个应用中,高效完成用户身份的管理和协同?

为了标准化的实现 IAM,一般业界会使用诸如 SCIM 协议这样的工具,这就要求 IAM 系统和各个 app 实现 SCIM 协议。

数据面权限控制也是麻烦事

在本文和上一篇中,我们在鉴权部分都讲的是控制面的权限控制,即抽象到控制面的资源的权限控制。而数据面的权限控制往往是另一套体系,这也是个大难点。如何解释数据面呢?举个简单的例子:在监控系统中,用户可以看带有某些 label 的数据,但如何限制用户只能看带有某些特定字段的数据?这就是数据面的权限控制。

以 k8s 为例,在 RBAC 规则里,可以定义 resource,也可以定义 nonResourceURL,其实这里 k8s 就尝试着定义数据面的权限控制,但功能比较有限。rancher 就压根不去定义数据面规则了。

一般来说,数据面的权限控制需要各个 app 自定定义,因为谁也不知道你跑的应用的数据到底是啥对吧。

总结

这篇就比较形象了,主要总结了 rancher 的认证和鉴权是怎么做的,之后很粗浅的讲了一点点进阶的概念,比如 OAuth,OIDC,IAM 等等,算是一点点小科普吧。

那到目前为止,rancher 的隧道已经分析完毕、认证鉴权部分梳理完毕,读者就知道了一个请求如何通过 rancher 的 access control,并转发到 k8s 后端真正执行成功的全过程了。

之后会讲讲 rancher 内部到底有哪些 controller,分别都在干什么,做了什么小动作。