(译)Kubernetes 中的用户和工作负载身份

2022-11-23 14:38:30 浏览数 (1)

本文中我们会试着解释,在 Kubernetes API Server 上如何对用户和工作负载进行认证的问题。

Kubernetes API Server 开放了 HTTP API 接口,让最终用户、集群组件以及外部组件可以进行通信。

绝大多数操作都可以用 kubectl 来完成,而且也可以使用 REST 调用的方式直接访问 API。

但是如何只允许认证用户访问 API 呢?

使用 curl 访问 Kubernetes API

让我们从调用 Kubernetes API 开始。

要列出集群中的所有命名空间,可以执行下列命令:

代码语言:javascript复制
$ export API_SERVER_URL=https://10.5.5.5:6443

$ curl $API_SERVER_URL/api/v1/namespaces
curl: (60) Peer Certificate issuer is not recognized.
# truncated output
If you'd like to turn off curl's verification of the certificate, use the -k (or --insecure) option.

输出内容表明,API Server 的接口用一个未识别的证书(例如自签发)提供了 https 服务,所以 curl 中断了这个请求。

接下来我们用 -k 参数跳过证书验证过程,并观察产生的响应:

代码语言:javascript复制
# curl -k $API_SERVER_URL/api/v1/namespaces
{
  "kind": "Status",
  "apiVersion": "v1",
  "status": "Failure",
  "message": "namespaces is forbidden: User "system:anonymous" cannot list resource "namespaces" ...",
  "reason": "Forbidden",
  "details": { "kind": "namespaces" },
  "code": 403
}

现在我们拿到了响应,但是:

  1. 对 API 端点的访问被禁止了(返回码 403
  2. 用户身份被识别为 system:anonymous,这个用户无权列出命名空间

上面的操作揭示了 kube-apiserver 的部分工作机制:

  1. 首先识别请求用户的身份
  2. 然后决策这个用户是否有权完成操作

正式一点的说法分别叫认证(也叫 AuthN)和鉴权(也叫 AuthZ):

  1. 发起 curl 请求时,流量触达 Kubernetes API Server
  2. 在 API Server 里,认证模块会首先收到请求。 如果认证失败,请求就会被标识为 anonymous
  3. 认证之后就进入鉴权环节、 匿名访问没有权限,所以鉴权组件拒绝请求,并返回 403

再次检视刚才的 curl 请求:

  1. 因为没有提供用户凭据,Kubernetes 认证模块会给请求标记为匿名请求
  2. 根据 Kubernetes API Server 配置,可能会收到一个 401 Unauthorized 代码
  3. Kubernetes 鉴权模块会检查 system:anonymous 是否具有列出命名空间的权限,如果没有,就返回 403 Forbidden 错误信息

例如 Kubelet 需要连接到 Kubernetes API 来报告状态:

调用请求可能使用 Token、证书或者外部管理的认证来提供身份。认证模块是整个系统的第一个门槛。

Kubernetes 的认证模块提供的几个重点能力:

  1. 同时支持人和非人用户
  2. 同时支持内部用户(Kubernetes 负责创建和管理的账号)和外部用户(例如集群外部署的应用)
  3. 支持标准的认证策略,例如静态 Token、Bearer Token、X509 认证、OIDC 等
  4. 同时支持多种认证策略
  5. 可以加入或者移除认证策略
  6. 还可以授权匿名用户访问 API

下面我们会走进观察认证模块的工作过程。

本文聚焦于认证领域。要了解更多鉴权内容,可以阅读 Limiting access to Kubernetes resources with RBAC 一文。

Kubernetes API 的内外部用户区别

Kubernetes API 支持两种 API 用户:内部和外部。

这两个东西有什么不同呢?

如果用户是集群的内部用户,我们需要给它定义一个规范(例如数据模型);而外部用户的规范是已经存在的。所以我们将用户分成下面几类:

  1. Kubernetes 管理的用户: Kubernetes 创建,并由集群内应用使用的用户账号。
  2. 非 Kubernetes 管理用户: 在 Kubernetes 集群外的用户,例如:
    • 集群管理员发放的静态 Token 或证书
    • 使用 Keystone、Google Account 以及 LDAP 等进行认证的用户

授权外部用户访问集群

假设有如下场景:使用 Bearer token 访问 Kubernetes。

代码语言:javascript复制
curl --cacert ${CACERT} 
  --header "Authorization: Bearer <my token>" 
  -X GET ${APISERVER}/api

Kubernetes API Server 是如何将 Token 识别为身份的?

Kubernetes 并不管理外部用户,所以应该有一种机制来从外部资源中获取信息(例如用户名和用户组)。

换句话说,Kubernetes API 接到了带有 Token 的请求后,就应该能够提取信息并进行后续的决策了。

下面用例子来解释一下这个场景。

创建一个 CSV 文件,其中包含了用户、Token 和用户组:

代码语言:javascript复制
token1,arthur,1,"admin,dev,qa"
token2,daniele,2,dev
token3,errge,3,qa

文件格式为 token, user, uid, groups

--token-auth-file 参数启动一个 minikube 集群:

代码语言:javascript复制
$ mkdir -p ~/.minikube/files/etc/ca-certificates
$ cd ~/.minikube/files/etc/ca-certificates
$ cat << | tokens.csv
token1,arthur,1,"admin,dev,qa"
token2,daniele,2,dev
token3,errge,3,qa
EOF
$ minikube start 
  --extra-config=apiserver.token-auth-file=/etc/ca-certificates/tokens.csv

为了发送请求给 Kubernetes API,需要集群的 IP 地址以及证书:

代码语言:javascript复制
kubectl config view
apiVersion: v1
clusters:
- cluster:
    certificate-authority: /Users/learnk8s/.minikube/ca.crt
    extensions:
    - extension:
        last-update: Fri, 10 Jun 2022 12:21:45  08
        provider: minikube.sigs.k8s.io
        version: v1.25.2
      name: cluster_info
    server: https://127.0.0.1:57761
  name: minikube
# truncated output

接下来向集群发送一个请求:

代码语言:javascript复制
$ export APISERVER=https://127.0.0.1:57761
$ export CACERT=/Users/learnk8s/.minikube/ca.crt
$ curl --cacert ${CACERT} -X GET ${APISERVER}/api
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "forbidden: User "system:anonymous" cannot get path "/"",
  "reason": "Forbidden",
  "details": {},
  "code": 403
}

响应信息表明,我们用匿名身份访问了 API,并且没有任何权限。

接下来用 token1(来自于 tokens.csv 文件中的用户 arthur)发起请求:

代码语言:javascript复制
$ export APISERVER=https://127.0.0.1:57761
$ export CACERT=/Users/learnk8s/.minikube/ca.crt
$ curl --cacert ${CACERT} --header "Authorization: Bearer token1" -X GET ${APISERVER}/api
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "forbidden: User "arthur" cannot get path "/"",
  "reason": "Forbidden",
  "details": {},
  "code": 403
}

如上所见,Kubernetes 能够识别出请求来自于 Arthur。发生了什么呢?tokens.csv--token-auth-file 参数起了什么作用?Kubernetes 有多种认证插件,现在我们使用的是静态 Token 文件

重放一下刚才的过程:

  1. API Server 启动后,读取 CSV 文件,把用户数据保存在内存里
  2. 用 Token 向 API Server 发起请求
  3. API Server 用 Token 找到匹配的用户,并解出剩余的用户信息(例如用户、用户组等)
  4. 这些详细信息会被包含在请求中,传递给鉴权模块
  5. 当前的鉴权模块(例如 RBAC)找不到 Arthur 的权限,拒绝请求。

创建一个 ClusterRoleBinding 就能快速修复这个问题:

代码语言:javascript复制
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin
subjects:
- kind: User
  name: arthur
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

用下面的命令把对象提交给集群:

代码语言:javascript复制
$ kubectl apply -f admin-binding.yaml
clusterrolebinding.rbac.authorization.k8s.io/admin created

再次执行命令就会成功了:

代码语言:javascript复制
curl --cacert ${CACERT} 
  --header "Authorization: Bearer token1" 
  -X GET ${APISERVER}/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.49.2:8443"
    }
  ]
}

上面向 kube-apiserver 发送了一个 HTTP 请求,认证模块会尝试将如下属性附加到请求之中:

  • Username: 字符串,例如 kube-adminjane@example.com
  • UID: 字符串,相对用户名来说,UID 是一个更稳定的属性
  • Groups: 例如 system:mastersdevops-team
  • 附加字段: 可能对认证过程有帮助的一些其他字段

请求上下文中加入这些信息之后,后续的 Kubernetes API 组件都能读取这些信息,这些信息对认证插件来说是透明的。

  1. 可以使用 Token 向集群发起一个认证请求
  2. Kubernetes 把请求 Token 进行匹配。 这是一个外部用户,因此需要依赖一个外部的用户管理系统(这里指的就是那个 CSV 文件)
  3. 拿到用户名、ID、用户组等信息之后,这些信息会被传递给鉴权模块进行校验

前面的例子中为用户名创建了一个 ClusterRoleBinding。其实 CSV 中为 Arthur 设置了三个用户组(admindevqa),因此也可以写成:

代码语言:javascript复制
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: admin
subjects:
- kind: Group
  name: admin
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: rbac.authorization.k8s.io

静态 Token 是一种简易的认证机制,集群管理员可以随意生成 Token 并指派给用户。但是这种方式有一定弊端:

  1. 必须知道所有的用户
  2. 编辑 tokens.csv 文件需要重启 API Server
  3. Token 不会过期

Kubernetes 还提供了其它几种外部认证机制:

  • X.509 客户端证书
  • OpenID
  • 认证代理
  • Webhook

每种方式都有各自的利弊,但是所有的工作流都跟静态 Token 类似:

  • 身份被保存在集群之外
  • 用户使用 Token 向 API Server 发起请求
  • Kubernetes 向外部认证源(例如 CSV 文件、认证服务、LDAP 等)请求检查 Token 的有效性
  • 如果认证有效,Kubernetes 会拿到用户名和其他元数据
  • 鉴权策略会使用这些数据来判断用户是否具备访问该资源的权限

那么如何选择认证插件呢?实际上可以同时启用多个认证插件,Kubernetes 会逐个调用每个插件,直到成功为止。

如果所有插件都没能成功,则请求会被标记为未认证或者是匿名访问。

  1. 认证不只是一个组件,而是由多个组件协同完成的
  2. 收到请求之后,插件会顺序执行,如果所有插件都失败了,请求就会被拒绝
  3. 如果成功,请求会被传递给鉴权模块

现在已经了解了外部用户的问题,接下来看看 Kubernetes 如何管理内部用户。

用 ServiceAccount 管理 Kubernetes 内部认证

在 Kubernetes 中,内部用户使用 Service Account 的概念来表达。

这些身份通过 kube-apiserver 创建,并分配给应用。

Service Account 会有相关联的 Token,应用向 kube-apiserver 发起请求时,会共享这个 Token 用于认证。

观察一下 Service Account 的定义:

代码语言:javascript复制
$ kubectl create serviceaccount test
serviceaccount/test created

这个资源的具体内容:

代码语言:javascript复制
$ kubectl get serviceaccount test -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test
secrets:
- name: test-token-6tmx7

如果集群版本高于 1.24,输出会有不同:

代码语言:javascript复制
$ kubectl get serviceaccount test -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
  name: test

差距很明显,只有老版本集群中会有 secrets 字段。

这个 Secret 包含了必要的 Token,API Server 可以用 Token 对请求进行认证:

代码语言:javascript复制
$ kubectl get secret test-token-6tmx7
apiVersion: v1
kind: Secret
metadata:
  name: test-token-6tmx7
type: kubernetes.io/service-account-token
data:
  ca.crt: LS0tLS1CR…
  namespace: ZGVmYXVs…
  token: ZXlKaGJHY2…

下面的 YAML 代码把这个身份分配给 Pod:

代码语言:javascript复制
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  serviceAccount: test
  containers:
  - image: nginx
    name: nginx

提交到集群,创建 Pod 并进入他的 Bash:

代码语言:javascript复制
$ kubectl apply -f nginx.yaml
pod/nginx created
$ kubectl exec -ti nginx -- bash

发起请求:

代码语言:javascript复制
$ export APISERVER=https://kubernetes.default.svc
$ export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
$ export CACERT=${SERVICEACCOUNT}/ca.crt
$ export TOKEN="token here"
$ curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.49.2:8443"
    }
  ]
}

调用成功了。

Kubernetes 1.24 以后的版本不再创建 Secret,那怎么获取 Token 呢?

为 Service Account 生成临时认证

新版本的 Kubernetes 中,Kubelet 负责从 API Server 申请临时 Token。

Token 格式类似 Secret 对象中的 Token,但是有个很大的不同是——他会过期。

这个 Token 不会被注入到 Secret 里面,而是使用 Projected Volume。

在 Kubernetes 1.24 中重复一下刚才的测试。

代码语言:javascript复制
$ kubectl create serviceaccount test
serviceaccount/test created

创建一个 Pod:

代码语言:javascript复制
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  serviceAccount: test
  containers:
  - name: nginx
    image: nginx

把 Pod 提交到集群上:

代码语言:javascript复制
$ kubectl apply -f nginx.yaml
pod/nginx created

首先确认一下,集群里没有 Secret:

代码语言:javascript复制
$ kubectl get secrets
No resources found in default namespace.

然后进入 Pod Shell:

代码语言:javascript复制
$ kubectl exec -ti nginx -- bash

检查一下 Token 的加载情况:

代码语言:javascript复制
$ export APISERVER=https://kubernetes.default.svc
$ export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
$ export CACERT=${SERVICEACCOUNT}/ca.crt
$  export TOKEN=$(cat ${SERVICEACCOUNT}/token)
$ curl --cacert ${CACERT} --header "Authorization: Bearer ${TOKEN}" -X GET ${APISERVER}/api
{
  "kind": "APIVersions",
  "versions": [
    "v1"
  ],
  "serverAddressByClientCIDRs": [
    {
      "clientCIDR": "0.0.0.0/0",
      "serverAddress": "192.168.49.2:8443"
    }
  ]
}

还是能成功,这个 Token 是怎么加载的?我们来看一下 Pod 的定义:

代码语言:javascript复制
$ kubectl get pod nginx -o yaml
apiVersion: v1
kind: Pod
  name: nginx
spec:
  containers:
  - image: nginx
    volumeMounts:
    - mountPath: /var/run/secrets/kubernetes.io/serviceaccount
      name: kube-api-access-69mqr
      readOnly: true
  serviceAccount: test
  volumes:
  - name: kube-api-access-69mqr
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          expirationSeconds: 3607
          path: token
      - configMap:
          items:
          - key: ca.crt
            path: ca.crt
          name: kube-root-ca.crt
      - downwardAPI:
          items:
          - fieldRef:
              apiVersion: v1
              fieldPath: metadata.namespace
            path: namespace

内容有点多,解析一下。

  1. 这里声明了一个 kube-api-access-69mqr
  2. 这个卷用只读的方式加载到了 /var/run/secrets/kubernetes.io/serviceaccount

这个卷用的是 projected 类型。

Projected 卷能把多个卷聚合在一起。但并不是所有类型的卷都能够绑定到 Projected 卷里面,目前仅限于 downwardAPIconfigMap 以及 serviceAccountToken

在这个例子里,Projected 卷的组成成分包括:

  1. serviceAccountToken 卷被加载到 token 路径
  2. configMap
  3. downwardAPI 卷被加载到 namespace 路径

这些卷都是干嘛的?

serviceAccountToken 是一种特别的卷,从当前的 Service Account 中加载 Secret,并填充到 /var/run/secrets/kubernetes.io/serviceaccount/token 文件中。

ConfigMap 卷会把 ConfigMap 中的每个 Key 加载成目录里面的文件。

这个文件的的内容就是对应 Key 的 Value(如果键值对的内容是 replicas:1,就会表达为一个命名为 replicas 的文件,其内容是 1)。

本例中,ConfigMap 卷中加载了调用 API 所必须的 ca.crt 证书。

downwardAPI 卷是一种特殊类型,使用 downwardAPI,将 Pod 信息开放给容器。

在这个例子里,用这种方法将当前命名空间用文件的方式暴露给容器。

可以在 Pod 里验证一下这个能力:

代码语言:javascript复制
$ export SERVICEACCOUNT=/var/run/secrets/kubernetes.io/serviceaccount
$ export NAMESPACE=$(cat ${SERVICEACCOUNT}/namespace)
$ echo $NAMESPACE
default

知道了 Token 的加载方式之后,那为什么 Kubernetes 要放弃 Secret 改用这种方式呢?

主要原因是:

  1. Secret 中的 Token 永不过期
  2. 创建 Service Account 的时候,会异步创建一个带令牌的 Secret

但是如果你只需要 Token,却不需要 Pod 呢?是否可以不加载 Projected Volume 就拿到 Token 数据呢?kubectl 有个新命令:

代码语言:javascript复制
$ kubectl create token test
eyJhbGciOiJSUzI1NiIsImtpZCI6ImctMHJNO…

这个 Token 是临时的,和 Kubelet 加载到 Pod 里面的 Token 是一样的。

重复执行命令会看到不同的结果,那么这个 Token 只是个长字符串吗?

Projected Servivce Account Token 是个签了名的 JWT Token

可以把这个字符串复制到 jwt.io 网站上,处理之后的输出内容结构如下:

  1. Header 描述了 Token 的签名方式
  2. Payload 就是 Token 中的真实数据
  3. Signature 用于校验 Token 是否被修改

观察一下这个 Token:

代码语言:javascript复制
{
  "aud": [    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1655083796,
  "iat": 1655080196,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "kubernetes.io": {
    "namespace": "default",
    "serviceaccount": {
      "name": "test",
      "uid": "6af2abe9-d8d8-4b8a-9bb5-3cc96442b322"
    }  },
  "nbf": 1655080196,
  "sub": "system:serviceaccount:default:test"}

上面的字段值得讨论:

  • sub: 主体。 本例中的主体是存在于缺省命名空间中的名为 test 的 Service Account。
  • aud: 受众。 这个 Token 对当前 Kubernetes 集群生效。
  • iss: 签发者。 因为这个 Token 是当前 Kubernetes 签发的,所以取值为当前集群的域名。
  • kubernetes.io: 自定义字段,用于描述 Kubernetes 的细节。

从 Nginx Pod 中读取 Token:

代码语言:javascript复制
{
  "aud": [    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1686617744,
  "iat": 1655081744,
  "iss": "https://kubernetes.default.svc.cluster.local",
  "kubernetes.io": {
    "namespace": "default",
    "pod": {
      "name": "nginx",
      "uid": "a11defcb-f510-4d49-9c4f-2e8e8da1c33c"
    },
    "serviceaccount": {
      "name": "test",
      "uid": "6af2abe9-d8d8-4b8a-9bb5-3cc96442b322"
    },
    "warnafter": 1655085351
  },
  "nbf": 1655081744,
  "sub": "system:serviceaccount:default:test"}

Payload 中包含了 Pod 的名字和 UUID。但是这些信息是谁在消费呢?

不仅能够检查 Token 的完整性和有效性,甚至还可以区分出同一个 Deployment 中的两个 Pod 的区别。

这个功能很有用,原因是:

  • 授权粒度精细到特定 Pod
  • 特定身份被攻破,也只会影响单一单元
  • 从一个 API 调用就能够知道其中包含的命名空间和 Pod

AWS 如何将 IaM 集成到 Kubernetes

设想一个场景,在 AWS 中运行 Kubernetes 集群之中,并希望从集群中上传文件到 S3 的场景。

注意在 Azure 和 GCP 也存在同等能力。

通常来说,需要用一个角色来完成这一任务,但是 AWS 的 IAM 角色只能赋予给计算实例、而非 Pod,换句话说,AWS 对 Pod 并无认知。

2019 年底,AWS 提供了一种原生的 Kubernetes 集成 IAM 的机制,被称为 IAM Roles for Service Accounts (IRSA),IRSA 在身份和 Projected Service Account Token 之间建立了联系。

  1. 创建一个 IAM 策略,其中包含了允许访问的资源
  2. 创建一个角色,其中包含了上一步中的策略,记录其 ARN
  3. 创建一个 Projected Service Account Token,并用文件的方式进行加载

把 Role ARN 和 Projected Service Account Token 呈现在 Pod 的环境变量之中:

代码语言:javascript复制
apiVersion: apps/v1
kind: Pod
metadata:
  name: myapp
spec:
  serviceAccountName: my-serviceaccount
  containers:
  - name: myapp
    image: myapp:1.2
    env:
    - name: AWS_ROLE_ARN
      value: arn:aws:iam::111122223333:policy/my-role
    - name: AWS_WEB_IDENTITY_TOKEN_FILE
      value: /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    volumeMounts:
    - mountPath: /var/run/secrets/eks.amazonaws.com/serviceaccount
      name: aws-iam-token
      readOnly: true
  volumes:
  - name: aws-iam-token
    projected:
      defaultMode: 420
      sources:
      - serviceAccountToken:
          audience: sts.amazonaws.com
          expirationSeconds: 86400
          path: token

有了这一配置,就能向 S3 上传文件了。

应用会使用这两个环境变量作为连接到 S3 所需要的 Token,但是如何实现的呢?

是 Kubernetes 而非 AWS 生成了 Token,那么 AWS 如何知道 Token 的有效性呢——是的 AWS 不知道。

AWS SDK 使用角色 ARN 以及 Projected Service Account Token 来交换标准的 AWS 访问凭据。

如果不用 AWS SDK 又怎么办呢?应用程序向 AWS IAM 发起请求,为当前身份(Service Account)换取一个角色。

IAM 收到这个 Token 后,会进行解压并检查 iss 字段,来判断 JWT Token 的合法性。

这个字段通常会被配置为用于创建该 Token 的公钥。

前面说过,这个 URL 指向 Kubernetes 集群:

代码语言:javascript复制
{
  "aud": [
    "https://kubernetes.default.svc.cluster.local"
  ],
  "exp": 1686617744,
  "iat": 1655081744,
  "iss": "https://kubernetes.default.svc.cluster.local",

注意,需要把这个 URL 改成一个完全限定名(FQDN),否则 AWS IAM 无法触达。可以用 --service-account-issuer 参数来指定

这个 URL 是一个标准的 OIDC Provider,AWS IAM 会查看两个路径:

  • {Issuer URL}/.well-known/openid-configuration: 又被称为 OIDC 发现文档。 其中包含了签发者的配置元数据
  • {Issuer URL}/openid/v1/jwks: 其中包含了签名公钥,用于验证 Service Account Token 的真实性

要注意,缺省情况下,这两个端点是不会暴露的,需要集群管理员进行设计。

首先看看 JWKS 端点:

代码语言:javascript复制
curl {Issuer URL}/openid/v1/jwks
  "keys": [
    {
      "use": "sig",
      "kty": "RSA",
      "kid": "ZO4TUgVjBzMWKVP8mmBwKLvsuyn8z-gfqUp27q9lO4w",
      "alg": "RS256",
      "n": "34a81xuMe…",
      "e": "AQAB"
    }
  ]
}

AWS IAM 会收到公钥,并校验 Token。下面的代码用于校验:

代码语言:javascript复制
var jwt = require('jsonwebtoken')var jwkToPem = require('jwk-to-pem')var pem = jwkToPem(jwk /* "kid" value from the jkws file */)
jwt.verify(token /* this is the token to verify */, pem, { algorithms: ['RS256'] }, function(err, decodedToken) {  // rest of the code})

如果 Token 有效,就生成一个具备指定权限的 Access Token:

代码语言:javascript复制
{
    "Credentials": {
        "AccessKeyId": "ASIAWY4CVPOBS4OIBWNL",
        "SecretAccessKey": "02n52u8Smc76…",
        "SessionToken": "IQoJb3JpZ…",
        "Expiration": "2022-06-13T10:50:25 00:00"
    },
    "SubjectFromWebIdentityToken": "system:serviceaccount:default:test",
    "AssumedRoleUser": {
        "AssumedRoleId": "AROAWY4CVPOBXUSBA5C2B:test",
        "Arn": "arn:aws:sts::[aws account id]:assumed-role/oidc/test"
    },
    "Provider": "arn:aws:iam::[aws account id]:oidc-provider/[bucket name].s3.amazonaws.com",
    "Audience": "test"}

拿到新凭据后,就可以用来访问 S3 存储桶了。

  1. Projected Serivce Account Token 代表一个集群内有效的身份它可以用来交换到一个其他场景下有效的 Token
  2. AWS IaM 服务收到这个 Token,并读取其 iss 字段的内容,用于验证 Token
  3. 如果身份有效,就签发自己的 Token
  4. 可以使用新的 Token 访问 AWS 的服务

另外还有一篇文章,完整的描述了手工进行集成的过程。

这种方式可以用于访问外部资源,然而访问内部服务时,是否也需要这样操作呢?

使用 Token Review API 校验 Projected Service Account

可以用 Token Review API 来对集群创建的 Token 进行校验。

首先为 Service Account 创建一个 Token:

代码语言:javascript复制
$ kubectl create token test
eyJhbG…

创建 YAML 资源,并在其中包含 Token:

代码语言:javascript复制
kind: TokenReview
apiVersion: authentication.k8s.io/v1
metadata:
  name: test
spec:
  token: eyJhbG… # <- token

提交资源,注意 -o yaml 输出的内容:

代码语言:javascript复制
$ kubectl apply -o yaml -f token.yaml
apiVersion: authentication.k8s.io/v1
kind: TokenReview
metadata:
  name: test
spec:
  token: eyJhbG…
status:
  audiences:
    - https://kubernetes.default.svc.cluster.local
  authenticated: true
  user:
    groups:
      - system:serviceaccounts
      - system:serviceaccounts:default
      - system:authenticated
    uid: eccac137-25e2-4e84-9d83-18b2f9c5e5af
    username: system:serviceaccount:default:test

Token Review API 的工作内容和 AWS IAM 集成类似:校验身份,并从 Token 中获取细节。当然,单一的 API 调用比 OIDC 流程要简单直接得多。

还可以使用定制 Audience 的方式来限制访问范围。

用 Kubernetes 1.24 或者更高版本生成 Service Account 的 Secret

从 1.24 开始,Kubernetes 不再为 ServiceAccount 自动生成 Secret。然而你还是可以使用传统的方式来创建 Service Account 并用注解的方式来附加给一个 Secret。

例如当前的 Service Account test 中没有 secret 对象。但是可以创建用这种方式创建 Secret (和 token):

代码语言:javascript复制
apiVersion: v1
kind: Secret
type: kubernetes.io/service-account-token
metadata:
  name: test
  annotations:
    kubernetes.io/service-account.name: "test"

提交给集群之后,进行观察:

代码语言:javascript复制
$ kubectl describe secret test

Name:         test
Namespace:    default

Type:  kubernetes.io/service-account-token

Data
====
ca.crt:     1111 bytes
namespace:  7 bytes
token:      eyJhbG…

还可以用 Token Review API 来校验这个 Token:

代码语言:javascript复制
kind: TokenReview
apiVersion: authentication.k8s.io/v1
metadata:
  name: test
spec:
  token: eyJhbG…

提交对象,并加入 -o yaml 开关:

代码语言:javascript复制
$ kubectl apply -o yaml -f token.yaml

apiVersion: authentication.k8s.io/v1
kind: TokenReview
metadata:
  name: test
spec:
  token: eyJhbG…
status:
  audiences:
  - https://kubernetes.default.svc.cluster.local
  authenticated: true
  user:
    groups:
    - system:serviceaccounts
    - system:serviceaccounts:default
    - system:authenticated
    uid: eccac137-25e2-4e84-9d83-18b2f9c5e5af
    username: system:serviceaccount:default:test

如果把 Token 内容提交给 jwt.io,会发现 Token 没有过期时间:

代码语言:javascript复制
{
  "iss": "kubernetes/serviceaccount",
  "kubernetes.io/serviceaccount/namespace": "default",
  "kubernetes.io/serviceaccount/secret.name": "test",
  "kubernetes.io/serviceaccount/service-account.name": "test",
  "kubernetes.io/serviceaccount/service-account.uid": "eccac137-25e2-4e84-9d83-18b2f9c5e5af",
  "sub": "system:serviceaccount:default:test"}

这种情况和 Kubernetes 的传统行为是一致的。

认证插件的选择

Kubernetes 提供了以下的认证插件:

  • 静态 Token 文件
  • X.509 证书
  • Open ID Connect
  • Authentication proxy
  • Webhook

如何选择呢?

在前面一节里,我们讨论了静态 Token 文件的限制:

  • 需要知道用户名
  • 修改 CSV 文件需要重启 API Server 才能生效
  • Token 不会过期

因此静态 Token 文件不是生产环境中的最佳选择。

X.509 客户端证书方案会略微好一些。

使用 X.509 客户端证书认证:

  1. kube-apiserver 使用 --client-ca-file=FILE 参数来指定 CA
  2. 管理员为外部用户签发客户端证书。 这些 X.509 客户端证书是自包含的,其中包含了用户名和用户组
  3. 用户使用这个证书,用 TLS 方式发起对 API Server 的访问
  4. kube-apiserver 用 CA 证书对客户端证书进行认证,如果有效,则解析其中包含的用户名和用户组。

工作流和静态 Token 类似,但还是有些区别:

  • 证书可以设置有效期
  • 创建新的客户端证书,无需修改 API Server 参数
  • 没有 CSV 文件,证书用 CRD 定义的方式来管理

然而,X.509 客户端证书也并不是一个值得推荐的方案。

  1. X.509 客户端证书通常是很长寿(以年计)
  2. CA 基础设施提供了作废证书的途径,但是 Kubernetes 不支持过期证书的检查
  3. 客户端证书是自包含的,因此用 RBAC 进行分组非常难
  4. 为了对客户进行认证,必须点对点的连接 API Server,不能使用反向代理或者 WAF 防火墙。

(临时)没有其它机制可用的应急场景下,正适合使用 X.509 认证方法。

Kubeadm 和 OpenShift 缺省会设置 API Server 的证书认证能力,这样本地的 Kubectl 就可以使用了。

除了上面的特例之外,可能最好的方式就是 OIDC 认证了。如果已经有了用于管理用户的 OpenID Connect 的基础设施,那就尤其合适了。这种情况下,可以用管理普通用户的方式来管理 Kubernetes 中的用户。

OpenID Connect Provider 能够签发 JSON Web Token(JWT),这意味着 Token 能够自动认证,无需连接到 Token 的签发方,并且会过期。

最后两种认证插件是:

  1. 认证代理
  2. Webhook

认证代理 插件能够通过外部的认证代理进行透明的认证。

当用户向 Kubernetes 集群发起请求时,请求首先会被认证代理进行处理。这种认证插件可以编写自己的认证逻辑,因此用来实现其它插件不支持的认证方式是很合适的。

最后 Webhook Token 认证插件让用户能够用 HTTP Bearer Token 的方式,对 Kubernetes 请求进行自定义认证逻辑。

Webhook Token 认证插件也同样适用于没有其它机制可用的场景。

总结

本文中阐述了 Kubernetes API Server 认证用户的能力。内容大致包括

  1. 外部用户和内部用户的区别
  2. Kubernetes API Server 如何实现不同的用户认证方法,例如静态 Token、Bearer Token、X.509 证书、OIDC 等
  3. Kubernetes 如何使用 Service Account 为内部用户授予身份
  4. 使用 Secret 创建的 Token,和 Kubelet 创建的 Token 有什么区别
  5. Projected Volume 把多个卷聚合到一起的方法
  6. 如何用 JWT 工具查看 Service Account Token
  7. 和 OIDC 联邦,并且和 AWS 之类的云供应商进行集成的方式
  8. 如何使用 API Review API 来校验 Service Account Token 的有效性。 认证通过后,就进入鉴权环节了。 然后可以阅读 Authentication between microservices using Kubernetes identities 来里了解相关内容。

0 人点赞