用户伪装功能在 KubeVela 中的应用

用户伪装功能在 KubeVela 中的应用

KubeVela 中使用用户伪装功能的主要有两个模块:KubeVela Controller 和 KubeVela API Server。

  • KubeVela Controller:实现了 KubeVela 的主要逻辑;
  • KubeVela API Server:提供 API 接口给 VelaUX。

在 KubeVela 核心组件里有两个和用户伪装相关的功能:应用认证和 ServiceAccount 伪装。VelaUX 由于自身带了一套用户权限相关的功能,当开启用户伪装后,会注入登录的用户信息作为伪装用户。

KubeVela Controller

应用认证

KubeVela 提供了一个叫做应用认证(AuthenticateApplication)的特性开关(featuregate),当该功能开启时 KubeVela Controller 会使用最后创建或修改的身份权限作为创建资源的用户身份。这个功能可以防止用户通过创建包含自身权限之外的应用来提权。

应用认证

应用认证有两个阶段:应用绑定身份和伪装身份。

应用绑定身份

当用户创建应用请求(例如创建一个新应用或修改现有应用) 请求将首先由 KubeVela 中的 Application MutatingAdmissionWebhook 处理。Webhook 将从请求中提取用户信息并记录到应用的注解中。

应用身份绑定的逻辑由 application.MutatingHandler.handleIdentity() 方法处理:

pkg/webhook/core.oam.dev/v1alpha2/application/mutating_handler.go:52
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (h *MutatingHandler) handleIdentity(ctx context.Context, req admission.Request, _ *v1beta1.Application, app *v1beta1.Application) (bool, error) {
// 检查是否启用应用认证
if !utilfeature.DefaultMutableFeatureGate.Enabled(features.AuthenticateApplication) {
return false, nil
}

if slices.Contains(h.skipUsers, req.UserInfo.Username) {
return false, nil
}

if metav1.HasAnnotation(app.ObjectMeta, oam.AnnotationApplicationServiceAccountName) {
return false, errors.New("service-account annotation is not permitted when authentication enabled")
}
klog.Infof("[ApplicationMutatingHandler] Setting UserInfo into Application, UserInfo: %v, Application: %s/%s", req.UserInfo, app.GetNamespace(), app.GetName())
// 保存用户信息到应用注解中
auth.SetUserInfoInAnnotation(&app.ObjectMeta, req.UserInfo)
return true, nil
}

如果开启了应用认证,则不能使用 ServiceAccount 伪装指定应用运行的 SA,这两个功能互斥。

应用认证功能的 Webhook 其实是将当前 APIServer 的用户信息保存到 Application 以下注解中:

  • app.oam.dev/username
  • app.oam.dev/group

伪装身份

上面注入的伪装注解在 GetUserInfoInAnnotation() 函数中提取出来:

pkg/auth/userinfo.go:83
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
func GetUserInfoInAnnotation(obj *metav1.ObjectMeta) user.Info {
annotations := obj.GetAnnotations()
if annotations == nil {
annotations = map[string]string{}
}

name := annotations[oam.AnnotationApplicationUsername]
if serviceAccountName := annotations[oam.AnnotationApplicationServiceAccountName]; serviceAccountName != "" && name == "" {
name = fmt.Sprintf("system:serviceaccount:%s:%s", obj.GetNamespace(), serviceAccountName)
}

if name == "" && utilfeature.DefaultMutableFeatureGate.Enabled(features.AuthenticateApplication) {
name = AuthenticationDefaultUser
}

return &user.DefaultInfo{
Name: name,
Groups: slices.Filter(
[]string{},
strings.Split(annotations[oam.AnnotationApplicationGroup], groupSeparator),
func(s string) bool {
return len(strings.TrimSpace(s)) > 0
}),
}
}

可以看出,提取时会同时处理应用认证和 ServiceAccount 伪装两个方式注入的注解。提取后交由外层两个方法使用,写入提供的 Context 中:

pkg/auth/userinfo.go:45
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// ContextWithUserInfo inject username & group from app annotations into context
// If serviceAccount is set and username is empty, identity will user the serviceAccount
func ContextWithUserInfo(ctx context.Context, app *v1beta1.Application) context.Context {
if app == nil {
return ctx
}
return request.WithUser(ctx, GetUserInfoInAnnotation(&app.ObjectMeta))
}

// MonitorContextWithUserInfo inject username & group from app annotations into monitor context
func MonitorContextWithUserInfo(ctx monitorContext.Context, app *v1beta1.Application) monitorContext.Context {
_ctx := ctx.GetContext()
authCtx := ContextWithUserInfo(_ctx, app)
ctx.SetContext(authCtx)
return ctx
}

在 KubeVela Controller 的 Reconcile() 方法用于处理 Application 资源对象的事件,在调用 Workflow 执行应用的工作流前注入到 Context 中。

pkg/controller/core.oam.dev/v1alpha2/application/application_controller.go:110
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (r *Reconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
ctx, cancel := ctrlrec.NewReconcileContext(ctx)
defer cancel()
logCtx := monitorContext.NewTraceContext(ctx, "").AddTag("application", req.String(), "controller", "application")
logCtx.Info("Start reconcile application")
defer logCtx.Commit("End reconcile application")
app := new(v1beta1.Application)

// ...略

executor := executor.New(workflowInstance, r.Client, nil)
authCtx := logCtx.Fork("execute application workflow")
defer authCtx.Commit("finish execute application workflow")
authCtx = auth.MonitorContextWithUserInfo(authCtx, app)
workflowState, err := executor.ExecuteRunners(authCtx, runners)

// ...略
}

在 KubeVela Controller 启动的 run() 函数中,使用 auth.NewImpersonatingRoundTripper 包装了一下请求:

1
2
3
4
5
6
7
8
9
10
func run(ctx context.Context, s *options.CoreOptions) error {
// ... 略

restConfig := ctrl.GetConfigOrDie()
restConfig.UserAgent = types.KubeVelaName + "/" + version.GitRevision
restConfig.QPS = float32(s.QPS)
restConfig.Burst = s.Burst
restConfig.Wrap(auth.NewImpersonatingRoundTripper)
// ... 略
}

auth.NewImpersonatingRoundTripper 实现了 RoundTripper 接口,当 Client 请求发出时会调用 RoundTrip() 方法对请求进行处理,impersonatingRoundTripper 就是 KubeVela 对发给 Cluster Gateway 伪装请求头进行处理的地方。

pkg/auth/round_trippers.go:50
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (rt *impersonatingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
ctx := req.Context()
req = req.Clone(ctx)
userInfo, exists := request.UserFrom(ctx)
klog.V(7).Infof("impersonation request log. path: %s method: %s user info: %+v", req.URL.String(), req.Method, userInfo)
if exists && userInfo != nil {
if name := userInfo.GetName(); name != "" {
req.Header.Set(transport.ImpersonateUserHeader, name)
for _, group := range userInfo.GetGroups() {
req.Header.Add(transport.ImpersonateGroupHeader, group)
}
q := req.URL.Query()
q.Add(impersonateKey, "true")
req.URL.RawQuery = q.Encode()
}
}
return rt.rt.RoundTrip(req)
}

代码逻辑:

  • 从 Context 取出用户信息,保存到请求头;
  • 给 URL Query 增加一个 impersonate=true 参数(给 Cluster Gateway 作判断)

接下来请求将交给后面的 RoundTripper 处理,最终转发给 Cluster Gateway,下一步逻辑请看前面文章《Kubernetes 中的用户伪装功能》的 APIServer 伪装处理Cluster Gateway 伪装处理的内容。

交互流程

交互流程

ServiceAccount 伪装

当应用认证特性关闭时可以使用 ServiceAccount 伪装功能,让应用以指定的 ServiceAccount 运行,只需要配置 app.oam.dev/service-account-name 注解既可。

使用 ServiceAccount 伪装功能指定的 ServiceAccount 需要在纳管集群事先创建,否则会报错。

比如以下名为 deployer 的 ServiceAccount 需要进行角色绑定后才能使用:

deployer 授权
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
apiVersion: v1
kind: ServiceAccount
metadata:
name: deployer
namespace: demo-service
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployments:admin
namespace: demo-service-prod
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deployments:admin
namespace: demo-service-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: deployments:admin
subjects:
- kind: ServiceAccount
name: deployer
namespace: demo-service

然后在 Application 中使用:

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
apiVersion: core.oam.dev/v1beta1
kind: Application
metadata:
name: multi-env-demo-with-service-account
namespace: demo-service
annotations:
app.oam.dev/service-account-name: deployer # the name of the ServiceAccount we created
spec:
components:
- name: nginx-server
type: webservice
properties:
image: nginx:1.21
port: 80
policies:
- name: env
type: env-binding
properties:
created: false
envs:
- name: prod
patch:
components:
- name: nginx-server
type: webservice
properties:
image: nginx:1.20
port: 80
placement:
namespaceSelector:
name: demo-service-prod
workflow:
steps:
- name: deploy-prod-server
type: deploy2env
properties:
policy: env
env: prod

应用认证注入的 app.oam.dev/usernameapp.oam.dev/group 注解其实我们可以手动在 Application 注解指定,可以完成和上述一样的功能。

KubeVela API Server

当 KubeVela 的 API Server 启动时,运行通过以下调用链后,来到 setKubeConfig() 函数:

server.Run() -> run() -> restServer.Run() -> s.buildIoCContainer() -> clients.SetKubeConfig() -> setKubeConfig()

这个函数使用 auth.NewImpersonatingRoundTripper 包装了 RoundTripper 以支持用户伪装的处理,前面 伪装身份 已经介绍过这个结构体,这里不再赘述。

另外 s.buildIoCContainer() 中还使用 NewAuthClient 创建了一个包装 Client

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *restServer) buildIoCContainer() error {
// infrastructure

err := clients.SetKubeConfig(s.cfg)
if err != nil {
return err
}
kubeConfig, err := clients.GetKubeConfig()
if err != nil {
return err
}
kubeClient, err := clients.GetKubeClient()
if err != nil {
return err
}
authClient := utils.NewAuthClient(kubeClient)
// ...略
}

authClient 作用是自动将 VelaUX 当前登录的用户信息提取出来,以支持伪装用户的方式保存到 Context 中:

  • 使用原生的 request.WithUser() 方法包装 ctx;
  • auth.impersonatingRoundTripper{} 提取 ctx 保存到请求头。

authClient 是以代理模式方式实现的,比如 ClientGet() 方法,调用前调用 ContextWithUserInfo() 处理用户信息:

pkg/apiserver/utils/auth.go:90
1
2
3
4
func (c *authClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object) error {
ctx = ContextWithUserInfo(ctx)
return c.Client.Get(ctx, key, obj)
}

ContextWithUserInfo() 方法在提取用户信息前先检查是否开启 EnableImpersonation 特性,开启了才提取用户信息并转换成 Kubernetes 的用户对象,默认 EnableImpersonation 是关闭的。

pkg/apiserver/utils/auth.go:48
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func ContextWithUserInfo(ctx context.Context) context.Context {
// 检查是否开启 API Server 的用户伪装
if !features.APIServerFeatureGate.Enabled(features.APIServerEnableImpersonation) {
return ctx
}
userInfo := &user.DefaultInfo{Name: user.Anonymous}
if username, ok := UsernameFrom(ctx); ok {
userInfo.Name = username
}
if project, ok := ProjectFrom(ctx); ok && project != "" {
userInfo.Groups = []string{KubeVelaProjectGroupPrefix + project, auth.KubeVelaClientGroup}
} else {
userInfo.Groups = []string{UXDefaultGroup}
}
if userInfo.Name == model.DefaultAdminUserName && features.APIServerFeatureGate.Enabled(features.APIServerEnableAdminImpersonation) {
userInfo.Groups = []string{UXDefaultGroup}
}
return request.WithUser(ctx, userInfo)
}

总结

KubeVela 作为一个应用交付平台,保证应用运行权限与创建用户的权限一致、防止应用越权是重中之重的功能。KubeVela 结合 Cluster Gateway 很好地利用了 Kubernetes 原生的用户伪装功能实现了这个需求,从更低层防止应用越权,是权限控制上比较好的实践思路。

借此思路,我也在我们公司的多集群管理系统中使用了用户伪装的功能,解决了权限控制总是浮于表面的问题。

用户伪装功能在 KubeVela 中的应用

https://blog.imoe.tech/2023/07/18/kubevela-user-impersonate/

作者

Jakes Lee

发布于

2023-07-18

更新于

2023-08-16

许可协议

评论