在 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 设置以下命令行参数

  1. # 如果 k8s 低于 1.21,需要设置 ServiceAccountIssuerDiscovery=true
  2. --feature-gates=ServiceAccountIssuerDiscovery=true
  3. --service-account-issuer=https://localhost:6443
  4. # 验证ServiceAccount Token的私钥或公钥文件
  5. --service-account-key-file=/etc/kubernetes/pki/sa.pub
  6. # 含有当前service account token issuer私钥的文件路径
  7. --service-account-signing-key-file=/etc/kubernetes/pki/sa.key
  8. --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"
}

oidc.png

// 为了能够获取公钥并针对 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