用户认证
本页提供身份认证有关的概述。
Kubernetes 中的用户
所有 Kubernetes 集群都有两类用户:由 Kubernetes 管理的服务账号和普通用户。
Kubernetes 假定普通用户是由一个与集群无关的服务通过以下方式之一进行管理的:
- 负责分发私钥的管理员
- 类似 Keystone 或者 Google Accounts 这类用户数据库
- 包含用户名和密码列表的文件
有鉴于此,Kubernetes 并不包含用来代表普通用户账号的对象。 普通用户的信息无法通过 API 调用添加到集群中。
尽管无法通过 API 调用来添加普通用户,Kubernetes 仍然认为能够提供由集群的证书 机构签名的合法证书的用户是通过身份认证的用户。基于这样的配置,Kubernetes 使用证书中的 ‘subject’ 的通用名称(Common Name)字段(例如,”/CN=bob”)来 确定用户名。接下来,基于角色访问控制(RBAC)子系统会确定用户是否有权针对 某资源执行特定的操作。进一步的细节可参阅 证书请求 下普通用户主题。
与此不同,服务账号是 Kubernetes API 所管理的用户。它们被绑定到特定的名字空间, 或者由 API 服务器自动创建,或者通过 API 调用创建。服务账号与一组以 Secret 保存 的凭据相关,这些凭据会被挂载到 Pod 中,从而允许集群内的进程访问 Kubernetes API。
API 请求则或者与某普通用户相关联,或者与某服务账号相关联,亦或者被视作 匿名请求。这意味着集群内外的每个进程在向 API 服务器发起 请求时都必须通过身份认证,否则会被视作匿名用户。这里的进程可以是在某工作站上 输入 kubectl
命令的操作人员,也可以是节点上的 kubelet
组件,还可以是控制面 的成员。
身份认证策略
Kubernetes 通过身份认证插件利用客户端证书、持有者令牌(Bearer Token)或身份认证代理(Proxy) 来认证 API 请求的身份。HTTP 请求发给 API 服务器时,插件会将以下属性关联到请求本身:
- 用户名:用来辩识最终用户的字符串。常见的值可以是
kube-admin
或jane@example.com
。 - 用户 ID:用来辩识最终用户的字符串,旨在比用户名有更好的一致性和唯一性。
- 用户组:取值为一组字符串,其中各个字符串用来标明用户是某个命名的用户逻辑集合的成员。 常见的值可能是
system:masters
或者devops-team
等。 - 附加字段:一组额外的键-值映射,键是字符串,值是一组字符串;用来保存一些鉴权组件可能 觉得有用的额外信息。
所有(属性)值对于身份认证系统而言都是不透明的,只有被 鉴权组件 解释过之后才有意义。
你可以同时启用多种身份认证方法,并且你通常会至少使用两种方法:
- 针对服务账号使用服务账号令牌
- 至少另外一种方法对用户的身份进行认证
当集群中启用了多个身份认证模块时,第一个成功地对请求完成身份认证的模块会 直接做出评估决定。API 服务器并不保证身份认证模块的运行顺序。
对于所有通过身份认证的用户,system:authenticated
组都会被添加到其组列表中。
与其它身份认证协议(LDAP、SAML、Kerberos、X509 的替代模式等等)都可以通过 使用一个身份认证代理或 身份认证 Webhoook来实现。
X509 客户证书
通过给 API 服务器传递 --client-ca-file=SOMEFILE
选项,就可以启动客户端证书身份认证。 所引用的文件必须包含一个或者多个证书机构,用来验证向 API 服务器提供的客户端证书。 如果提供了客户端证书并且证书被验证通过,则 subject 中的公共名称(Common Name)就被 作为请求的用户名。 自 Kubernetes 1.4 开始,客户端证书还可以通过证书的 organization 字段标明用户的组成员信息。 要包含用户的多个组成员信息,可以在证书种包含多个 organization 字段。
例如,使用 openssl
命令行工具生成一个证书签名请求:
openssl req -new -key jbeda.pem -out jbeda-csr.pem -subj "/CN=jbeda/O=app1/O=app2"
此命令将使用用户名 jbeda
生成一个证书签名请求(CSR),且该用户属于 “app” 和 “app2” 两个用户组。
参阅管理证书了解如何生成客户端证书。
静态令牌文件
当 API 服务器的命令行设置了 --token-auth-file=SOMEFILE
选项时,会从文件中 读取持有者令牌。目前,令牌会长期有效,并且在不重启 API 服务器的情况下 无法更改令牌列表。
令牌文件是一个 CSV 文件,包含至少 3 个列:令牌、用户名和用户的 UID。 其余列被视为可选的组名。
Note:
如果要设置的组名不止一个,则对应的列必须用双引号括起来,例如
token,user,uid,"group1,group2,group3"
在请求中放入持有者令牌
当使用持有者令牌来对某 HTTP 客户端执行身份认证时,API 服务器希望看到 一个名为 Authorization
的 HTTP 头,其值格式为 Bearer <token>
。 持有者令牌必须是一个可以放入 HTTP 头部值字段的字符序列,至多可使用 HTTP 的编码和引用机制。 例如:如果持有者令牌为 31ada4fd-adec-460c-809a-9e56ceb75269
,则其 出现在 HTTP 头部时如下所示:
Authorization: Bearer 31ada4fd-adec-460c-809a-9e56ceb75269
启动引导令牌
FEATURE STATE: Kubernetes v1.18 [stable]
为了支持平滑地启动引导新的集群,Kubernetes 包含了一种动态管理的持有者令牌类型, 称作 启动引导令牌(Bootstrap Token)。 这些令牌以 Secret 的形式保存在 kube-system
名字空间中,可以被动态管理和创建。 控制器管理器包含的 TokenCleaner
控制器能够在启动引导令牌过期时将其删除。
这些令牌的格式为 [a-z0-9]{6}.[a-z0-9]{16}
。第一个部分是令牌的 ID;第二个部分 是令牌的 Secret。你可以用如下所示的方式来在 HTTP 头部设置令牌:
Authorization: Bearer 781292.db7bc3a58fc5f07e
你必须在 API 服务器上设置 --enable-bootstrap-token-auth
标志来启用基于启动 引导令牌的身份认证组件。 你必须通过控制器管理器的 --controllers
标志来启用 TokenCleaner 控制器; 这可以通过类似 --controllers=*,tokencleaner
这种设置来做到。 如果你使用 kubeadm
来启动引导新的集群,该工具会帮你完成这些设置。
身份认证组件的认证结果为 system:bootstrap:<令牌 ID>
,该用户属于 system:bootstrappers
用户组。 这里的用户名和组设置都是有意设计成这样,其目的是阻止用户在启动引导集群之后 继续使用这些令牌。 这里的用户名和组名可以用来(并且已经被 kubeadm
用来)构造合适的鉴权 策略,以完成启动引导新集群的工作。
请参阅启动引导令牌 以了解关于启动引导令牌身份认证组件与控制器的更深入的信息,以及如何使用 kubeadm
来管理这些令牌。
服务账号令牌
服务账号(Service Account)是一种自动被启用的用户认证机制,使用经过签名的 持有者令牌来验证请求。该插件可接受两个可选参数:
--service-account-key-file
一个包含用来为持有者令牌签名的 PEM 编码密钥。 若未指定,则使用 API 服务器的 TLS 私钥。--service-account-lookup
如果启用,则从 API 删除的令牌会被回收。
服务账号通常由 API 服务器自动创建并通过 ServiceAccount
准入控制器 关联到集群中运行的 Pod 上。 持有者令牌会挂载到 Pod 中可预知的位置,允许集群内进程与 API 服务器通信。 服务账号也可以使用 Pod 规约的 serviceAccountName
字段显式地关联到 Pod 上。
Note: serviceAccountName
通常会被忽略,因为关联关系是自动建立的。
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment
namespace: default
spec:
replicas: 3
template:
metadata:
# ...
spec:
serviceAccountName: bob-the-bot
containers:
- name: nginx
image: nginx:1.14.2
在集群外部使用服务账号持有者令牌也是完全合法的,且可用来为长时间运行的、需要与 Kubernetes API 服务器通信的任务创建标识。要手动创建服务账号,可以使用 kubectl create serviceaccount <名称>
命令。此命令会在当前的名字空间中生成一个 服务账号和一个与之关联的 Secret。
kubectl create serviceaccount jenkins
serviceaccount/jenkins created
查验相关联的 Secret:
kubectl get serviceaccounts jenkins -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
# ...
secrets:
- name: jenkins-token-1yvwg
所创建的 Secret 中会保存 API 服务器的公开的 CA 证书和一个已签名的 JSON Web 令牌(JWT)。
kubectl get secret jenkins-token-1yvwg -o yaml
apiVersion: v1
data:
ca.crt: <Base64 编码的 API 服务器 CA>
namespace: ZGVmYXVsdA==
token: <Base64 编码的持有者令牌>
kind: Secret
metadata:
# ...
type: kubernetes.io/service-account-token
Note: 字段值是按 Base64 编码的,这是因为 Secret 数据总是采用 Base64 编码来存储。
已签名的 JWT 可以用作持有者令牌,并将被认证为所给的服务账号。 关于如何在请求中包含令牌,请参阅前文。 通常,这些 Secret 数据会被挂载到 Pod 中以便集群内访问 API 服务器时使用, 不过也可以在集群外部使用。
服务账号被身份认证后,所确定的用户名为 system:serviceaccount:<名字空间>:<服务账号>
, 并被分配到用户组 system:serviceaccounts
和 system:serviceaccounts:<名字空间>
。
警告:由于服务账号令牌保存在 Secret 对象中,任何能够读取这些 Secret 的用户 都可以被认证为对应的服务账号。在为用户授予访问服务账号的权限时,以及对 Secret 的读权限时,要格外小心。
OpenID Connect(OIDC)令牌
OpenID Connect 是一种 OAuth2 认证方式, 被某些 OAuth2 提供者支持,例如 Azure 活动目录、Salesforce 和 Google。 协议对 OAuth2 的主要扩充体现在有一个附加字段会和访问令牌一起返回, 这一字段称作 ID Token(ID 令牌)。 ID 令牌是一种由服务器签名的 JSON Web 令牌(JWT),其中包含一些可预知的字段, 例如用户的邮箱地址,
要识别用户,身份认证组件使用 OAuth2 令牌响应 中的 id_token
(而非 access_token
)作为持有者令牌。 关于如何在请求中设置令牌,可参见前文。
sequenceDiagram participant user as 用户 participant idp as 身份提供者 participant kube as Kubectl participant api as API 服务器 user ->> idp: 1. 登录到 IdP activate idp idp —>> user: 2. 提供 access_token,
id_token, 和 refresh_token deactivate idp activate user user ->> kube: 3. 调用 Kubectl 并
设置 —token 为 id_token
或者将令牌添加到 .kube/config deactivate user activate kube kube ->> api: 4. Authorization: Bearer… deactivate kube activate api api ->> api: 5. JWT 签名合法么? api ->> api: 6. JWT 是否已过期?(iat+exp) api ->> api: 7. 用户被授权了么? api —>> kube: 8. 已授权:执行
操作并返回结果 deactivate api activate kube kube —x user: 9. 返回结果 deactivate kube
JavaScript must be enabled to view this content
- 登录到你的身份服务(Identity Provider)
- 你的身份服务将为你提供
access_token
、id_token
和refresh_token
- 在使用
kubectl
时,将id_token
设置为--token
标志值,或者将其直接添加到kubeconfig
中 kubectl
将你的id_token
放到一个称作Authorization
的头部,发送给 API 服务器- API 服务器将负责通过检查配置中引用的证书来确认 JWT 的签名是合法的
- 检查确认
id_token
尚未过期 - 确认用户有权限执行操作
- 鉴权成功之后,API 服务器向
kubectl
返回响应 kubectl
向用户提供反馈信息
由于用来验证你是谁的所有数据都在 id_token
中,Kubernetes 不需要再去联系身份服务。 在一个所有请求都是无状态请求的模型中,这一工作方式可以使得身份认证的解决方案更容易处理大规模请求。 不过,此访问也有一些挑战:
- Kubernetes 没有提供用来触发身份认证过程的 “Web 界面”。 因为不存在用来收集用户凭据的浏览器或用户接口,你必须自己先行完成对身份服务的认证过程。
id_token
令牌不可收回。因其属性类似于证书,其生命期一般很短(只有几分钟), 所以,每隔几分钟就要获得一个新的令牌这件事可能很让人头疼。- 如果需要向 Kubernetes 控制面板执行身份认证,你必须使用
kubectl proxy
命令或者一个能够注入id_token
的反向代理。
配置 API 服务器
要启用此插件,须在 API 服务器上配置以下标志:
参数 | 描述 | 示例 | 必需? |
---|---|---|---|
—oidc-issuer-url | 允许 API 服务器发现公开的签名密钥的服务的 URL。只接受模式为 https:// 的 URL。此值通常设置为服务的发现 URL,不含路径。例如:”https://accounts.google.com“ 或 “https://login.salesforce.com"。此 URL 应指向 .well-known/openid-configuration 下一层的路径。 | 如果发现 URL 是 https://accounts.google.com/.well-known/openid-configuration ,则此值应为 https://accounts.google.com | 是 |
—oidc-client-id | 所有令牌都应发放给此客户 ID。 | kubernetes | 是 |
—oidc-username-claim | 用作用户名的 JWT 申领(JWT Claim)。默认情况下使用 sub 值,即最终用户的一个唯一的标识符。管理员也可以选择其他申领,例如 email 或者 name ,取决于所用的身份服务。不过,除了 email 之外的申领都会被添加令牌发放者的 URL 作为前缀,以免与其他插件产生命名冲突。 | sub | 否 |
—oidc-username-prefix | 要添加到用户名申领之前的前缀,用来避免与现有用户名发生冲突(例如:system: 用户)。例如,此标志值为 oidc: 时将创建形如 oidc:jane.doe 的用户名。如果此标志未设置,且 —oidc-username-claim 标志值不是 email ,则默认前缀为 <令牌发放者的 URL># ,其中 <令牌发放者 URL > 的值取自 —oidc-issuer-url 标志的设定。此标志值为 - 时,意味着禁止添加用户名前缀。 | oidc: | 否 |
—oidc-groups-claim | 用作用户组名的 JWT 申领。如果所指定的申领确实存在,则其值必须是一个字符串数组。 | groups | 否 |
—oidc-groups-prefix | 添加到组申领的前缀,用来避免与现有用户组名(如:system: 组)发生冲突。例如,此标志值为 oidc: 时,所得到的用户组名形如 oidc:engineering 和 oidc:infra 。 | oidc: | 否 |
—oidc-required-claim | 取值为一个 key=value 偶对,意为 ID 令牌中必须存在的申领。如果设置了此标志,则 ID 令牌会被检查以确定是否包含取值匹配的申领。此标志可多次重复,以指定多个申领。 | claim=value | 否 |
—oidc-ca-file | 指向一个 CA 证书的路径,该 CA 负责对你的身份服务的 Web 证书提供签名。默认值为宿主系统的根 CA。 | /etc/kubernetes/ssl/kc-ca.pem | 否 |
很重要的一点是,API 服务器并非一个 OAuth2 客户端,相反,它只能被配置为 信任某一个令牌发放者。这使得使用公共服务(如 Google)的用户可以不信任发放给 第三方的凭据。 如果管理员希望使用多个 OAuth 客户端,他们应该研究一下那些支持 azp
(Authorized Party,被授权方)申领的服务。 azp
是一种允许某客户端代替另一客户端发放令牌的机制。
Kubernetes 并未提供 OpenID Connect 的身份服务。 你可以使用现有的公共的 OpenID Connect 身份服务(例如 Google 或者 其他服务)。 或者,你也可以选择自己运行一个身份服务,例如 CoreOS dex、 Keycloak、 CloudFoundry UAA 或者 Tremolo Security 的 OpenUnison。
要在 Kubernetes 环境中使用某身份服务,该服务必须:
- 支持 OpenID connect 发现; 但事实上并非所有服务都具备此能力
- 运行 TLS 协议且所使用的加密组件都未过时
- 拥有由 CA 签名的证书(即使 CA 不是商业 CA 或者是自签名的 CA 也可以)
关于上述第三条需求,即要求具备 CA 签名的证书,有一些额外的注意事项。 如果你部署了自己的身份服务,而不是使用云厂商(如 Google 或 Microsoft)所提供的服务, 你必须对身份服务的 Web 服务器证书进行签名,签名所用证书的 CA
标志要设置为 TRUE
,即使用的是自签名证书。这是因为 GoLang 的 TLS 客户端实现对证书验证 标准方面有非常严格的要求。如果你手头没有现成的 CA 证书,可以使用 CoreOS 团队所开发的这个脚本 来创建一个简单的 CA 和被签了名的证书与密钥对。 或者你也可以使用 这个类似的脚本, 生成一个合法期更长、密钥尺寸更大的 SHA256 证书。
特定系统的安装指令:
使用 kubectl
选项一 - OIDC 身份认证组件
第一种方案是使用 kubectl 的 oidc
身份认证组件,该组件将 id_token
设置 为所有请求的持有者令牌,并且在令牌过期时自动刷新。在你登录到你的身份服务之后, 可以使用 kubectl 来添加你的 id_token
、refresh_token
、client_id
和 client_secret
,以配置该插件。
如果服务在其刷新令牌响应中不包含 id_token
,则此插件无法支持该服务。 这时你应该考虑下面的选项二。
kubectl config set-credentials USER_NAME \
--auth-provider=oidc \
--auth-provider-arg=idp-issuer-url=( issuer url ) \
--auth-provider-arg=client-id=( your client id ) \
--auth-provider-arg=client-secret=( your client secret ) \
--auth-provider-arg=refresh-token=( your refresh token ) \
--auth-provider-arg=idp-certificate-authority=( path to your ca certificate ) \
--auth-provider-arg=id-token=( your id_token )
作为示例,在完成对你的身份服务的身份认证之后,运行下面的命令:
kubectl config set-credentials mmosley \
--auth-provider=oidc \
--auth-provider-arg=idp-issuer-url=https://oidcidp.tremolo.lan:8443/auth/idp/OidcIdP \
--auth-provider-arg=client-id=kubernetes \
--auth-provider-arg=client-secret=1db158f6-177d-4d9c-8a8b-d36869918ec5 \
--auth-provider-arg=refresh-token=q1bKLFOyUiosTfawzA93TzZIDzH2TNa2SMm0zEiPKTUwME6BkEo6Sql5yUWVBSWpKUGphaWpxSVAfekBOZbBhaEW+VlFUeVRGcluyVF5JT4+haZmPsluFoFu5XkpXk5BXqHega4GAXlF+ma+vmYpFcHe5eZR+slBFpZKtQA= \
--auth-provider-arg=idp-certificate-authority=/root/ca.pem \
--auth-provider-arg=id-token=eyJraWQiOiJDTj1vaWRjaWRwLnRyZW1vbG8ubGFuLCBPVT1EZW1vLCBPPVRybWVvbG8gU2VjdXJpdHksIEw9QXJsaW5ndG9uLCBTVD1WaXJnaW5pYSwgQz1VUy1DTj1rdWJlLWNhLTEyMDIxNDc5MjEwMzYwNzMyMTUyIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29pZGNpZHAudHJlbW9sby5sYW46ODQ0My9hdXRoL2lkcC9PaWRjSWRQIiwiYXVkIjoia3ViZXJuZXRlcyIsImV4cCI6MTQ4MzU0OTUxMSwianRpIjoiMm96US15TXdFcHV4WDlHZUhQdy1hZyIsImlhdCI6MTQ4MzU0OTQ1MSwibmJmIjoxNDgzNTQ5MzMxLCJzdWIiOiI0YWViMzdiYS1iNjQ1LTQ4ZmQtYWIzMC0xYTAxZWU0MWUyMTgifQ.w6p4J_6qQ1HzTG9nrEOrubxIMb9K5hzcMPxc9IxPx2K4xO9l-oFiUw93daH3m5pluP6K7eOE6txBuRVfEcpJSwlelsOsW8gb8VJcnzMS9EnZpeA0tW_p-mnkFc3VcfyXuhe5R3G7aa5d8uHv70yJ9Y3-UhjiN9EhpMdfPAoEB9fYKKkJRzF7utTTIPGrSaSU6d2pcpfYKaxIwePzEkT4DfcQthoZdy9ucNvvLoi1DIC-UocFD8HLs8LYKEqSxQvOcvnThbObJ9af71EwmuE21fO5KzMW20KtAeget1gnldOosPtz1G5EwvaQ401-RPQzPGMVBld0_zMCAwZttJ4knw
此操作会生成以下配置:
users:
- name: mmosley
user:
auth-provider:
config:
client-id: kubernetes
client-secret: 1db158f6-177d-4d9c-8a8b-d36869918ec5
id-token: eyJraWQiOiJDTj1vaWRjaWRwLnRyZW1vbG8ubGFuLCBPVT1EZW1vLCBPPVRybWVvbG8gU2VjdXJpdHksIEw9QXJsaW5ndG9uLCBTVD1WaXJnaW5pYSwgQz1VUy1DTj1rdWJlLWNhLTEyMDIxNDc5MjEwMzYwNzMyMTUyIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL29pZGNpZHAudHJlbW9sby5sYW46ODQ0My9hdXRoL2lkcC9PaWRjSWRQIiwiYXVkIjoia3ViZXJuZXRlcyIsImV4cCI6MTQ4MzU0OTUxMSwianRpIjoiMm96US15TXdFcHV4WDlHZUhQdy1hZyIsImlhdCI6MTQ4MzU0OTQ1MSwibmJmIjoxNDgzNTQ5MzMxLCJzdWIiOiI0YWViMzdiYS1iNjQ1LTQ4ZmQtYWIzMC0xYTAxZWU0MWUyMTgifQ.w6p4J_6qQ1HzTG9nrEOrubxIMb9K5hzcMPxc9IxPx2K4xO9l-oFiUw93daH3m5pluP6K7eOE6txBuRVfEcpJSwlelsOsW8gb8VJcnzMS9EnZpeA0tW_p-mnkFc3VcfyXuhe5R3G7aa5d8uHv70yJ9Y3-UhjiN9EhpMdfPAoEB9fYKKkJRzF7utTTIPGrSaSU6d2pcpfYKaxIwePzEkT4DfcQthoZdy9ucNvvLoi1DIC-UocFD8HLs8LYKEqSxQvOcvnThbObJ9af71EwmuE21fO5KzMW20KtAeget1gnldOosPtz1G5EwvaQ401-RPQzPGMVBld0_zMCAwZttJ4knw
idp-certificate-authority: /root/ca.pem
idp-issuer-url: https://oidcidp.tremolo.lan:8443/auth/idp/OidcIdP
refresh-token: q1bKLFOyUiosTfawzA93TzZIDzH2TNa2SMm0zEiPKTUwME6BkEo6Sql5yUWVBSWpKUGphaWpxSVAfekBOZbBhaEW+VlFUeVRGcluyVF5JT4+haZmPsluFoFu5XkpXk5BXq
name: oidc
当你的 id_token
过期时,kubectl
会尝试使用你的 refresh_token
来刷新你的 id_token
,并且在 .kube/config
文件的 client_secret
中存放 refresh_token
和 id_token
的新值。
选项二 - 使用 --token
选项
kubectl
命令允许你使用 --token
选项传递一个令牌。 你可以将 id_token
的内容复制粘贴过来,作为此标志的取值:
kubectl --token=eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL21sYi50cmVtb2xvLmxhbjo4MDQzL2F1dGgvaWRwL29pZGMiLCJhdWQiOiJrdWJlcm5ldGVzIiwiZXhwIjoxNDc0NTk2NjY5LCJqdGkiOiI2RDUzNXoxUEpFNjJOR3QxaWVyYm9RIiwiaWF0IjoxNDc0NTk2MzY5LCJuYmYiOjE0NzQ1OTYyNDksInN1YiI6Im13aW5kdSIsInVzZXJfcm9sZSI6WyJ1c2VycyIsIm5ldy1uYW1lc3BhY2Utdmlld2VyIl0sImVtYWlsIjoibXdpbmR1QG5vbW9yZWplZGkuY29tIn0.f2As579n9VNoaKzoF-dOQGmXkFKf1FMyNV0-va_B63jn-_n9LGSCca_6IVMP8pO-Zb4KvRqGyTP0r3HkHxYy5c81AnIh8ijarruczl-TK_yF5akjSTHFZD-0gRzlevBDiH8Q79NAr-ky0P4iIXS8lY9Vnjch5MF74Zx0c3alKJHJUnnpjIACByfF2SCaYzbWFMUNat-K1PaUk5-ujMBG7yYnr95xD-63n8CO8teGUAAEMx6zRjzfhnhbzX-ajwZLGwGUBT4WqjMs70-6a7_8gZmLZb2az1cZynkFRj2BaCkVT3A2RrjeEwZEtGXlMqKJ1_I2ulrOVsYx01_yD35-rw get nodes
Webhook 令牌身份认证
Webhook 身份认证是一种用来验证持有者令牌的回调机制。
--authentication-token-webhook-config-file
指向一个配置文件,其中描述 如何访问远程的 Webhook 服务。--authentication-token-webhook-cache-ttl
用来设定身份认证决定的缓存时间。 默认时长为 2 分钟。
配置文件使用 kubeconfig 文件的格式。文件中,clusters
指代远程服务,users
指代远程 API 服务 Webhook。下面是一个例子:
# Kubernetes API 版本
apiVersion: v1
# API 对象类别
kind: Config
# clusters 指代远程服务
clusters:
- name: name-of-remote-authn-service
cluster:
certificate-authority: /path/to/ca.pem # 用来验证远程服务的 CA
server: https://authn.example.com/authenticate # 要查询的远程服务 URL。生产环境中建议使用 'https'。
# users 指代 API 服务的 Webhook 配置
users:
- name: name-of-api-server
user:
client-certificate: /path/to/cert.pem # Webhook 插件要使用的证书
client-key: /path/to/key.pem # 与证书匹配的密钥
# kubeconfig 文件需要一个上下文(Context),此上下文用于本 API 服务器
current-context: webhook
contexts:
- context:
cluster: name-of-remote-authn-service
user: name-of-api-sever
name: webhook
当客户端尝试在 API 服务器上使用持有者令牌完成身份认证( 如前所述)时, 身份认证 Webhook 会用 POST 请求发送一个 JSON 序列化的对象到远程服务。 该对象是 authentication.k8s.io/v1beta1
组的 TokenReview
对象, 其中包含持有者令牌。 Kubernetes 不会强制请求提供此 HTTP 头部。
要注意的是,Webhook API 对象和其他 Kubernetes API 对象一样,也要受到同一 版本兼容规则约束。 实现者要了解对 Beta 阶段对象的兼容性承诺,并检查请求的 apiVersion
字段, 以确保数据结构能够正常反序列化解析。此外,API 服务器必须启用 authentication.k8s.io/v1beta1
API 扩展组 (--runtime-config=authentication.k8s.io/v1beta1=true
)。
POST 请求的 Body 部分将是如下格式:
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"spec": {
"token": "<持有者令牌>"
}
}
远程服务应该会填充请求的 status
字段,以标明登录操作是否成功。 响应的 Body 中的 spec
字段会被忽略,因此可以省略。 如果持有者令牌验证成功,应该返回如下所示的响应:
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": true,
"user": {
"username": "janedoe@example.com",
"uid": "42",
"groups": [
"developers",
"qa"
],
"extra": {
"extrafield1": [
"extravalue1",
"extravalue2"
]
}
}
}
}
而不成功的请求会返回:
{
"apiVersion": "authentication.k8s.io/v1beta1",
"kind": "TokenReview",
"status": {
"authenticated": false
}
}
HTTP 状态码可用来提供进一步的错误语境信息。
身份认证代理
API 服务器可以配置成从请求的头部字段值(如 X-Remote-User
)中辩识用户。 这一设计是用来与某身份认证代理一起使用 API 服务器,代理负责设置请求的头部字段值。
--requestheader-username-headers
必需字段,大小写不敏感。用来设置要获得用户身份所要检查的头部字段名称列表(有序)。第一个包含数值的字段会被用来提取用户名。--requestheader-group-headers
可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 “X-Remote-Group”。用来指定一组头部字段名称列表,以供检查用户所属的组名称。 所找到的全部头部字段的取值都会被用作用户组名。--requestheader-extra-headers-prefix
可选字段,在 Kubernetes 1.6 版本以后支持,大小写不敏感。 建议设置为 “X-Remote-Extra-“。用来设置一个头部字段的前缀字符串,API 服务器会基于所给 前缀来查找与用户有关的一些额外信息。这些额外信息通常用于所配置的鉴权插件。 API 服务器会将与所给前缀匹配的头部字段过滤出来,去掉其前缀部分,将剩余部分 转换为小写字符串并在必要时执行百分号解码 后,构造新的附加信息字段键名。原来的头部字段值直接作为附加信息字段的值。
Note: 在 1.13.3 版本之前(包括 1.10.7、1.9.11),附加字段的键名只能包含 HTTP 头部标签的合法字符。
例如,使用下面的配置:
--requestheader-username-headers=X-Remote-User
--requestheader-group-headers=X-Remote-Group
--requestheader-extra-headers-prefix=X-Remote-Extra-
针对所收到的如下请求:
GET / HTTP/1.1
X-Remote-User: fido
X-Remote-Group: dogs
X-Remote-Group: dachshunds
X-Remote-Extra-Acme.com%2Fproject: some-project
X-Remote-Extra-Scopes: openid
X-Remote-Extra-Scopes: profile
会生成下面的用户信息:
name: fido
groups:
- dogs
- dachshunds
extra:
acme.com/project:
- some-project
scopes:
- openid
- profile
为了防范头部信息侦听,在请求中的头部字段被检视之前, 身份认证代理需要向 API 服务器提供一份合法的客户端证书, 供后者使用所给的 CA 来执行验证。 警告:不要 在不同的上下文中复用 CA 证书,除非你清楚这样做的风险是什么以及 应如何保护 CA 用法的机制。
--requestheader-client-ca-file
必需字段,给出 PEM 编码的证书包。 在检查请求的头部字段以提取用户名信息之前,必须提供一个合法的客户端证书, 且该证书要能够被所给文件中的机构所验证。--requestheader-allowed-names
可选字段,用来给出一组公共名称(CN)。 如果此标志被设置,则在检视请求中的头部以提取用户信息之前,必须提供 包含此列表中所给的 CN 名的、合法的客户端证书。
匿名请求
启用匿名请求支持之后,如果请求没有被已配置的其他身份认证方法拒绝,则被视作 匿名请求(Anonymous Requests)。这类请求获得用户名 system:anonymous
和 对应的用户组 system:unauthenticated
。
例如,在一个配置了令牌身份认证且启用了匿名访问的服务器上,如果请求提供了非法的 持有者令牌,则会返回 401 Unauthorized
错误。 如果请求没有提供持有者令牌,则被视为匿名请求。
在 1.5.1-1.5.x 版本中,匿名访问默认情况下是被禁用的,可以通过为 API 服务器设定 --anonymous-auth=true
来启用。
在 1.6 及之后版本中,如果所使用的鉴权模式不是 AlwaysAllow
,则匿名访问默认是被启用的。 从 1.6 版本开始,ABAC 和 RBAC 鉴权模块要求对 system:anonymous
用户或者 system:unauthenticated
用户组执行显式的权限判定,所以之前的为 *
用户或 *
用户组赋予访问权限的策略规则都不再包含匿名用户。
用户伪装
一个用户可以通过伪装(Impersonation)头部字段来以另一个用户的身份执行操作。 使用这一能力,你可以手动重载请求被身份认证所识别出来的用户信息。 例如,管理员可以使用这一功能特性来临时伪装成另一个用户,查看请求是否被拒绝, 从而调试鉴权策略中的问题,
带伪装的请求首先会被身份认证识别为发出请求的用户,之后会切换到使用被伪装的用户 的用户信息。
- 用户发起 API 调用时 同时 提供自身的凭据和伪装头部字段信息
- API 服务器对用户执行身份认证
- API 服务器确认通过认证的用户具有伪装特权
- 请求用户的信息被替换成伪装字段的值
- 评估请求,鉴权组件针对所伪装的用户信息执行操作
以下 HTTP 头部字段可用来执行伪装请求:
Impersonate-User
:要伪装成的用户名Impersonate-Group
:要伪装成的用户组名。可以多次指定以设置多个用户组。 可选字段;要求 “Impersonate-User” 必须被设置。Impersonate-Extra-<附加名称>
:一个动态的头部字段,用来设置与用户相关的附加字段。 此字段可选;要求 “Impersonate-User” 被设置。为了能够以一致的形式保留,<附加名称>
部分必须是小写字符,如果有任何字符不是 合法的 HTTP 头部标签字符, 则必须是 utf8 字符,且转换为百分号编码。Impersonate-Uid
:一个唯一标识符,用来表示所伪装的用户。此头部可选。 如果设置,则要求 “Impersonate-User” 也存在。 Kubernetes 对此字符串没有格式要求。
Note: 在 1.11.3 版本之前(以及 1.10.7、1.9.11),<附加名称>
只能包含 合法的 HTTP 标签字符。
Note:
Impersonate-Uid
仅在 1.22.0 及更高版本中可用。
伪装带有用户组的用户时,所使用的伪装头部字段示例:
Impersonate-User: jane.doe@example.com
Impersonate-Group: developers
Impersonate-Group: admins
伪装带有 UID 和附加字段的用户时,所使用的伪装头部字段示例:
Impersonate-User: jane.doe@example.com
Impersonate-Group: developers
Impersonate-Group: admins
Impersonate-Extra-dn: cn=jane,ou=engineers,dc=example,dc=com
Impersonate-Extra-acme.com%2Fproject: some-project
Impersonate-Extra-scopes: view
Impersonate-Extra-scopes: development
在使用 kubectl
时,可以使用 --as
标志来配置 Impersonate-User
头部字段值, 使用 --as-group
标志配置 Impersonate-Group
头部字段值。
kubectl drain mynode
Error from server (Forbidden): User "clark" cannot get nodes at the cluster scope. (get nodes mynode)
设置 --as
和 --as-group
标志:
kubectl drain mynode --as=superman --as-group=system:masters
node/mynode cordoned
node/mynode drained
Note:
kubectl
不能对附加字段或 UID 执行伪装。
若要伪装成某个用户、某个组、用户标识符(UID))或者设置附加字段, 执行伪装操作的用户必须具有对所伪装的类别(“user”、“group”、“uid” 等)执行 “impersonate” 动词操作的能力。 对于启用了 RBAC 鉴权插件的集群,下面的 ClusterRole 封装了设置用户和组伪装字段所需的规则:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: impersonator
rules:
- apiGroups: [""]
resources: ["users", "groups", "serviceaccounts"]
verbs: ["impersonate"]
为了执行伪装,附加字段和所伪装的 UID 都位于 “authorization.k8s.io” apiGroup
中。 附加字段会被作为 userextras
资源的子资源来执行权限评估。 如果要允许用户为附加字段 “scopes” 和 UID 设置伪装头部,该用户需要被授予以下角色:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: scopes-and-uid-impersonator
rules:
# 可以设置 "Impersonate-Extra-scopes" 和 "Impersonate-Uid" 头部
- apiGroups: ["authentication.k8s.io"]
resources: ["userextras/scopes", "uids"]
verbs: ["impersonate"]
你也可以通过约束资源可能对应的 resourceNames
限制伪装头部的取值:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: limited-impersonator
rules:
# 可以伪装成用户 "jane.doe@example.com"
- apiGroups: [""]
resources: ["users"]
verbs: ["impersonate"]
resourceNames: ["jane.doe@example.com"]
# 可以伪装成用户组 "developers" 和 "admins"
- apiGroups: [""]
resources: ["groups"]
verbs: ["impersonate"]
resourceNames: ["developers","admins"]
# 可以将附加字段 "scopes" 伪装成 "view" 和 "development"
- apiGroups: ["authentication.k8s.io"]
resources: ["userextras/scopes"]
verbs: ["impersonate"]
resourceNames: ["view", "development"]
# 可以伪装 UID "06f6ce97-e2c5-4ab8-7ba5-7654dd08d52b"
- apiGroups: ["authentication.k8s.io"]
resources: ["uids"]
verbs: ["impersonate"]
resourceNames: ["06f6ce97-e2c5-4ab8-7ba5-7654dd08d52b"]
client-go 凭据插件
FEATURE STATE: Kubernetes v1.22 [stable]
k8s.io/client-go
及使用它的工具(如 kubectl
和 kubelet
)可以执行某个外部 命令来获得用户的凭据信息。
这一特性的目的是便于客户端与 k8s.io/client-go
并不支持的身份认证协议(LDAP、 Kerberos、OAuth2、SAML 等)继承。 插件实现特定于协议的逻辑,之后返回不透明的凭据以供使用。 几乎所有的凭据插件使用场景中都需要在服务器端存在一个支持 Webhook 令牌身份认证组件的模块, 负责解析客户端插件所生成的凭据格式。
示例应用场景
在一个假想的应用场景中,某组织运行这一个外部的服务,能够将特定用户的已签名的 令牌转换成 LDAP 凭据。此服务还能够对 Webhook 令牌身份认证组件的请求做出响应以 验证所提供的令牌。用户需要在自己的工作站上安装一个凭据插件。
要对 API 服务器认证身份时:
- 用户发出
kubectl
命令。 - 凭据插件提示用户输入 LDAP 凭据,并与外部服务交互,获得令牌。
- 凭据插件将令牌返回该 client-go,后者将其用作持有者令牌提交给 API 服务器。
- API 服务器使用Webhook 令牌身份认证组件向 外部服务发出
TokenReview
请求。 - 外部服务检查令牌上的签名,返回用户的用户名和用户组信息。
配置
凭据插件通过 kubectl 配置文件 来作为 user 字段的一部分设置。
apiVersion: v1
kind: Config
users:
- name: my-user
user:
exec:
# 要执行的命令。必需。
command: "example-client-go-exec-plugin"
# 解析 ExecCredentials 资源时使用的 API 版本。必需。
#
# 插件返回的 API 版本必需与这里列出的版本匹配。
#
# 要与支持多个版本的工具(如 client.authentication.k8sio/v1alpha1)集成,
# 可以设置一个环境变量或者向工具传递一个参数标明 exec 插件所期望的版本,
# 或者从 KUBERNETES_EXEC_INFO 环境变量的 ExecCredential 对象中读取版本信息。
apiVersion: "client.authentication.k8s.io/v1"
# 执行此插件时要设置的环境变量。可选字段。
env:
- name: "FOO"
value: "bar"
# 执行插件时要传递的参数。可选字段。
args:
- "arg1"
- "arg2"
# 当可执行文件不存在时显示给用户的文本。可选的。
installHint: |
需要 example-client-go-exec-plugin 来在当前集群上执行身份认证。可以通过以下命令安装:
MacOS: brew install example-client-go-exec-plugin
Ubuntu: apt-get install example-client-go-exec-plugin
Fedora: dnf install example-client-go-exec-plugin
...
# 是否使用 KUBERNETES_EXEC_INFO 环境变量的一部分向这个 exec 插件
# 提供集群信息(可能包含非常大的 CA 数据)
provideClusterInfo: true
# Exec 插件与标准输入 I/O 数据流之间的协议。如果协议无法满足,
# 则插件无法运行并会返回错误信息。合法的值包括 "Never" (Exec 插件从不使用标准输入),
# "IfAvailable" (Exec 插件希望在可以的情况下使用标准输入),
# 或者 "Always" (Exec 插件需要使用标准输入才能工作)。必需字段。
interactiveMode: Never
clusters:
- name: my-cluster
cluster:
server: "https://172.17.4.100:6443"
certificate-authority: "/etc/kubernetes/ca.pem"
extensions:
- name: client.authentication.k8s.io/exec # 为每个集群 exec 配置保留的扩展名
extension:
arbitrary: config
this: 在设置 provideClusterInfo 时可通过环境变量 KUBERNETES_EXEC_INFO 指定
you: ["can", "put", "anything", "here"]
contexts:
- name: my-cluster
context:
cluster: my-cluster
user: my-user
current-context: my-cluster
apiVersion: v1
kind: Config
users:
- name: my-user
user:
exec:
# 要执行的命令。必需。
command: "example-client-go-exec-plugin"
# 解析 ExecCredentials 资源时使用的 API 版本。必需。
#
# 插件返回的 API 版本必需与这里列出的版本匹配。
#
# 要与支持多个版本的工具(如 client.authentication.k8sio/v1alpha1)集成,
# 可以设置一个环境变量或者向工具传递一个参数标明 exec 插件所期望的版本,
# 或者从 KUBERNETES_EXEC_INFO 环境变量的 ExecCredential 对象中读取版本信息。
apiVersion: "client.authentication.k8s.io/v1"
# 执行此插件时要设置的环境变量。可选字段。
env:
- name: "FOO"
value: "bar"
# 执行插件时要传递的参数。可选字段。
args:
- "arg1"
- "arg2"
# 当可执行文件不存在时显示给用户的文本。可选的。
installHint: |
需要 example-client-go-exec-plugin 来在当前集群上执行身份认证。可以通过以下命令安装:
MacOS: brew install example-client-go-exec-plugin
Ubuntu: apt-get install example-client-go-exec-plugin
Fedora: dnf install example-client-go-exec-plugin
...
# 是否使用 KUBERNETES_EXEC_INFO 环境变量的一部分向这个 exec 插件
# 提供集群信息(可能包含非常大的 CA 数据)
provideClusterInfo: true
# Exec 插件与标准输入 I/O 数据流之间的协议。如果协议无法满足,
# 则插件无法运行并会返回错误信息。合法的值包括 "Never" (Exec 插件从不使用标准输入),
# "IfAvailable" (Exec 插件希望在可以的情况下使用标准输入),
# 或者 "Always" (Exec 插件需要使用标准输入才能工作)。可选字段。
# 默认值为 "IfAvailable"。
interactiveMode: Never
clusters:
- name: my-cluster
cluster:
server: "https://172.17.4.100:6443"
certificate-authority: "/etc/kubernetes/ca.pem"
extensions:
- name: client.authentication.k8s.io/exec # 为每个集群 exec 配置保留的扩展名
extension:
arbitrary: config
this: 在设置 provideClusterInfo 时可通过环境变量 KUBERNETES_EXEC_INFO 指定
you: ["can", "put", "anything", "here"]
contexts:
- name: my-cluster
context:
cluster: my-cluster
user: my-user
current-context: my-cluster
解析相对命令路径时,kubectl 将其视为与配置文件比较而言的相对路径。 如果 KUBECONFIG 被设置为 /home/jane/kubeconfig
,而 exec 命令为 ./bin/example-client-go-exec-plugin
,则要执行的可执行文件为 /home/jane/bin/example-client-go-exec-plugin
。
- name: my-user
user:
exec:
# 对 kubeconfig 目录而言的相对路径
command: "./bin/example-client-go-exec-plugin"
apiVersion: "client.authentication.k8s.io/v1"
interactiveMode: Never
输出和输出格式
所执行的命令会在 stdout
打印 ExecCredential
对象。 k8s.io/client-go
使用 status
中返回的凭据信息向 Kubernetes API 服务器执行身份认证。 所执行的命令会通过环境变量 KUBERNETES_EXEC_INFO
收到一个 ExecCredential
对象作为其输入。 此输入中包含类似于所返回的 ExecCredential
对象的预期 API 版本, 以及是否插件可以使用 stdin
与用户交互这类信息。
在交互式会话(即,某终端)中运行时,stdin
是直接暴露给插件使用的。 插件应该使用来自 KUBERNETES_EXEC_INFO
环境变量的 ExecCredential
输入对象中的 spec.interactive
字段来确定是否提供了 stdin
。 插件的 stdin
需求(即,为了能够让插件成功运行,是否 stdin
是可选的、 必须提供的或者从不会被使用的)是通过 kubeconfig 中的 user.exec.interactiveMode
来声明的(参见下面的表格了解合法值)。 字段 user.exec.interactiveMode
在 client.authentication.k8s.io/v1beta1
中是可选的,在 client.authentication.k8s.io/v1
中是必需的。
interactiveMode 取值 | 含义 |
---|---|
Never | 此 exec 插件从不需要使用标准输入,因此如论是否有标准输入提供给用户输入,该 exec 插件都能运行。 |
IfAvailable | 此 exec 插件希望在标准输入可用的情况下使用标准输入,但在标准输入不存在时也可运行。因此,无论是否存在给用户提供输入的标准输入,此 exec 插件都会运行。如果存在供用户输入的标准输入,则该标准输入会被提供给 exec 插件。 |
Always | 此 exec 插件需要标准输入才能正常运行,因此只有存在供用户输入的标准输入时,此 exec 插件才会运行。如果不存在供用户输入的标准输入,则 exec 插件无法运行,并且 exec 插件的执行者会因此返回错误信息。 |
与使用持有者令牌凭据,插件在 ExecCredential 的状态中返回一个令牌:
{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"status": {
"token": "my-bearer-token"
}
}
{
"apiVersion": "client.authentication.k8s.io/v1beta1",
"kind": "ExecCredential",
"status": {
"token": "my-bearer-token"
}
}
另一种方案是,返回 PEM 编码的客户端证书和密钥,以便执行 TLS 客户端身份认证。 如果插件在后续调用中返回了不同的证书或密钥,k8s.io/client-go
会终止其与服务器的连接,从而强制执行新的 TLS 握手过程。
如果指定了这种方式,则 clientKeyData
和 clientCertificateData
字段都必需存在。
clientCertificateData
字段可能包含一些要发送给服务器的中间证书(Intermediate Certificates)。
{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"status": {
"clientCertificateData": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"clientKeyData": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}
}
{
"apiVersion": "client.authentication.k8s.io/v1beta1",
"kind": "ExecCredential",
"status": {
"clientCertificateData": "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
"clientKeyData": "-----BEGIN RSA PRIVATE KEY-----\n...\n-----END RSA PRIVATE KEY-----"
}
}
作为一种可选方案,响应中还可以包含以 RFC 3339 时间戳格式给出的证书到期时间。 证书到期时间的有无会有如下影响:
- 如果响应中包含了到期时间,持有者令牌和 TLS 凭据会被缓存,直到到期期限到来、 或者服务器返回 401 HTTP 状态码,或者进程退出。
如果未指定到期时间,则持有者令牌和 TLS 凭据会被缓存,直到服务器返回 401 HTTP 状态码或者进程退出。
- client.authentication.k8s.io/v1beta1
{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"status": {
"token": "my-bearer-token",
"expirationTimestamp": "2018-03-05T17:30:20-08:00"
}
}
{
"apiVersion": "client.authentication.k8s.io/v1beta1",
"kind": "ExecCredential",
"status": {
"token": "my-bearer-token",
"expirationTimestamp": "2018-03-05T17:30:20-08:00"
}
}
为了让 exec 插件能够获得特定与集群的信息,可以在 kubeconfig 中的 user.exec
设置 provideClusterInfo
。 这一特定于集群的信息就会通过 KUBERNETES_EXEC_INFO
环境变量传递给插件。 此环境变量中的信息可以用来执行特定于集群的凭据获取逻辑。 下面的 ExecCredential
清单描述的是一个示例集群信息。
{
"apiVersion": "client.authentication.k8s.io/v1",
"kind": "ExecCredential",
"spec": {
"cluster": {
"server": "https://172.17.4.100:6443",
"certificate-authority-data": "LS0t...",
"config": {
"arbitrary": "config",
"this": "可以在设置 provideClusterInfo 时通过 KUBERNETES_EXEC_INFO 环境变量提供",
"you": ["can", "put", "anything", "here"]
}
},
"interactive": true
}
}
{
"apiVersion": "client.authentication.k8s.io/v1beta1",
"kind": "ExecCredential",
"spec": {
"cluster": {
"server": "https://172.17.4.100:6443",
"certificate-authority-data": "LS0t...",
"config": {
"arbitrary": "config",
"this": "可以在设置 provideClusterInfo 时通过 KUBERNETES_EXEC_INFO 环境变量提供",
"you": ["can", "put", "anything", "here"]
}
},
"interactive": true
}
}
What’s next
最后修改 March 30, 2022 at 12:24 PM PST: [zh] Update authentication.md (1905b25b8)