0%

Kubeflow 如何搭配 Istio 實作 Multi-user 認證與權限管理(下)

前言

在 Kubeflow 部署 中提到了透過 Kubernetes RBACIstio ServiceRole/ServiceRoleBinding 來做服務與資源的權限認證,以達成 Multi-user 情境。

今天來說說如何進行認證,以及這個獨立的 AuthService 是什麼。

Istio-Gateway and EnvoyFilter 做認證

我們提到Istio在判斷該使用者能否連線使用該服務透過的是請求上的 Header: kubeflow-userid ,這便是在做使用者認證時另外覆寫夾帶上去的,使用的是Kubeflow額外開發的認證服務 AuthService

從這張圖可以看到所有的請求都會被這個 Istio Gateway 導向這個 OIDC AuthService 並接上第三方身份管理服務來認證使用者。

這個將請求導流的方法利用到的就是 Istio EnvoyFilter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$ kubectl get EnvoyFilter/authn-filter -n kubeflow

apiVersion: networking.istio.io/v1alpha3
kind: EnvoyFilter
metadata:
generation: 1
name: authn-filter
namespace: kubeflow
spec:
filters:
- filterConfig:
httpService:
authorizationRequest:
allowedHeaders:
patterns:
- exact: cookie
authorizationResponse:
allowedUpstreamHeaders:
patterns:
- exact: kubeflow-userid
serverUri:
cluster: outbound|8080||authservice.istio-system.svc.cluster.local
failureModeAllow: false
timeout: 10s
uri: http://authservice.istio-system.svc.cluster.local
statusOnError:
code: GatewayTimeout
filterName: envoy.ext_authz
filterType: HTTP
insertPosition:
index: FIRST
listenerMatch:
listenerType: GATEWAY
portNumber: 443
workloadLabels:
istio: ingressgateway

可以看到上面直接在 Istio ingressgateway 上面加上一個 HTTP Filter 將從 port:443 的流量全部倒向 http://authservice.istio-system.svc.cluster.local ,並允許 UpstreamHeaders 加上 Header: kubeflow-userid

authorizationResponse.allowed_upstream_headers: When this list is set, authorization response headers that have a correspondent match will be added to the original client request. Note that coexistent headers will be overridden.

AuthService and OIDC Workflow

如同上面所說,所有的請求都會先通過 Istio ingressgateway ,而被導向 AuthService 這一個獨立的服務來對 Dex 進行 OIDC 認證,而 Dex 可以再接第三方的身份管理服務。

而完整的請求流程如同下圖,

圖中的Ambassador對應到Kubeflow目前的架構就是Istio ingressgateway

  1. 當使用者未登入時,AuthService 會將該使用者導引到登入頁面,進行登入與 OIDC 流程。
  2. 使用者登入後,會呼叫定義好的 callback uri,在這邊是 https://KUBEFLOWDOMAIN/login/oidc
  3. 請求再一次通過 gateway 並被導引到 AuthService,這一次 AuthService 有了使用者的登入資訊。
  4. AuthService 將使用者登入資訊轉成 jwt 並且存入 cookie ,並將使用者導引回最初存取的 Kubeflow 服務。
  5. 請求這一次通過 AuthService ,會被 AuthService 加上從 jwt 解析出來的 user_id 並被加上 header kubeflow_user:user_id
  6. 接著由前一篇提到的 istio ServiceRole/ServiceRoleBinding 來從 header 中夾帶的使用者名稱判斷該請求是否可以存取目標服務。

而程式碼可以直接參考以下連結,v0.7 版所用的 AuthService 即是編譯自下面這個 arrikto/ambassador-auth-oidc:feature-kubeflow project 與 branch。
https://github.com/arrikto/ambassador-auth-oidc/tree/feature-kubeflow

Kfam

Project: https://github.com/kubeflow/kubeflow/tree/master/components/access-management

我們現在了解了 Kubeflow 如何實作認證與權限管理,每一個使用者會擁有自己的一個 Namespace ,可以與其他使用者做隔離。
但是關於共享資源的 group 又是怎麼實現與管理的呢?

這就要提到 Kubeflow 另一個元件 - Kfam

介紹

Kfam(Kubeflow Access Management API) 是一個 HTTP Server ,提供 namespace 層級的 access control 管理。

前面提到權限管理由兩種 RBAC 資源來實現,而 Kfam 就是提供了一層管理以下兩種資源的 API。

  1. Profile

    • 標示一個使用者
    • 擁有屬於自己的 Namespace
    • 創建相關的 istio RBAC 資源管理服務權限
    • 創建相關的 K8s RBAC 資源管理 K8s 資源權限
    • 透過 Binding 來管理使用者與權限關係
  2. Binding

    • 標示使用者與 Namespace 關係
    • 允許使用者操作該 Namespace 資源

以下直接使用情境來說明 group 如何運作。

不屬於該Group

如果一個使用者為 admin ,且希望能夠取得所有在 user 這個使用者的 namespace 底下的 notebook ,則會得到這樣的錯誤

這非常合理,因為 admin 並沒有權限取得 user 的資源或是服務,且 admin 並不共享 user namespace 。

而這部分的判斷其實是來自這一個部分,透過 K8s RBAC 達成。因為現在所有的 Kubeflow 服務都可以從請求的 header 中取得 user_id ,所以可以知道目前希望操作 notebook 的使用者是誰,再參考 K8s RBAC ,即可知道該使用者是否有權限。

直接看程式碼吧

也因此上面的邏輯其實是實作在 Kubeflow 的 jupyter-web-app 服務。

components/jupyter-web-app/backend/kubeflow_jupyter/common/api.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_notebooks(ns, user=None):
if not is_authorized(user, ns):
return {
"success": False,
"log": "User '{}' is not authorized for namespace '{}'".format(
user, ns
)
}

return wrap_resp(
"notebooks",
custom_api.list_namespaced_custom_object,
"kubeflow.org",
"v1beta1",
ns,
"notebooks"
)

components/jupyter-web-app/backend/kubeflow_jupyter/common/api.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def is_authorized(user, namespace):
'''
Queries KFAM for whether the provided user has access
to the specific namespace
'''
if user is None:
# In case a user is not present, preserve the behavior from 0.5
# Pass the authorization check and make the calls with the webapp's SA
return True

try:
resp = requests.get("http://{}/kfam/v1/bindings?namespace={}".format(
KFAM, namespace)
)
except Exception as e:
logger.warning("Error talking to KFAM: {}".format(parse_error(e)))
return False

if resp.status_code == 200:
# Iterate through the namespace's bindings and check for the user
for binding in resp.json().get("bindings", []):
if binding["user"]["name"] == user:
return True

return False
else:
logger.warning("{}: Error talking to KFAM!".format(resp.status_code))
return False

透過從 Kfam 檢查 Binding , Jupyter UI 即可知道該使用者是否有權限存取操作特定 Namespace 底下的資源。

那我們如何將 admin 加入成為 user 的共同編輯者呢?

將使用者加入某一個group

有了上面這個概念後就會知道,其實 Kubeflow 沒有特別創建出不同於 user 的一個 group 的資源,一個 user 也可以是 group,只要我們讓多個使用者有跟該 user 一樣在特定 namespace 底下的權限。

如下圖,其實 ML Engineer 也是一個 user 有自己的 Namespace ,只是其餘使用者也擁有操作 Namespace:ML Engineer 資源的權限。

直接看程式碼吧

在安裝好的 Kubeflow 0.7 CentralDashboard 你可以看到有一個頁面可以加入其餘使用者成為 Contributor ,這背後的邏輯如下。

components/centraldashboard/app/api_workgroup.ts#L189

1
2
3
4
5
6
7
8
9
10
11
12
const binding = mapSimpleBindingToWorkgroupBinding({
user: contributor,
namespace,
role: 'contributor',
});
const {headers} = req;
delete headers['content-length'];
const actionAPI = action === 'create' ? 'createBinding' : 'deleteBinding';
await profilesService[actionAPI](binding, {headers}); // actionAPI = Create
errIndex++;
const users = await this.getContributors(namespace);
res.json(users);

當你將另一個使用者加為 Contributor 時,他就創建了一個 Binding 標示該使用者與你的 Namespace 有聯繫。

而這個 Binding 會近一步創建 K8s 與 Istio 的 RBAC 資源,來賦予該使用者在這個 Namespace 的權限。

舉例來說,一個使用者 Jack ,被加入成為另一個使用者 MLEngineer 的 Contributor 。則會自動創建 RBAC 資源賦予 Jack 在該 Namespace 底下的 ClusterRole:adminServiceRole:ns-access-istio 的權限。

而這幾個 RBAC 資源的創建就是在 Kfam API 裡面。

kfam/routers.go

1
2
3
4
5
6
Route{
"CreateBinding",
strings.ToUpper("Post"),
"/kfam/v1/bindings",
kfamV1Alpha1.CreateBinding,
}

kfam/api_default.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (c *KfamV1Alpha1Client) CreateBinding(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json; charset=UTF-8")
var binding Binding
if err := json.NewDecoder(r.Body).Decode(&binding); err != nil {
json.NewEncoder(w).Encode(err)
w.WriteHeader(http.StatusForbidden)
return
}
// check permission before create binding
useremail := c.getUserEmail(r.Header)
if c.isOwnerOrAdmin(useremail, binding.ReferredNamespace) {
err := c.bindingClient.Create(&binding, c.userIdHeader, c.userIdPrefix)
if err == nil {
w.WriteHeader(http.StatusOK)
} else {
w.WriteHeader(http.StatusForbidden)
w.Write([]byte(err.Error()))
}
} else {
w.WriteHeader(http.StatusForbidden)
}
}

kfam/bindings.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
func (c *BindingClient) Create(binding *Binding, userIdHeader string, userIdPrefix string) error {
// TODO: permission check before go ahead
bindingName, err := getBindingName(binding)
if err != nil {
return err
}
roleBinding := rbacv1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{USER: binding.User.Name, ROLE: binding.RoleRef.Name},
Name: bindingName,
},
RoleRef: rbacv1.RoleRef{
APIGroup: binding.RoleRef.APIGroup,
Kind: binding.RoleRef.Kind,
Name: roleBindingNameMap[binding.RoleRef.Name],
},
Subjects: []rbacv1.Subject{
*binding.User,
},
}
_, err = c.kubeClient.RbacV1().RoleBindings(binding.ReferredNamespace).Create(&roleBinding)
if err != nil {
return err
}

// create istio service role binding
istioServiceRoleBinding := &istiorbac.ServiceRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{USER: binding.User.Name, ROLE: binding.RoleRef.Name},
Name: bindingName,
Namespace: binding.ReferredNamespace,
},
Spec: istiorbac.ServiceRoleBindingSpec{
Subjects: []*istiorbac.Subject{
{
Properties: map[string]string{fmt.Sprintf("request.headers[%v]", userIdHeader): userIdPrefix + binding.User.Name},
},
},
RoleRef: &istiorbac.RoleRef{
Kind: "ServiceRole",
Name: SERVICEROLEISTIO,
},
},
}
result := istiorbac.ServiceRoleBinding{}
return c.restClient.
Post().
Namespace(binding.ReferredNamespace).
Resource(ServiceRoleBinding).
Body(istioServiceRoleBinding).
Do().
Into(&result)
}

小結

透過今天這一篇,我們就完整暸解 Kubeflow 如何利用 Istio 來進行認證。並透過完整的身份管理來建構出 Multi-user 與 Group 的使用情境。
這個方法我認為非常推薦,因為全程幾乎只用到 Kubernetes 與 Istio 的 RBAC 即做到使用者權限管理,而這些是 Kubernetes 上面的資源與物件就可以定義,並不需要引入第三方的服務來設定與定義,例如使用OpenStack Keystone 這樣的服務,因此實作方面也非常簡單。
另外未來除了權限的設定外,還可以在 Profile CRD 中設定每一個使用者可以使用的資源,例如 CPU 與 Mem ,如以下的範例,可以說完全利用 Kubernetes Native 的方法實作了認證、權限與資源限制的方法,值得大家學習。