序文
KubeCube(https://kubecube.io)は、NetEase Shufanによって最近オープンソース化された軽量のエンタープライズレベルのコンテナプラットフォームであり、企業にkubernetesリソースの視覚的な管理と、統合されたマルチクラスタおよびマルチテナント管理機能を提供します。KubeCubeコミュニティは、一連の技術記事を通じてKubeCubeの設計機能と技術的実装を解釈し、開発者とユーザーがKubeCubeをより早く理解して開始できるようにします。この記事は、KubeCubeでのユーザー管理とID認証の実装に焦点を当てた第3部です。
ユーザー管理
すべてのKubernetesクラスタには、Kubernetesによって管理されるサービスアカウントと通常のユーザーの2種類のユーザーがあります。
Kubernetesは、通常のユーザーが次のいずれかの方法でクラスターに依存しないサービスによって管理されていることを前提としています。
- 秘密鍵の配布を担当する管理者
- KeystoneやGoogleアカウントなどのユーザーデータベース
- ユーザー名とパスワードのリストを含むファイル
このため、Kubernetesには通常のユーザーアカウントを表すオブジェクトは含まれていません。通常のユーザーの情報は、API呼び出しを介してクラスターに追加することはできません。
Kubernetesの公式声明によると、Kubernetes自体はユーザー管理機能を直接提供せず、通常のユーザーオブジェクトをサポートせず、通常のユーザーに関する情報を保存しません。ユーザーを作成する必要がある場合は、ユーザーの秘密鍵と証明書を作成し、その証明書を認証に使用する必要があります。さらに、ユーザー情報が保存されないため、クラスター管理者はユーザーを一元管理できず、他のユーザーを認識しません。したがって、KubeCubeは最初にユーザーの概念を再定義します。つまり、ユーザーをリソースタイプとして提供し、ユーザー情報を格納し、ユーザーを管理し、その後のID認証とアクセス許可の検証を容易にします。
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
ユーザーは、フロントエンドページで管理者が手動で作成することも、外部認証を使用して初めてログインするときにシステムが自動的に作成することもできます。したがって、ユーザーは、登録方法の観点から、システムの通常の登録ユーザーとサードパーティの許可されたログインユーザーに分けることができます。ただし、これら2つの作成方法では、対応するユーザーcrがコントロールクラスターに作成されます。次に、Wardenのリソース同期マネージャーは、crを制御クラスターからコンピューティングクラスターに同期し、その後の複数のクラスターの統合認証を容易にします。
このように、ユーザー管理ページでは、管理クラスター内のユーザーリソースを照会するだけで、一元化されたユーザー管理を実現できます。また、ユーザーは、ユーザーメタ情報に簡単に追加、照会、および変更できます。
認証
KubeCubeでは、ローカル認証と外部認証の両方がサポートされています。ローカル認証とは、KubeCubeで共通ユーザーを作成し、作成時に登録したユーザー名とパスワードを使用してログイン・認証することです。外部認証とは、ユーザーがユーザーを作成せずに、サードパーティの認証プラットフォームを介してIDを認証することにより、KubeCubeにアクセスできることを意味します。これら2つの認証方法の実装については、以下で個別に紹介します。
ローカル認証
KubeCubeでは、ユーザー認証は主にJWT(JSON Web Token)を介して実行されます。
ローカル認証でログインする場合、ユーザーはユーザー名とパスワードを入力する必要があります。KubeCubeは、ユーザー名に従ってクラスター内のユーザーcrを照会し、パスワードを比較します。ユーザーが照会され、パスワードが同じである場合、ログインは成功したと見なされます。KubeCubeはユーザーのログインステータスを更新した後、ユーザー名に従ってJWTを生成し、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を検証します。有効な場合、新しいトークンが生成されて返され、上記のプロセスがループします。このように、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()
}
}
}
外部認証
外部認証の実装は、現在、主に一般認証、LDAP認証、OAuth2認証の3種類に分けられます。
ユニバーサル認証
ユーザーが自分の認証システムに接続しやすくするために、KubeCubeは一般的な認証方法をサポートしています。ユーザーは、一般的な認証方法を有効にし、認証システムのアドレスを構成できるため、ユーザーがKubeCubeにアクセスするたびに、自分の認証システムにアクセスして認証を受けることができます。認証に合格した後、ユーザーのユーザー名をKubeCubeに返す必要があります。KubeCubeは、ユーザー名に従って対応するベアラートークンを生成し、その後の権限検証のためにヘッダーに配置します。主なロジックコードは次のように実装されています。
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認証を開始します。
-
LDAPクライアントとして、KubeCubeはユーザーのユーザー名とパスワードを取得し、管理者DNと管理者パスワードをパラメーターとして使用して管理者バインド要求メッセージをLDAPサーバーに送信し、照会許可を取得します。
-
LDAPサーバーは、管理者バインド要求メッセージを受信した後、管理者DNと管理者パスワードが正しいかどうかを確認します。管理者DNと管理者パスワードが正しい場合は、正常にバインドされた管理者バインド応答メッセージをKubeCubeに送信します。
-
KubeCubeは、バインディング応答メッセージを受信した後、ユーザーが入力したユーザー名パラメーターを使用してフィルター条件を作成し、ユーザーDNクエリ要求メッセージをLDAPサーバーに送信します。例:構築フィルターの条件はCN=User2です。
-
LDAPサーバーは、ユーザーDNクエリ要求メッセージを受信した後、メッセージ内のクエリ開始点、クエリスコープ、およびフィルター条件に従ってユーザーDNを検索します。クエリが成功した場合は、クエリが成功したという応答メッセージをKubeCubeに送信します。クエリから取得した1つ以上のユーザーDNが存在する可能性があります。取得したユーザーが1人でない場合は、ユーザー名またはパスワードが間違っていると考えられ、認証に失敗します。
-
KubeCubeは、クエリから取得したユーザーDNと、ユーザーがパラメーターとして入力したパスワードに従って、ユーザーバインディング要求メッセージをLDAPサーバーに送信します。
-
LDAPサーバーは、ユーザーバインディング要求メッセージを受信した後、ユーザーが入力したパスワードが正しいかどうかを確認します。
- ユーザーが入力したパスワードが正しければ、バインドに成功したというバインド応答メッセージがKubeCubeに送信されます。
- ユーザーが入力したパスワードが正しくない場合、バインド失敗の応答メッセージがKubeCubeに送信されます。KubeCubeは、照会された次のユーザーのDNをパラメーターとして受け取り、DNが正常にバインドされるまで、LDAPサーバーにバインド要求を送信し続けます。すべてのユーザーDNがバインドに失敗した場合、KubeCubeは認証が失敗したことをユーザーに通知します。
認証が成功した後、ローカル認証のロジックは同じです。ユーザーがクラスターに存在しない場合、ユーザーcrはユーザー名に従って作成され、対応するベアラートークンはユーザー名とCookieに保存され、ユーザーIDの次の識別要求で実行されます。
OAuth2認証
KubeCubeでは、OAuth2認証は認証コードモードを採用しています。これは、このモードが最も完全な機能と最も厳密なプロセスを備えた認証モードであるためです。OAuth2の通常の認証フローは次のとおりです。
- ユーザーはクライアントにアクセスし、クライアントは前者を認証サーバーに転送します。
- ユーザーは、クライアントを承認するかどうかを選択します。
- ユーザーが認証を付与すると、認証サーバーは、認証コードとともに、クライアントの事前に指定された「リダイレクトURI」(リダイレクトURI)にユーザーを誘導します。
- クライアントは認証コードを受け取り、以前の「リダイレクトURI」を添付して、認証サーバーにトークンを要求します。この手順は、クライアントのバックグラウンドでサーバー上で実行され、ユーザーには表示されません。
- 認証サーバーは認証コードとリダイレクトURIをチェックし、それが正しいことを確認した後、アクセストークンと更新トークンをクライアントに送信します。
KubeCubeの実装では、例としてGitHubログインを取り上げます。
- ユーザーがログイン時にGitHub認証ログインを選択すると、フロントエンドがリクエストをGitHubに転送します。
- GitHubは、KubeCubeを承認することに同意するかどうかをユーザーに尋ねます。
- ユーザーが同意すると、GitHubはKubeCube(
/oauth/redirect
)にリダイレクトし、認証コード(code
)を送り返します。 - KubeCubeは認証コードを使用してGitHubにトークンをリクエストします(
access_token
); - GitHubはtoken(
access_token
);を返します。 - KubeCubeはトークン(
access_token
)を使用してGitHubにユーザー情報データを要求します。 - クラスタにクエリを実行します。ユーザーが存在しない場合は、ユーザー情報に基づいてユーザーcrを作成します。
- ユーザー名に従ってクラスターにアクセスするためのベアラートークンを生成し、認証の成功を返します。
- フロントエンドはベアラートークンをCookieに保存し、次のリクエストでそれを運びます。
OpenAPI認証
上記の設計スキームに基づいて、OpenAPIの認証実装もJWTを介して完了していることが簡単に推測できます。ユーザーはAKとSKの各グループにバインドされ、対応するユーザーはAKとSKを介して照会され、ベアラートークンが生成され、User.Nameを介して返されます。次のリクエストでは、ユーザーはCookieまたはヘッダーでトークンを運ぶ必要があります。KubeCube認証ミドルウェアはトークンを使用してユーザーのIDを解析し、認証を完了することができます。
クラスター認証
ミドルウェアが認証を完了すると、トークンが更新されますが、トークンをリクエストヘッダーに直接入れて、kube-apiserverにクラスター認証を完了するようにリクエストする場合は、KubeCubeをデプロイするときにkube-apiserverの認証バックエンドを変更する必要があります。つまり、kube-apiserver構成を変更します。これにより、ネイティブのkubernetesクラスターに侵入し、KubeCubeのデプロイコストと運用およびメンテナンスコストが大幅に増加します。したがって、クラスター認証を完了するために別のモジュール(auth-proxy)を構築する必要があります。
ユーザーがKubeCubeにアクセスし、kubernetesリソースを要求すると、認証ミドルウェアを介して透過的な送信インターフェイスに入った後、auth-proxyモジュールに移動します。auth-proxyは、要求内のベアラートークンを対応するユーザーに解析します。ユーザーのなりすましこのようにして、要求プロキシがkube-apiserverに送信されます。つまり、「admin」ユーザーは、現在のユーザーになりすましてkube-apiserverを要求します。これにより、認証が「スキップ」され、後続の認証が容易になります。
エピローグ
KubeCubeのユーザー管理システムは主にユーザーCRDに基づいて実装され、認証システムはローカルと外部の両方の認証方法をサポートします。ローカル認証はJWTに基づいて実装されます。外部認証がサードパーティの認証プラットフォームの認証に合格した後、また、クラスター内にユーザーcrを作成する必要があります。後続のユーザー管理、アクセス許可のバインドなどを実行します。クラスタ認証には、主にKubernetesが提供するなりすまし方式「スキップ認証」が使用されます。全体的な設計と実装は比較的単純で、KubeCubeの軽量設計コンセプトに準拠しています。
詳細については、以下を参照してください。
-
KubeCube公式ウェブサイト:https ://www.kubecube.io/
-
KubeCubeソースコード:https ://github.com/kubecube-io/kubecube
著者について: Jiahui、NetEase Shufanのシニアエンジニア、KubeCubeコミュニティのコアメンバー