我是 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 现已失去办理的含义。
总结眼前这个工作,问题首要如下:
- 假如这个 Token 走漏,将给运用这个 Token 的运用带来许多安全危险。
- Access token 这样的明文分发是触摸式,安全组的小伙伴十分对立,希望咱们能够提出一种无触摸的方法。
- Access token 还有一套发放办理系统,以及其他的系统的 Token 文件导入处处。 系统过于冗杂,需求有人员办理和保护,以及数据存储等等问题。
- 每年公司技能安全评审会,Access token 的问题都是十分头痛,大量需求改造和提高的地方。
隐含的神经压力,以及运用流程上面临的许多应战,都让人焦虑不已。怎样处理这个问题?,我想最好的方法是:在运用创立和保护的时分提供一个进口,让运用者自己相关运用到现已创立的 Access token,不在走申请 Access token,导出,然后在发布工具中导入。 直接经过渠道内部相关,直接运用。
已然这儿提到是心智担负,可是真实担负在哪里? 实践上面现已提到了心智担负的核心内容:便是怎样让运用者真实的无触摸,将运用与现已创立的 Access token 相关。
有想法的小伙伴会说:“不便是后端服务打通下?有什么好说的?嘶嘶嘶。”,我想说,已然老李出马,就不会这么简单,必定有比这个更高雅的计划,请各位客官耐心往下看。
前置常识
经过一段时间的调研和计划讨论,咱们实践清晰知道这样做能够减少 Access Token 的糟蹋,以及提高 Access Token 的安全性,同时也能够简化日常 Access Token 申请与运用的流程杂乱度(由于是无触摸式的,必定导致安全审阅方法以及发放方法比传统的触摸式要少许多)。
在动之前仍是要准备些常识,还要做好计划设计,这样才干做到:测底从底层处理问题,而不是单纯的从前端 web 换到了后端接口
分享下我了解的一个 Access Token 怎样与一个 Deployment 最高雅相关的。
有的小伙伴看到这个图觉得有点眼熟,估计马上就想到了 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 用大白话解说:
- 我是谁 (Who am i) : 对应 ServiceAccount,表明了当前这个 Token 对应的身份是什么?
- 我能干嘛 (What can i do): 对应 ClusterRole/Role,对资源的权限操控,表明这个规则在 Kubernetes 中对指定资源拥有什么样权限或许操控策略。
- 我在哪里 (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"`
...
}
其中 Host 和 BearerToken 这两个 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
}
看到代码中的 tokenFile 和 rootCAFile 中界说位置了吧,便是咱们经过 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 ,之前的失控得到很好的操控。
终究仍是比较欣慰的,一个小小运用流程上问题,终究引发一套工具系统的大改革,真的是:“表层的问题,都是内涵矛盾积累后的迸发”。