KubeCube (https://kubecube.io) 是由网易数帆近期开源的一个轻量化的企业级容器平台,为企业提供 kubernetes 资源可视化管理以及统一的多集群多租户管理功能。KubeCube 社区将通过系列技术文章解读 KubeCube 的设计特点和技术实现,帮助开发者和用户更快地理解和上手 KubeCube。本文是第三篇,重点介绍 KubeCube 中用户管理与身份认证的实现方案。
所有 Kubernetes 集群都有两类用户:由 Kubernetes 管理的服务账号和普通用户。Kubernetes 假定普通用户是由一个与集群无关的服务通过以下方式之一进行管理的:负责分发私钥的管理员类似 Keystone 或者 Google Accounts 这类用户数据库包含用户名和密码列表的文件有鉴于此,Kubernetes 并不包含用来代表普通用户账号的对象。 普通用户的信息无法通过 API 调用添加到集群中。
根据 Kubernetes 官方所述,Kubernetes 本身并不直接提供用户管理的特性,不支持普通用户对象,更不存储普通用户的任何信息。如果需要创建一个用户,需要为该用户创建私钥和证书,通过证书进行身份认证。并且,由于不存储用户信息,集群管理员无法集中管理用户,对其他用户无感知。因此,KubeCube 首先重新定义了用户这一概念,即提供了 User 这一资源类型,存储用户信息,进行用户管理,同时方便后续的身份认证和权限校验等。
apiVersion: user.kubecube.io/v1
kind: User
metadata:
name: 登录账号,用户唯一标识,用户自定义,不可重复,不可修改
spec:
password: 密码,必填,系统会将密码进行md5加盐加密后保存
displayName: 用户名
email: 邮箱
phone: 电话
language: 语言:en/ch
loginType: 用户登录方式:normal/ldap/github/...
state: 用户状态:normal/forbidden
status:
lastLoginTime: 上次登录时间
lastLoginIp: 上次登录IP
用户可以由管理员在前端页面手动创建,也可以在使用外部认证第一次登录时系统自动创建。因此,用户在注册方式上可以分为系统普通注册用户和第三方授权登录用户。但对于这两种创建方式,都是对应在管控集群创建相应的 User cr。然后 Warden 的资源同步管理器会将该 cr 从管控集群同步到计算集群,以便于后续多集群统一认证。
这样,在用户管理页面,只需要查询管控集群内的 User 资源,即可实现用户的集中管理。并且,可以轻松地添加用户、查询用户以及对用户元信息的修改。
在 KubeCube 中,支持本地认证和外部认证。本地认证是指,在 KubeCube 中创建普通用户,用户再使用其创建时注册的用户名密码进行登录和认证。而外部认证,是指无需创建用户,通过第三方的认证平台认证用户身份,从而访问 KubeCube。下面将分别介绍这两种认证方式的实现。
在 KubeCube 中,主要是通过 JWT(JSON Web Token)进行用户的身份认证的。
在使用本地认证登录时,用户需要输入用户名和密码。KubeCube 会根据用户名在集群中查询 User cr 并比较密码,如果查询到 User 并且密码一致,视为登录成功。KubeCube 在更新用户登录状态后,会根据用户名生成 JWT 并拼接成 Bearer Token,存储在 cookie 中返回。
用户登录时校验用户名密码成功后的代码如下:
// generate token and return
authJwtImpl := jwt.GetAuthJwtImpl()
token, err := authJwtImpl.GenerateToken(&v1beta1.UserInfo{Username: name})
if err != nil {
response.FailReturn(c, errcode.AuthenticateError)
return
}
bearerToken := jwt.BearerTokenPrefix + " " + token
c.SetCookie(constants.AuthorizationHeader, bearerToken, int(authJwtImpl.TokenExpireDuration), "/", "", false, true)
user.Spec.Password = ""
response.SuccessReturn(c, user)
return
用户成功登录后,后续的每次请求,前端都会通过 cookie 带上该 JWT进行请求,后端认证中间件再对该 JWT 进行校验。如果有效,则会生成新的 token 返回,循环上述过程。这样,即使 KubeCube 中生成 JWT 的默认有效时间为1小时,只要用户持续访问,JWT 便会不断刷新使用户始终处于登录状态。
认证中间件的部分代码如下:
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
if !withinWhiteList(c.Request.URL, c.Request.Method, whiteList) {
authJwtImpl := jwt.GetAuthJwtImpl()
userToken, err := token.GetTokenFromReq(c.Request)
if err != nil {
response.FailReturn(c, errcode.AuthenticateError)
return
}
newToken, respInfo := authJwtImpl.RefreshToken(userToken)
if respInfo != nil {
response.FailReturn(c, errcode.AuthenticateError)
return
}
v := jwt.BearerTokenPrefix + " " + newToken
c.Request.Header.Set(constants.AuthorizationHeader, v)
c.SetCookie(constants.AuthorizationHeader, v, int(authJwtImpl.TokenExpireDuration), "/", "", false, true)
c.Next()
}
}
}
外部认证的实现目前主要分为3种,分别为通用认证、LDAP 认证和 OAuth2 认证。
为了方便用户可以对接一套自己的认证系统,KubeCube 中支持了一种通用认证方式。用户可以通过开启通用认证方式以及配置认证系统的地址,使用户在每一次访问 KubeCube 时,都会去自己的认证系统中进行认证。认证通过后,需要返回给 KubeCube 该用户的用户名,KubeCube 依然会根据该用户名生成对应的 Bearer Token 放在 header 中,以进行后续的权限校验等。主要的逻辑代码实现如下:
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
if !withinWhiteList(c.Request.URL, c.Request.Method, whiteList) {
authJwtImpl := jwt.GetAuthJwtImpl()
if generic.Config.GenericAuthIsEnable {
h := generic.GetProvider()
user, err := h.Authenticate(c.Request.Header)
if err != nil || user == nil {
clog.Error("generic auth error: %v", err)
response.FailReturn(c, errcode.AuthenticateError)
return
}
newToken, error := authJwtImpl.GenerateToken(&v1beta1.UserInfo{Username: user.GetUserName()})
if error != nil {
response.FailReturn(c, errcode.AuthenticateError)
return
}
b := jwt.BearerTokenPrefix + " " + newToken
c.Request.Header.Set(constants.AuthorizationHeader, b)
}
c.Next()
}
}
}
当用户选择 LDAP 登录方式时,用户输入用户名和密码。首先会检查集群内是否存在该用户,并且该用户是否为“禁用”状态。如果不存在或存在且为正常状态,则开始进行 LDAP 认证:
认证成功后,和本地认证的逻辑相同:如果该用户在集群中未存在,则根据用户名创建User cr; 并且根据该用户名生成对应的 Bearer Token 存储到 Cookie 中,在下次请求时携带以识别用户身份。
在 KubeCube 中 OAuth2 认证采用授权码模式,因为该模式是功能最完整、流程最严密的授权模式。OAuth2 通常的认证流程为:
用户访问客户端,后者将前者导向认证服务器。用户选择是否给予客户端授权。假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
在 KubeCube 的实现中,以 GitHub 登录为例:
基于以上的设计方案,可以轻松的推断出,OpenAPI的认证实现,也是通过 JWT 完成。User 和每组 AK、SK进行绑定,通过AK、SK查询到对应的 User,再通过该 User.Name 生成 Bearer Token 返回。在下次请求时,用户需要在 Cookie 或 header 中携带该 Token,KubeCube 认证中间件就可以通过该 Token 解析出用户身份,从而完成认证。
在中间件完成身份认证后会 refresh token,但是如果直接在请求头中携带该 token 请求 kube-apiserver 来完成集群认证,则需要在部署 KubeCube 时修改 kube-apiserver 的认证后端,即修改 kube-apiserver 的配置。这会对原生的 kubernetes 集群造成侵入,大大增加 KubeCube 的部署成本和运维成本。因此,我们需要建立另一模块来帮助完成集群认证——auth-proxy。
用户对 KubeCube 进行访问请求 kubernetes 资源时,在通过认证中间件进入到透传接口后,会走到 auth-proxy 模块;auth-proxy 将 request 中的 Bearer Token 解析为对应的 User;再使用 User impersonation 的方式,将 request 代理发送至 kube-apiserver,即使用 “admin” 用户伪装成当前用户来请求 kube-apiserver, 从而“跳过”认证,并且有利于后续鉴权。
KubeCube 的用户管理系统主要基于 User CRD 实现;认证系统支持了本地和外部两种认证方式,本地认证基于 JWT 实现,外部认证在第三方认证平台认证通过后同样需要在集群内创建一个 User cr,以进行后续的用户管理、权限绑定等。对于集群认证,主要使用了 Kubernetes 提供的 Impersonation 方法“跳过认证”。整体设计和实现相对简单,秉承了 KubeCube 轻量化的设计理念。
更多信息请参阅:
作者简介: 嘉慧,网易数帆高级工程师,KubeCube 社区核心成员