2.3.2 认证
1. 认证中使用的用户
Kubernetes 集群中有两种用户:一种是由 Kubernetes 管理的 ServiceAccount ;另一种是普通用户。普通用户是由集群之外的服务管理的,比如存储在 Keystone 中的用户、存储在文件中的用户和存储在 ldap 中的用户等。 所有的 API 请求中都绑定了一个ServiceAccount 或普通用户,如果都未绑定,就是一个使用匿名用户的请求。ServiceAccount 与 它 关 联 的 请 求 中 的 用 户 是 什 么 样 的 呢? ServiceAccoount 在Kuberentes 中关联着一个 Secret,这个 Secret 中包括一个 JWT 格式的 Token,它会使用 Token 中的 Payload 中的 Sub 字段作为请求用户的名称。下面是对 Secret 中的 Token解析的一个示例,请求所使用的用户名是 system:serviceaccount:default:default,见代码清单 2-60。
代码清单 2-60
{ "iss": "kubernetes/serviceaccount", "kubernetes.io/serviceaccount/namespace": "default", "kubernetes.io/serviceaccount/secret.name": "default-token-bxzg6", "kubernetes.io/serviceaccount/service-account.name": "default", "kubernetes.io/serviceaccount/service-account.uid": "a96a30e3-3ee0-44cf- 99b2-1ad4bdbc7632", "sub": "system:serviceaccount:default:default" }
普通用户有多种方式携带认证信息去访问 APIServer。以客户端证书的方式为例,请求中如果携带了客户端证书,并且证书是由集群的 CA 证书(Certification Authority)签发的,那么这个请求的认证会通过,它会使用证书中的 Subject 字段作为用户的名称。代码清单 2-61 是一个证书的示例,示例中携带的用户名是 kcp (Subject: O=system:kcp,CN=kcp)。
代码清单 2-61
Certificate: Data: Version: 3 (0x2) Serial Number: 08:e2:5b:3a:2c:8c:6c:06:80:f8:aa:1b:81:76:93:4f Signature Algorithm: sha256WithRSAEncryption Issuer: CN=kubernetes Validity Not Before: Mar 3 05:59:42 2021 GMT Not After : Jul 11 09:37:38 2030 GMT Subject: O=system:kcp, CN=kcp ......
2. 认证策略
(1) X509 客户端证书认证
客户端证书认证是双向认证,服务器端需要验证客户端证书的正确性,客户端需要验证服务器端证书的正确性。客户端证书认证方式可以通过 Kube-Apiserver 的 --clientca-file 参数启用,它指向的文件用来验证提供给 API 服务器的客户端证书。当请求中携带的客户端证书通过了认证时,请求关联的用户名就是证书中的 Subject 字段的内容。我们可以使用代码清单 2-62 查看证书内容。
代码清单 2-62
openssl x509 -in < 证书 > -text -noout
(2)静态 Token 文件
静态 Token 文件认证方式可以通过 --token-auth-file 参数启用,在这个文件中存放着没有时间限制的 Bearer Token,如果想变更 Token,则必须要重启 APIServer。代码清单 2-63 是一个 Token 文件的示例,每一行包括 Token、用户名和用户 ID。
代码清单 2-63
JGaOWpJuyBL8NXmeA9V341JOCkHJbOTf,system:kubectl-kcpm1,system:kubectl-kcpm1
访问 APIServer 时在 HTTP 请求头上加入 Authorization 的请求头,值的格式是Bearer ,上面的 Token 见代码清单 2-64。
代码清单 2-64
curl -H "Authorization: Bearer JGaOWpJuyBL8NXmeA9V341JOCkHJbOTf" -k https://127.0.0.1:6443/
(3)引导 Token
我们在创建集群或是将新节点加入集群时,新节点上的组件要与 APIServer 进行通信(如Kubelet),但通信需要证书,手动签发证书比较麻烦。为了简化这个过程,Kubernetes 1.4 之后的版本会通过新节点发送请求的方式为新节点签发证书。而发送请求获取证书的请求时使用的 Bearer Token 叫作 Bootstrap Token,这种 Token 在 Kube-System Namespace 下有一个对应的 Secret。Controller-Manager 中有一个 TokenCleaner 的 Controller,它会将那些已经过期的 Bootstrap Token 掉。
Token 的 格 式 是 [a-z0-9]{6}.[a-z0-9]{16},Token 的 前 半 部 分 是 Token ID, 通过前半部分能找到 UI 应的 Secret ;第二部分是 Token Secret,保存在 Secret 中。以Kubeadm 创建的 tokencwb0ly.cqdj5l0k2qa19evv 为例,在 Kube-System 的 Namespace下会有一个名字为 bootstrap-token-cwb0ly 的 Secret,内容见代码清单 2-65。
代码清单 2-65
apiVersion: v1 data: auth-extra-groups: system:bootstrappers:kubeadm:default-node-token expiration: 2021-03-13T13:44:23+08:00 token-id: cwb0ly token-secret: cqdj5l0k2qa19evv usage-bootstrap-authentication: true usage-bootstrap-signing: true kind: Secret metadata: manager: kubeadm operation: Update name: bootstrap-token-cwb0ly namespace: kube-system
使用该 Token 作为 Authorization 请求头访问 APIServer,请求认证通过后关联到的用户名的格式是 bootstrap-token-,即 system:bootstrap:cwb0ly,请求示例见代码清单 2-66。
代码清单 2-66
curl -H "Authorization: Bearer cwb0ly.cqdj5l0k2qa19evv" -k https://127.0.0.1:6443/
要想使用引导 Token 认证,需要在 APIServer 启动的时候添加 --enable-bootstraptoken-auth 启动参数,如果想使用 Token Clear Controller,需要在 Controller-Manager的启动参数中添加 --controllers=*,tokencleaner。
(4) ServiceAccount Token
ServiceAccount 认证是自动开启的认证方式,它使用签过名的 Bearer Token 去验证请求,这个认证模块包括两个可配置项。
① --service-account-key-file:包含签名 Token 的 PEM 格式的密钥文件,如果不指定这个参数,将使用 APIServer 的 TLS 私钥。
② --service-account-lookup:如果被设置为 True,从 API 请求中删除的 Token 将被收回。ServiceAccount 由 APIServer 自动创建,Pod 在运行时通过 Admission Controller关联 ServiceAccount。Bearer Toen 挂载到 Pod 的特定目录上,并允许集群内的进程与APIServer 通信。 可以使用 PodSpec 的 ServiceAccountName 字段将账户与 Pod 进行关联。ServiceAccount 中包含一个 Secret,示例见代码清单 2-67。
代码清单 2-67
apiVersion: v1 kind: ServiceAccount metadata: name: default namespace: default resourceVersion: "361" secrets: - name: default-token-h29t7 # Secret 中包含了 APIServer 公开的 CA 证书和一个 JWT 格式的 Token apiVersion: v1 data: ca.crt: < 证书内容 > namespace: ZGVmYXVsdA== token: <JWT Token> kind: Secret metadata: name: default-token-h29t7 namespace: default type:
(5) OpenID Connect Tokens
OpenID Connect 是一套基于 OAuth2 协议的认证规范,由提供商实现,比如 Azure Active Directory、Salesforce 和 Google。这个认证模块的使用流程:用户先从认证服务器上获取一个 ID Token,这个 Token 是一个 JWT 格式的 Token,用户收到这个 Token 后访问 APIServer。
这个认证模块使用从 OAuth2 中获取的 id_token 进行认证,认证的过程如图 2-10 所示。
图 2-10 OIDC 认证流程
① 登录到用户身份认证服务提供商。
② 用户身份认证服务提供商返回 access_token、id_token 和 refresh_token。
③ 用户使用 Kubectl 工具时通过 --token 参数指定 id_token 或将它写入 kubeconfig 中。
④ Kubectl 将 id_token 作为认证信息放在请求头中调用 APIServer。
⑤ APIServer 将通过指定的证书检查 JWT 中的签名的正确性。
⑥ 检查 id_token 是不是已经过期了。
⑦ 确保用户请求的资源有操作权限。
⑧ 一旦鉴权通过,APIServer 将返回一个响应给 Kubectl。
⑨ Kubectl 工具箱用户提供反馈。
用来对用户身份进行认证的所有数据都在 id_token 中,在上述整个流程中 Kubernetes 不需要与身份认证服务交互。在一个都是无状态请求的模型中,这种工作方式为身份认证提供了一种更容易处理大规模请求的解决方案。
要使用 OIDC(OpenID Connect)认证模块,需要在 APIServer 中配置如下参数。
① --oidc-issuer-url: 认证服务提供商的地址,允许 APIServer 发现公开的签名密钥服务的 URL。只接受 https:// 的 URL。此值通常设置为服务的发现 URL,不含路径。
② --oidc-client-id:发放 Token 的 Client ID。
③ --oidc-username-claim:使用 JWT 中的哪个字段作为用户名,默认的是 sub。
④ --oidc-username-prefix:为了防止不同认证系统的用户冲突,给用户名添加一个前缀,如果使用的用户名不是 email,那么用户名将是 #。如果设置为“-”,将不会使用前缀。
⑤ --oidc-groups-claim: 使用 JWT 中的哪个字段作为用户的组名。
⑥ --oidc-groups-prefix:组名的前缀,所有的组都将以此值为前缀,以避免与其他身份认证策略发生冲突。
⑦ --oidc-required-claim: 键值对描述 ID Token 中的必要声明,如果设置了这个值,则验证声明是否存在于 ID Token 中且具有匹配值,重复设置可以指定多个声明。
⑧ --oidc-ca-file:签署身份认证提供商的 CA 证书的路径,默认的是主机的根 CA 证书的路径。
OIDC 认证实现见代码清单 2-68。
代码清单 2-68
func (v *IDTokenVerifier) Verify(ctx context.Context, rawIDToken string) (*IDToken, error) { jws, err := jose.ParseSigned(rawIDToken) if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %v", err) } // 解析 JWT 的 Payload 部分,JWT 分为三段,以逗号作为分隔符,第二段是 Payload 部分, 是 JSON 格式,使用 base64 进行编码 payload, err := parseJWT(rawIDToken) if err != nil { return nil, fmt.Errorf("oidc: malformed jwt: %v", err) } var token idToken if err := json.Unmarshal(payload, &token); err != nil { return nil, fmt.Errorf("oidc: failed to unmarshal claims: %v", err) } distributedClaims := make(map[string]claimSource) for cn, src := range token.ClaimNames { if src == "" { return nil, fmt.Errorf("oidc: failed to obtain source from claim name") } s, ok := token.ClaimSources[src] if !ok { return nil, fmt.Errorf("oidc: source does not exist") } distributedClaims[cn] = s } t := &IDToken{ Issuer: token.Issuer, Subject: token.Subject, Audience: []string(token.Audience), Expiry: time.Time(token.Expiry), IssuedAt: time.Time(token.IssuedAt), Nonce: token.Nonce, AccessTokenHash: token.AtHash, claims: payload, distributedClaims: distributedClaims, } ...... // 检查 Token 是否过期 if !v.config.SkipExpiryCheck { now := time.Now if v.config.Now != nil { now = v.config.Now } nowTime := now() if t.Expiry.Before(nowTime) { return nil, fmt.Errorf("oidc: token is expired (Token Expiry: %v)", t.Expiry) } if token.NotBefore != nil { nbfTime := time.Time(*token.NotBefore) leeway := 1 * time.Minute if nowTime.Add(leeway).Before(nbfTime) { return nil, fmt.Errorf("oidc: current time %v before the nbf (not before) time: %v", nowTime, nbfTime) } } } switch len(jws.Signatures) { case 0: return nil, fmt.Errorf("oidc: id token not signed") case 1: default: return nil, fmt.Errorf("oidc: multiple signatures on id token not supported") } sig := jws.Signatures[0] supportedSigAlgs := v.config.SupportedSigningAlgs if len(supportedSigAlgs) == 0 { supportedSigAlgs = []string{RS256} } ....... t.sigAlgorithm = sig.Header.Algorithm // 校验签名是否正确 gotPayload, err := v.keySet.VerifySignature(ctx, rawIDToken) if err != nil { return nil, fmt.Errorf("failed to verify signature: %v", err) } if !bytes.Equal(gotPayload, payload) { return nil, errors.New("oidc: internal error, payload parsed did not match previous payload") } return t, nil }
(6) WebHook Token 认证
WebHook 认证就是一种回调机制,用来验证 Bearer Token 的正确性,要使用这种认证方式需要配置如下参数。
① --authentication-token-webhook-config-file:这是一个配置文件,用于描述如何访问远程的 WebHook 服务。
② --authentication-token-webhook-cache-ttl:缓存认证时间,默认是 2 分钟。
③ --authentication-token-webhook-version:使用哪个版本发送和接收 WebHook的消息,TokenReview 可以使用 authentication.k8s.io/v1beta1 或 authentication.k8s.io/v1,默认的是 authentication.k8s.io/v1beta1。
当客户端使用一个 Bearer Token 去访问 APIServer 时,WebHook 认证模块会使用Token Review 对象的 JSON 格式向远端服务器发送请求,这个对象中包含了 Token。远端服务器为返回给 TokenReview 对象的 Status 字段填充内容,内容包含此次请求认证是否通过。
(7)认证代理
可以为 Kubernetes 设置一个认证代理,这个认证代理将信息放在请求头中发送给APIServer,APIServer 从请求头中识别用户。启用认证代理需要设置以下几个参数。
① --requestheader-username-headers:用于指定用户名列表,不区分大小写,按照顺序检查用户身份。
② --requestheader-group-headers:用于指定组列表,不区分大小写,按照顺序检查用户组的名称。
③ --requestheader-extra-headers-prefix:指定额外的列表,不区分大小写。
④ --requestheader-client-ca-file:指定有效的客户端的证书,在检查请求头中的用户名之前,必须在指定的文件中提供有效的客户端证书并针对证书颁发机构进行验证。为防止请求头攻击,在检查请求头之前,代理客户端要为 APIServer 提供有效的客户端证书进行校验。
(8)匿名认证
启用后,未被其他配置身份验证方模块拒绝的请求将被视为匿名请求,并被赋予用户名 system:anonymous 和组 system:unauthenticated。在配置了 Token 身份验证且启用了匿名访问的服务器上,提供无效 Bearer Token 的请求将收到 401 未经授权错误。而不提供 Bearer Token 的请求将被视为匿名请求。
在 Kubernetes 1.6 之后的版本,如果鉴权模式不是 AlwaysAllow,则匿名访问默认是启用的。