likes
comments
collection
share

如何优雅的让 Pod 通过 ServiceAccount 访问 Kubernetes Apiserver

作者站长头像
站长
· 阅读数 13

我是 LEE,老李,一个在 IT 行业摸爬滚打 17 年的技术老兵。

事件背景

某天早上刚到公司工位上,正准备开会。被一个业务组项目负责人抓住了,然后着急的说到:“老李啊,跟你说。昨天晚上准备要上线的 Ingressroutes 监控分析模块不能上线了,现在导致我们这边的数据清洗模块,根据计划今天下午应该能对接与接收数据的。 现在怎么办?”,我突然一愣,怎么回事?然后找昨天晚上负责的运维和研发一打听,才知道是因为在 Online Kubernetes 上部署的 Pod 如果需要调用 RBAC 中 token,都不予发放密文,然后申请流程被卡住了。这才导致了小伙伴拿到不到对应的 Access Token 导致 Pod 中的 Informer 没有办法抓取资源导致应用上线失败。

本想这个是一个小问题,没一会我去开会的时候,我就被紧急叫到另外一个会议室。 刚进门就有人喊到:“老李,你来了,正好正好,xxxxxx”,果不其然还是那个事情。 经过一段时间的故事发展后,就出现了一个需求,落在我们这边。

技术组的小伙伴想:有没有办法让发布的应用 Pod 在通过 Access Token 访问 ApiServer 的时候,不让申请者接触到 Access Token 的内容。

心智负担

虽然 Access token 是访问 ApiServer 一个凭证,在已有的 Kubernetes RBAC 管理系统上就可以完成申请和使用。但是随着时间推移,以及日常使用中,Access token 已经被人滥用,而且在公司内部企微聊天群内,各种 Access token 满天飞。我想这个也是安全组小伙伴忍无可忍的原因吧,实际上 Access token 已经失去管理的意义。

总结眼前这个事情,问题主要如下:

  1. 如果这个 Token 泄露,将给使用这个 Token 的应用带来很多安全风险。
  2. Access token 这样的明文分发是接触式,安全组的小伙伴非常反对,希望我们能够提出一种无接触的方式。
  3. Access token 还有一套发放管理系统,以及其他的系统的 Token 文件导入到处。 系统过于繁杂,需要有人员管理和维护,以及数据存储等等问题。
  4. 每年公司技术安全评审会,Access token 的问题都是非常头痛,大量需要改造和提升的地方。

隐含的神经压力,以及使用流程上面临的很多挑战,都让人焦虑不已。如何解决这个问题?,我想最好的办法是:在应用创建和维护的时候提供一个入口,让使用者自己关联应用到已经创建的 Access token,不在走申请 Access token,导出,然后在发布工具中导入。 直接通过平台内部关联,直接使用。

既然这里说到是心智负担,但是真正负担在哪里? 实际上面已经提到了心智负担的核心内容:就是如何让使用者真正的无接触,将应用与已经创建的 Access token 关联。

有想法的小伙伴会说:“不就是后端服务打通下?有什么好说的?嘶嘶嘶。”,我想说,既然老李出马,就不会这么简单,一定有比这个更优雅的方案,请各位客官耐心往下看。

前置知识

经过一段时间的调研和方案讨论,我们实际明确知道这样做可以减少 Access Token 的浪费,以及提高 Access Token 的安全性,同时也可以简化日常 Access Token 申请与使用的流程复杂度(因为是无接触式的,必然导致安全审核方式以及发放方式比传统的接触式要少很多)。

在动之前还是要准备些知识,还要做好方案设计,这样才能做到:测底从底层解决问题,而不是单纯的从前端 web 换到了后端接口

分享下我理解的一个 Access Token 如何与一个 Deployment 最优雅关联的。

如何优雅的让 Pod 通过 ServiceAccount 访问 Kubernetes Apiserver

有的小伙伴看到这个图觉得有点眼熟,估计马上就想到了 Deployment 与 Configmaps、Secret 这类资源的 VolumeMounts 方式嘛? No!! No!! No!! 都说了“更优雅的方案”,是更有意思的方式。

不卖关子了,官方文档:kubernetes.io/docs/tasks/…

关键词:serviceAccountName

RBAC

无接触式的使用 Access Token 之前,还需要了解下 RBAC 的一些概念。

官方文档: kubernetes.io/docs/refere…

如果需要了解更多的中文相关内容,小伙伴可以自行 baidu 下,很多相关内容。 而这里主要是说 SA、Role、Binding 3 者之间的关系,并用大白话定义他们。

RBAC 用大白话解释:

  1. 我是谁 (Who am i) : 对应 ServiceAccount,表示了当前这个 Token 对应的身份是什么?
  2. 我能干嘛 (What can i do): 对应 ClusterRole/Role,对资源的权限控制,表示这个规则在 Kubernetes 中对指定资源拥有什么样权限或者控制策略。
  3. 我在哪里 (Where am i): 对应 ClusterRoleBinding/RoleBinding,将 Role 与 ServiceAccount 进行绑定,告诉 Token 在什么地方或者资源上生效。

最后在创建 RBAC 对应的 namespace 中产生一个 secret 的资源,而这个资源里面就是对应的 Access Token。

Pod 的 ServiceAccount

在 Kubernetes 运行环境中,我们随便 describe 一个 Pod 的信息,都会发现在 Mounts 字段中有一个 secrets/kubernetes.io/serviceaccount ,这个 ServiceAccount 是 Kubernetes 默认给 Pod 挂载的,方便 Pod 内部应用访问 Apiserver,但是这个 ServiceAccount 的权限太小了,导致什么事情都做不了。

Containers:
  application:
    Container ID:  docker://9e9c92065671dacd0b996e4e26bd6713f5f6d0f9e3d06fbce9c8f00b0b981ea0
    Mounts:
      /var/run/secrets/kubernetes.io/serviceaccount from kube-api-access-2znnm (rw)

既然要无接触式的 Access Token 与应用关联,是不是通过手动替换这个 secrets/kubernetes.io/serviceaccount 就可以实现想要的效果呢? 可以,Kubernetes 官方也建议这么使用

如何关联

创建了 RBAC 资源,如何将这个 Access Token 与一个 Deployment 资源关联在一起? 是不是还要把 Access Token 中 token 字段内容贴到 Deployment 内容中呢?不需要。看上面 serviceAccountName 的官方文档,文中有说到。

举个例子:

apiVersion: v1
kind: Deployment
metadata:
    name: my-app
spec:
    serviceAccountName: my-rbac # 这里将创建好的 rbac 的 SA 账号名称与 Deployment 关联,完全不需要输入任何 Token

啊!就这?? 我说了一大段,最后就这么一行? 唉,我说过了:更优雅的方案,就是这么点单,就说优不优雅。

解决思路

当然有了前面的思路和“优雅”方案,是不是 Pod 内的应用程序不要修改呢? 需要的。如果内部代码不修改的,下面底层做了再多的事情,还是没有效果。

那么我们需要怎么做才能让开发的代码使用 Pod 内部挂载好的 Access Token 呢? 说到这里,我们不得不看看 client-go 的代码。

k8s.io/client-go@v0.26.1/kubernetes/clientset.go

// NewForConfig creates a new Clientset for the given config.
// If config's RateLimiter is not set and QPS and Burst are acceptable,
// NewForConfig will generate a rate-limiter in configShallowCopy.
// NewForConfig is equivalent to NewForConfigAndClient(c, httpClient),
// where httpClient was generated with rest.HTTPClientFor(c).
func NewForConfig(c *rest.Config) (*Clientset, error) {
	configShallowCopy := *c

	if configShallowCopy.UserAgent == "" {
		configShallowCopy.UserAgent = rest.DefaultKubernetesUserAgent()
	}

	// share the transport between all clients
	httpClient, err := rest.HTTPClientFor(&configShallowCopy)
	if err != nil {
		return nil, err
	}

	return NewForConfigAndClient(&configShallowCopy, httpClient)
}

上面的代码就是我们创建一个 Kubernetes 客户端需要调用的函数,这个函数就一个入参:c *rest.Config。通过 rest.Config 来配置 Apiserver 和 Access Token 等信息。

我们继续往下追 rest.Config 看看源代码中是怎么定义的。

k8s.io/client-go@v0.26.1/rest/config.go

// Config holds the common attributes that can be passed to a Kubernetes client on
// initialization.
type Config struct {
	// Host must be a host string, a host:port pair, or a URL to the base of the apiserver.
	// If a URL is given then the (optional) Path of that URL represents a prefix that must
	// be appended to all request URIs used to access the apiserver. This allows a frontend
	// proxy to easily relocate all of the apiserver endpoints.
	Host string

	...

	// Server requires Bearer authentication. This client will not attempt to use
	// refresh tokens for an OAuth2 flow.
	// TODO: demonstrate an OAuth2 compatible client.
	BearerToken string `datapolicy:"token"`

	...
}

其中 HostBearerToken 这两个 String 就是定义 ApiServer 地址和 Access Token 的。 马上就有小伙伴会问:“我们配置好的 ServiceAccount 怎么与这两个值关联在一起?”。

不着急,在回答这个问题之前,我们要知道一个新的名词定义:InCluster

InCluster 表示在集群内部,也就是说让 client-go 在创建 Config 的时候使用 InCluster 模式。 我们继续看 InCluster 实现 InClusterConfig 代码是什么样的。

k8s.io/client-go@v0.26.1/rest/config.go

// InClusterConfig returns a config object which uses the service account
// kubernetes gives to pods. It's intended for clients that expect to be
// running inside a pod running on kubernetes. It will return ErrNotInCluster
// if called from a process not running in a kubernetes environment.
func InClusterConfig() (*Config, error) {
	const (
		tokenFile  = "/var/run/secrets/kubernetes.io/serviceaccount/token"
		rootCAFile = "/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"
	)
	host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT")
	if len(host) == 0 || len(port) == 0 {
		return nil, ErrNotInCluster
	}

	token, err := os.ReadFile(tokenFile)
	if err != nil {
		return nil, err
	}

	tlsClientConfig := TLSClientConfig{}

	if _, err := certutil.NewPool(rootCAFile); err != nil {
		klog.Errorf("Expected to load root CA config from %s, but got err: %v", rootCAFile, err)
	} else {
		tlsClientConfig.CAFile = rootCAFile
	}

	return &Config{
		// TODO: switch to using cluster DNS.
		Host:            "https://" + net.JoinHostPort(host, port),
		TLSClientConfig: tlsClientConfig,
		BearerToken:     string(token),
		BearerTokenFile: tokenFile,
	}, nil
}

看到代码中的 tokenFilerootCAFile 中定义位置了吧,就是我们通过 serviceAccountName 将自定义的 ServiceAccount 挂载到 Deployment 中,最后在 Pod 运行时,Access Token 挂载的位置。同时代码也会通过 host, port := os.Getenv("KUBERNETES_SERVICE_HOST"), os.Getenv("KUBERNETES_SERVICE_PORT") 获得 Apiserver 的 Ip 和 Port,最后拼成字符串传递给 Host

那我们要使用 InCluster 创建一个 Kubernetes 客户端怎么写代码呢?

举个例子:

package main

import (
	"context"
	"fmt"
	"time"

	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
	"k8s.io/client-go/kubernetes"
	"k8s.io/client-go/rest"
)

func main() {

	// creates the in-cluster config
	config, err := rest.InClusterConfig()
	if err != nil {
		fmt.Println(err)
	}

	// creates the clientset
	clientset, err := kubernetes.NewForConfig(config)
	if err != nil {
		fmt.Println(err)
	}

	for {
		pods, err := clientset.CoreV1().Pods("").List(context.TODO(), metav1.ListOptions{})
		if err != nil {
			fmt.Println(err)
		} else {
			fmt.Printf("There are %d pods in the cluster\n", len(pods.Items))
		}
		time.Sleep(10 * time.Second)
	}
}

是不是很简单,没有那么复杂。将代码编译打包成 Docker Image,然后在 Kubernetes 上部署下,查看日志就能看到结果了。

Console 输出:

# kubectl logs k8s-pod-test-699bd54dfd-g7qv8
There are 26 pods in the cluster
There are 26 pods in the cluster

写在最后

当这个技术方案最后被落地,并于内部系统完成融合,解决了“无接触式”的 Access Token 分发,而且整个过程没有太多的影响。 当然这个也只是众多方案的中的一种解决方案,因为我们这边应用后端开发语言比较纯粹,而且底层调用这块都有一个项目组在维护 SDK,而这部分代码最终合并到了 SDK 中,对整个研发日常开发代码没有任何影响。

经过一段时间方案试行,各方反馈都比较正面。

  • 研发:没有繁琐的 Access Token 申请过程,与应用各种绑定也变得非常方便了。
  • 运维:Access Token 自从“无接触式”后,很少有人来找,基本没有 Access Token 的问题。
  • 安全:现在没有人在公司企微里面到处传 Access Token ,之前的失控得到很好的控制。

最后还是比较欣慰的,一个小小使用流程上问题,最后引发一套工具体系的大改革,真的是:“表层的问题,都是内在矛盾积累后的爆发”。