在 Kubernetes Pod 中运行的应用使用 ServiceAccount 令牌对 Kubernetes API 进行身份验证。这些 JWT 令牌作为文件挂载到容器中。JWT 令牌由 Kubernetes 集群的私钥签名,并且只能使用 TokenReview API 进行验证。
Service Account Issuer Discovery 通过群集(身份提供方)根据 OIDC Discovery Spec 将 Kubernetes 服务帐户令牌与外部系统(依赖方)联合。
使用 OIDC issuer discovery 通过外部系统验证Kubernetes应用程序
Service Account Issuer Discovery 这一功能使得用户能够用联邦的方式结合使用 Kubernetes 集群(Identity Provider,标识提供者)与外部系统(relying parties, 依赖方)所分发的服务账号令牌。
Kubernetes Service Account 可以使用令牌 JWT 对 Kubernetes API 进行身份验证,但是Kubernetes API 是目前(version 1.21 以下版本)唯一能够验证这些令牌的服务。
由于 Kubernetes API 服务器不能从公共网络访问,一些工作负载必须使用独立的系统进行身份验证。比如,跨集群身份验证。
Service Account Issuer Discovery 增强的目的是提高 Kubernetes service account token的实用性,允许集群之外的服务用作身份验证方法,而无需重载 Kubernetes API 服务器。为实现这点,Kubernetes API 服务器提供了一个 OpenID Connect(OIDC)发现文档
// ,其中包含令牌公钥和其他数据。验证者可以使用这些密钥来验证 KSA 令牌。
启用此功能需要投影 Service Account Tokens,需要为 kube-apiserver 设置以下命令行参数
# 如果 k8s 低于 1.21,需要设置 ServiceAccountIssuerDiscovery=true--feature-gates=ServiceAccountIssuerDiscovery=true--service-account-issuer=https://localhost:6443# 验证ServiceAccount Token的私钥或公钥文件--service-account-key-file=/etc/kubernetes/pki/sa.pub# 含有当前service account token issuer私钥的文件路径--service-account-signing-key-file=/etc/kubernetes/pki/sa.key--api-audiences=vault
创建一个示例应用程序,并且挂载一个投影的 ServiceAccountToken
apiVersion: v1
kind: Pod
metadata:
name: nginx
spec:
serviceAccountName: default
containers:
- image: nginx
name: oidc
volumeMounts:
- mountPath: /var/run/secrets/tokens
name: oidc-token
volumes:
- name: oidc-token
projected:
sources:
- serviceAccountToken:
path: oidc-token
expirationSeconds: 7200
audience: vault
Projected SA JWT Token 已经装载到 /var/run/secrets/tokens/oidc-token
# 查看 SA JWT Token
kubectl exec nginx -- cat /var/run/secrets/tokens/oidc-token
# 解析 SA JWT Token
kubectl exec nginx -- cat /var/run/secrets/tokens/oidc-token | step crypto jwt inspect --insecure
{
"header": {
"alg": "RS256",
"kid": "tqsnK8Han3Jd2-DU2MmP-hOwKvFLTdGPDIVBK6iXxxs"
},
"payload": {
"aud": [
"vault"
],
"exp": 1636243691,
"iat": 1636236491,
"iss": "https://localhost:6443",
"kubernetes.io": {
"namespace": "default",
"pod": {
"name": "nginx",
"uid": "74583f34-e7f5-4cce-a233-6713b3d60e9f"
},
"serviceaccount": {
"name": "default",
"uid": "c1739b57-b4bf-4100-bdba-254e5f51f6ae"
}
},
"nbf": 1636236491,
"sub": "system:serviceaccount:default:default"
},
"signature": "cIzDvAm7Y4qlNGY96NpyVU_tNwzlyOx5DQoZImmKv-REdYiEVAGB-Qz5TsdqhzY_PTpRbUQiJHSce7p2PtsFFZuD5JpuZNZRO2sVdSFMNLW8VGvZYBYnHhtUbJlHYFJvSP0irMqw3d_wagXtXu3Rw9JgcWiv0RPq3fEPcGNpfzqCc4KvlATwmKmCy2mQ-SSKTllWRrrTrpqKQ3VINp718SXjq0Yv1Si9LDMbRBg8zVR0zYGmxk1SlYom0hfbuZuE6-Waj5rCalH1ZJB9UgfHNVGW4GlarTQsG0Shs6a3Yayapf4Fx1JmJj6A-gx2IuqtfCTvhSE28K-1TdvVLQD70Q"
}

// 为了能够获取公钥并针对 Kubernetes 集群的颁发者验证 JWT 令牌,我们必须允许外部未经身份验证的请求。为此,我们将此特殊角色(system:service-account-issuer-discovery)与 ClusterRoleBinding 绑定到未经身份验证的用户:
kubectl create clusterrolebinding oidc-reviewer --clusterrole=system:service-account-issuer-discovery --group=system:unauthenticated
集群包括一个的默认 RBAC ClusterRole, 名为 system:service-account-issuer-discovery。 如果要集成外部系统,需要将该角色绑定到 system:authenticated
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: oidc-reviewer
subjects:
- kind: Group
name: system:authenticated
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: system:service-account-issuer-discovery
apiGroup: rbac.authorization.k8s.io
// 获取 Kubernetes API 服务器证书的 CA 签名证书以对其进行验证:
kubectl exec nginx -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > kubernetes_ca.crt
获取 Projected SA JWT token
export TOKEN=$(kubectl exec nginx -- cat /var/run/secrets/tokens/oidc-token)
访问 Kubernetes OIDC URL
// curl --cacert kubernetes_ca.crt https://localhost:6443/.well-known/openid-configuration | jq
curl -k -H "Authorization: Bearer $TOKEN" https://localhost:6443/.well-known/openid-configuration | jq
{
"issuer": "https://localhost:6443",
"jwks_uri": "https://localhost:6443/openid/v1/jwks",
"response_types_supported": [
"id_token"
],
"subject_types_supported": [
"public"
],
"id_token_signing_alg_values_supported": [
"RS256"
]
}
访问 JWKS 地址(”jwks_uri”)查看公钥:
// curl --cacert kubernetes_ca.crt https://localhost:6443/openid/v1/jwks | jq
curl -k -H "Authorization: Bearer $TOKEN" https://localhost:6443/openid/v1/jwks | jq
{
"keys": [
{
"use": "sig",
"kty": "RSA",
"kid": "tqsnK8Han3Jd2-DU2MmP-hOwKvFLTdGPDIVBK6iXxxs",
"alg": "RS256",
"n": "omu3ptqnQ4D2g5l2vLJQ5IBgPDlDPS5avpvE_PBPxApXfGACN2kUGqE--Xi_C8dTZwvyKYz8lIx109Lnnd-TN2QFJQsdD2cftL6dwLM_EueI7Ic4-VkmuATqv7wGuw9LFAaNoL_cUizEXO5OjhChfAe6RCuXoZu7D_PbLMkFqup4UuIBrzRlmZJfNvHusjqVGsCFCA5drEqYLN6k_RyNPx6srT1L95usqQpwUPPKDT12zeAYf3kTZBfGVKHUoIGcMuTrCW2e95VXgKnhArLTl6--rfGyteGAokHM3lfQjwCBYyuG9__8My__HYO7hvT9C_ITYcbwgeIiv_NttUZ0bQ",
"e": "AQAB"
}
]
}
结果:可以从 K8s API 服务器提供的 Discovery 端点,获取用于验证 OpenID Configuration 的信息和用于从 JWKs 端点验证 SA Token 签名的密钥信息。
将 Vault 配置为 OIDC 使用者
vault server -dev
配置 JWT Auth,将 Kubernetes JWT 令牌与 OIDC 端点联合
vault auth enable jwt
vault write auth/jwt/config \
oidc_discovery_url=https://localhost:6443 \
oidc_discovery_ca_pem=@kubernetes_ca.crt \
bound_issuer=https://localhost:6443
vault write auth/jwt/role/demo \
role_type=jwt \
bound_audiences=vault \
bound_subject="system:serviceaccount:default:default" \
user_claim=sub \
policies=default
获取投影的令牌并将其保存到变量中,然后将该令牌发送到 Vault 的 JWT 身份验证端点,以将其交换为 Vault 令牌
JWT=$(kubectl exec nginx -- cat /var/run/secrets/tokens/oidc-token)
curl http://127.0.0.1:8200/v1/auth/jwt/login --data "{\"jwt\": \"$JWT\", \"role\": \"demo\"}" | jq
{
"request_id": "26adb6c5-dd42-67fb-b484-1b4a3372b646",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": null,
"warnings": null,
"auth": {
"client_token": "s.yw6S1MFmpqbpTgCGthOde7uY",
"accessor": "1mLX8smcJXgbbSZMDfjhZd3I",
"policies": [
"default"
],
"token_policies": [
"default"
],
"metadata": {
"role": "demo"
},
"lease_duration": 2764800,
"renewable": true,
"entity_id": "b49f50c1-98b4-18ac-99cf-aad39e018901",
"token_type": "service",
"orphan": true
}
}
将上面的 Token 保存在 VAULT_TOKEN 变量中,并通过 Vault API 检测是否正常工作
VAULT_TOKEN=$(curl http://127.0.0.1:8200/v1/auth/jwt/login --data "{\"jwt\": \"$JWT\", \"role\": \"demo\"}" | jq -r .auth.client_token)
curl -H "X-Vault-Token: ${VAULT_TOKEN}" http://127.0.0.1:8200/v1/auth/token/lookup-self | jq
{
"request_id": "efabf7d7-5c6c-6c89-aa80-76d6eac4d7de",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": {
"accessor": "0pyvwtSMNfIqhiGafNrsJxT8",
"creation_time": 1636342168,
"creation_ttl": 2764800,
"display_name": "jwt-system:serviceaccount:default:default",
"entity_id": "b49f50c1-98b4-18ac-99cf-aad39e018901",
"expire_time": "2021-12-10T11:29:28.12252825+08:00",
"explicit_max_ttl": 0,
"id": "s.BktJJymaQamA1gLxjdEHsNrY",
"issue_time": "2021-11-08T11:29:28.122552807+08:00",
"meta": {
"role": "demo"
},
"num_uses": 0,
"orphan": true,
"path": "auth/jwt/login",
"policies": [
"default"
],
"renewable": true,
"ttl": 2764785,
"type": "service"
},
"wrap_info": null,
"warnings": null,
"auth": null
}
总结:在 k8s v1.21 之前,只能通过 TokenReview API 验证 SA Token。但是,如果在大规模环境下同时进行 SA Token 验证,就需要考虑降低 K8s API 服务器的负载。在这种情况下,新引入的功能可能就变得非常重要(Service Account Issuer Discovery)。
curl \
--header "X-Vault-Token: $JWT" \
--request POST \
http://127.0.0.1:8200/v1/auth/kubernetes/role/dev-role
curl --header "X-Vault-Token: $JWT" http://127.0.0.1:8200/v1/auth/jwt/role/test --data "{\"bound_service_account_namespaces\": \"*\"}" | jq
curl -H header "X-Vault-Token: ${JWT}" http://127.0.0.1:8200/v1/auth/jwt/role/demo
curl -H "X-Vault-Token: ${VAULT_TOKEN}" http://127.0.0.1:8200/v1/auth/jwt/config
curl http://127.0.0.1:8200/v1/auth/jwt/config --data "{\"jwt\": \"$JWT\", \"role\": \"demo\"}"
curl -H "X-Vault-Token: ${JWT}" http://127.0.0.1:8200/v1/auth/jwt/config
curl --request POST --data '{"jwt": "${jwt}", "role": "demo"}' http://127.0.0.1:8200/v1/auth/kubernetes/login
