如何优雅的让 Pod 通过 ServiceAccount 访问 K8s api-server

2023-02-24 00:00:00 代码 方案 关联 接触 小伙伴

本文转自掘金 LEE 的博客,原文:https://juejin.cn/post/7195842444302123069,版权归原作者所有。

事件背景

某天早上刚到公司工位上,正准备开会。被一个业务组项目负责人抓住了,然后着急的说到:“老李啊,跟你说。昨天晚上准备要上线的  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 优雅关联的。

结构图

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

不卖关子了,官方文档:Opt out of API credential automounting[1]

关键词:serviceAccountName

RBAC

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

官方文档: Using RBAC Authorization[2]

如果需要了解更多的中文相关内容,小伙伴可以自行 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 stringa 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) ==  || len(port) ==  {
  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 ,之前的失控得到很好的控制。

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

引用链接

[1]

Opt out of API credential automounting: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/#opt-out-of-api-credential-automounting

[2]

Using RBAC Authorization: https://kubernetes.io/docs/reference/access-authn-authz/rbac/







相关文章