KubeCube 使用者管理與身份認證

語言: CN / TW / HK

前言

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 登入方式時,使用者輸入使用者名稱和密碼。首先會檢查叢集內是否存在該使用者,並且該使用者是否為“禁用”狀態。如果不存在或存在且為正常狀態,則開始進行 LDAP 認證:

  1. KubeCube 作為 LDAP 客戶端,獲取到使用者的使用者名稱和密碼,以管理員DN和管理員密碼為引數向 LDAP 伺服器傳送管理員繫結請求報文以獲得查詢許可權。

  2. LDAP 伺服器收到管理員繫結請求報文後,驗證管理員DN和管理員密碼是否正確。如果管理員DN和管理員密碼正確,則向 KubeCube 傳送繫結成功的管理員繫結響應報文。

  3. KubeCube 收到繫結響應報文後,以使用者輸入的使用者名稱為引數構造過濾條件,向 LDAP 伺服器傳送使用者DN查詢請求報文。例如:構造過濾條件為 CN=User2。

  4. LDAP 伺服器收到使用者DN查詢請求報文後,根據報文中的查詢起點、查詢範圍、以及過濾條件,對使用者DN進行查詢。如果查詢成功,則向 KubeCube 傳送查詢成功的響應報文。查詢得到的使用者DN可以是一個或多個。如果得到的使用者不為一個,認為使用者名稱或密碼錯誤,認證失敗。

  5. KubeCube 根據查詢得到的使用者DN和使用者輸入的密碼為引數,向 LDAP 伺服器傳送使用者繫結請求報文。

  6. LDAP 伺服器收到使用者繫結請求報文後,檢查使用者輸入的密碼是否正確。

  • 如果使用者輸入的密碼正確,則向 KubeCube 傳送繫結成功的繫結響應報文。
  • 如果使用者輸入的密碼不正確,則向 KubeCube 傳送繫結失敗的響應報文。KubeCube 以查詢到的下一個使用者DN為引數,繼續向 LDAP 伺服器傳送繫結請求,直至有一個DN繫結成功。如果所有使用者DN都繫結失敗,則 KubeCube 通知使用者認證失敗。

認證成功後,和本地認證的邏輯相同:如果該使用者在叢集中未存在,則根據使用者名稱建立User cr; 並且根據該使用者名稱生成對應的 Bearer Token 儲存到 Cookie 中,在下次請求時攜帶以識別使用者身份。

使用者認證

OAuth2 認證

在 KubeCube 中 OAuth2 認證採用授權碼模式,因為該模式是功能最完整、流程最嚴密的授權模式。OAuth2 通常的認證流程為:

  1. 使用者訪問客戶端,後者將前者導向認證伺服器。
  2. 使用者選擇是否給予客戶端授權。
  3. 假設使用者給予授權,認證伺服器將使用者導向客戶端事先指定的"重定向URI"(redirection URI),同時附上一個授權碼。
  4. 客戶端收到授權碼,附上早先的"重定向URI",向認證伺服器申請令牌。這一步是在客戶端的後臺的伺服器上完成的,對使用者不可見。
  5. 認證伺服器核對了授權碼和重定向URI,確認無誤後,向客戶端傳送訪問令牌(access token)和更新令牌(refresh token)。

在 KubeCube 的實現中,以 GitHub 登入為例:

  1. 使用者在登入時選擇 GitHub 認證登入,前端將請求轉發給 GitHub;
  2. GitHub 詢問使用者是否同意授權給 KubeCube;
  3. 如果使用者同意,GitHub 就會重定向回KubeCube(/oauth/redirect),同時發回一個授權碼(code);
  4. KubeCube 使用授權碼,向 GitHub 請求令牌(access_token);
  5. GitHub 返回令牌(access_token);
  6. KubeCube 使用令牌(access_token),向 GitHub 請求使用者資訊資料;
  7. 查詢叢集,如果該使用者不存在,則根據使用者資訊建立 User cr;
  8. 根據使用者名稱生成訪問叢集的 Bearer Token,並返回認證成功;
  9. 前端將 Bearer Token 儲存到 Cookie 中,在下次請求時攜帶。

OpenAPI 認證

基於以上的設計方案,可以輕鬆的推斷出,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 社群核心成員