我是 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 ,之前的失控得到很好的操控。

终究仍是比较欣慰的,一个小小运用流程上问题,终究引发一套工具系统的大改革,真的是:“表层的问题,都是内涵矛盾积累后的迸发”。