Kubernetes安全机制深度解析(一):从身份认证到资源鉴权
#作者:程宏斌
文章目录
- 用户认证(Authenticating)
- 身份认证策略
- X509 客户证书
- 静态令牌文件
- 启动引导令牌
- 服务账号令牌
- Webhook 令牌身份认证
- 鉴权(Authorization)
- 鉴权裁定
- 鉴权中使用的请求属性
- 请求动词和鉴权
- 资源请求
用户认证(Authenticating)
所有 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),且该用户属于 “app1” 和 “app2” 两个用户组。
静态令牌文件
当 API 服务器的命令行设置了 --token-auth-file=SOMEFILE 选项时,会从文件中读取持有者令牌。 目前,令牌会长期有效,并且在不重启 API 服务器的情况下无法更改令牌列表。
令牌文件是一个 CSV 文件,包含至少 3 个列:令牌、用户名和用户的 UID。 其余列被视为可选的组名。
说明:
如果要设置的组名不止一个,则对应的列必须用双引号括起来,例如:
token,user,uid,“group1,group2,group3”
在请求中放入持有者令牌
当使用持有者令牌来对某 HTTP 客户端执行身份认证时,API 服务器希望看到一个名为 Authorization 的 HTTP 头,其值格式为 Bearer 。 持有者令牌必须是一个可以放入 HTTP 头部值字段的字符序列,至多可使用 HTTP 的编码和引用机制。 例如:如果持有者令牌为 31ada4fd-adec-460c-809a-9e56ceb75269,则其出现在 HTTP 头部时如下所示:
Authorization: Bearer 31ada4fd-adec-460c-809a-9e56ceb75269
启动引导令牌
特性状态: 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 用来)构造合适的鉴权策略, 以完成启动引导新集群的工作。
服务账号令牌
服务账号(Service Account)是一种自动被启用的用户认证机制,使用经过签名的持有者令牌来验证请求。 该插件可接受两个可选参数:
–service-account-key-file 文件包含 PEM 编码的 x509 RSA 或 ECDSA 私钥或公钥, 用于验证 ServiceAccount 令牌。这样指定的文件可以包含多个密钥, 并且可以使用不同的文件多次指定此参数。若未指定,则使用 --tls-private-key-file 参数。
–service-account-lookup 如果启用,则从 API 删除的令牌会被回收。
服务账号通常由 API 服务器自动创建并通过 ServiceAccount 准入控制器关联到集群中运行的 Pod 上。 持有者令牌会挂载到 Pod 中可预知的位置,允许集群内进程与 API 服务器通信。 服务账号也可以使用 Pod 规约的 serviceAccountName 字段显式地关联到 Pod 上。
说明:
serviceAccountName 通常会被忽略,因为关联关系是自动建立的。
OpenID Connect(OIDC)令牌
OpenID Connect 是一种 OAuth2 认证方式, 被某些 OAuth2 提供者支持,例如 Microsoft Entra ID、Salesforce 和 Google。 协议对 OAuth2 的主要扩充体现在有一个附加字段会和访问令牌一起返回, 这一字段称作 ID Token(ID 令牌)。 ID 令牌是一种由服务器签名的 JWT 令牌,其中包含一些可预知的字段, 例如用户的邮箱地址,
要识别用户,身份认证组件使用 OAuth2 令牌响应中的 id_token(而非 access_token)作为持有者令牌。
配置 API 服务器
https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/authentication/#configuring-the-api-server
使用标志
https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/authentication/#%E4%BD%BF%E7%94%A8%E6%A0%87%E5%BF%97
要启用此插件,须在 API 服务器上配置以下标志:
参数 | 描述 | 示例 | 必需? |
---|---|---|---|
–oidc-issuer-url | 允许 API 服务器发现公开的签名密钥的服务的 URL。只接受模式为 https:// 的 URL。此值通常设置为服务的发现 URL,已更改为空路径。 | 如果发行人的 OIDC 发现 URL 是 https://accounts.google.com/.well-known/openid-configuration,则此值应为 https://accounts.provider.example | 是 |
–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 | 否 |
–oidc-signing-algs | 采纳的签名算法。默认为 “RS256”。可选值为:RS256、RS384、RS512、ES256、ES384、ES512、PS256、PS384、PS512。值由 RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1 定义。 | RS512 | 否 |
Webhook 令牌身份认证
Webhook 身份认证是一种用来验证持有者令牌的回调机制。
- –authentication-token-webhook-config-file 指向一个配置文件, 其中描述如何访问远程的 Webhook 服务。
- –authentication-token-webhook-cache-ttl 用来设定身份认证决定的缓存时间。 默认时长为 2 分钟。
- –authentication-token-webhook-version 决定是使用 authentication.k8s.io/v1beta1 还是 authenticationk8s.io/v1 版本的 TokenReview 对象从 Webhook 发送/接收信息。 默认为“v1beta1”。
配置文件使用 kubeconfig 文件的格式。文件中,clusters 指代远程服务,users 指代远程 API 服务 Webhook。
鉴权(Authorization)
Kubernetes 鉴权在身份认证之后进行。 通常,发出请求的客户端必须经过身份验证(登录)才能允许其请求; 但是,Kubernetes 在某些情况下也允许匿名请求。
鉴权裁定
Kubernetes 对 API 请求的鉴权在 API 服务器内进行。 API 服务器根据所有策略评估所有请求属性,可能还会咨询外部服务,然后允许或拒绝该请求。
API 请求的所有部分都必须通过某种鉴权机制才能继续, 换句话说:默认情况下拒绝访问。
说明:
依赖于特定对象种类的特定字段的访问控制和策略由准入控制器处理。
Kubernetes 准入控制发生在鉴权完成之后(因此,仅当鉴权决策是允许请求时)。
当系统配置了多个鉴权模块时,Kubernetes 将按顺序使用每个模块。 如果任何鉴权模块批准或拒绝请求,则立即返回该决定,并且不会与其他鉴权模块协商。 如果所有模块对请求没有意见,则拒绝该请求。 总体拒绝裁决意味着 API 服务器拒绝请求并以 HTTP 403(禁止)状态进行响应。
鉴权中使用的请求属性
Kubernetes 仅审查以下 API 请求属性:
- 用户 —— 身份验证期间提供的 user 字符串。
- 组 —— 经过身份验证的用户所属的组名列表。
- 额外信息 —— 由身份验证层提供的任意字符串键到字符串值的映射。
- API —— 指示请求是否针对 API 资源。
- 请求路径 —— 各种非资源端点的路径,如 /api 或 /healthz。
- API 请求动词 —— API 动词 get、list、create、update、patch、watch、 proxy、redirect、delete 和 deletecollection 用于资源请求。 要确定资源 API 端点的请求动词,请参阅请求动词和鉴权。
- HTTP 请求动词 —— HTTP 动词 get、post、put 和 delete 用于非资源请求。
- 资源 —— 正在访问的资源的 ID 或名称(仅限资源请求)- 对于使用 get、update、patch 和 delete 动词的资源请求,你必须提供资源名称。
- 子资源 —— 正在访问的子资源(仅限资源请求)。
- 名字空间 —— 正在访问的对象的名称空间(仅适用于名字空间资源请求)。
- API 组 —— 正在访问的 API 组 (仅限资源请求)。空字符串表示核心 API 组。
请求动词和鉴权
非资源请求
对于 /api/v1/… 或 /apis///… 之外的端点的请求被视为非资源请求(Non-Resource Requests), 并使用该请求的 HTTP 方法的小写形式作为其请求动词。
例如,对 /api 或 /healthz 这类端点的 GET 请求将使用 get 作为其动词。
资源请求
为了确定资源 API 端点的请求动词,Kubernetes 会映射所使用的 HTTP 动词, 并考虑该请求是否作用于单个资源或资源集合:
HTTP 动词 | 请求动词 |
---|---|
POST | create |
GET、HEAD | get(针对单个资源)、list(针对集合,包括完整的对象内容)、watch(用于查看单个资源或资源集合) |
PUT | update |
PATCH | patch |
DELETE | delete(针对单个资源)、deletecollection(针对集合) |
注意:
get、list 和 watch 动作都可以返回一个资源的完整详细信息。就返回的数据而言,它们是等价的。 例如,对 secrets 使用 list 仍然会显示所有已返回资源的 data 属性。
Kubernetes 有时使用专门的动词以对额外的权限进行鉴权。例如:
- 身份认证的特殊情况
对核心 API 组中 users、groups 和 serviceaccounts 以及 authentication.k8s.io API 组中的 userextras 所使用的 impersonate 动词。 - RBAC
对 rbac.authorization.k8s.io API 组中 roles 和 clusterroles 资源的 bind 和 escalate 动词