首先,对kubernetes API Server的访问控制流程进行初步认识,如下图所示:
APIServer是访问kubernetes集群资源的统一入口,每个请求在APIServer中都需要经过3个阶段才能访问到目标资源,分别是:认证、鉴权和准入控制。本文主要分析鉴权部分,kubernetes不仅支持多种鉴权方式,还支持同时开启多个鉴权模块,进行联合鉴权。
一、鉴权模块
- Node —— 一个专用鉴权模式,根据调度到 kubelet 上运行的 Pod 为 kubelet 授予权限。 具体请参阅节点鉴权。
- ABAC —— 基于属性的访问控制(ABAC)定义了一种访问控制范型,通过使用将属性组合在一起的策略, 将访问权限授予用户。策略可以使用任何类型的属性(用户属性、资源属性、对象,环境属性等)。 具体请参阅 ABAC 模式。
- RBAC —— 基于角色的访问控制(RBAC) 是一种基于企业内个人用户的角色来管理对计算机或网络资源的访问的方法。 在此上下文中,权限是单个用户执行特定任务的能力, 例如查看、创建或修改文件。具体请参阅 RBAC 模式。
- 被启用之后,RBAC(基于角色的访问控制)使用
rbac.authorization.k8s.io
API 组来驱动鉴权决策,从而允许管理员通过 Kubernetes API 动态配置权限策略。 - 要启用 RBAC,请使用
--authorization-mode = RBAC
启动 API 服务器。
- 被启用之后,RBAC(基于角色的访问控制)使用
- Webhook —— WebHook 是一个 HTTP 回调:发生某些事情时调用的 HTTP POST; 通过 HTTP POST 进行简单的事件通知。 实现 WebHook 的 Web 应用程序会在发生某些事情时将消息发布到 URL。 具体请参阅 Webhook 模式。
二、鉴权机制
1. Decision:决策状态
Decision 决策状态类似于认证中的 true 和 false,用于决定是否鉴权成功。 鉴权支持三种 Decision 决策状态,例如鉴权成功,则返回 DecisionAllow,代码如下:
代码语言:javascript复制// staging/src/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go
type Decision int
const (
// DecisionDeny means that an authorizer decided to deny the action.
DecisionDeny Decision = iota
// DecisionAllow means that an authorizer decided to allow the action.
DecisionAllow
// DecisionNoOpionion means that an authorizer has no opinion on whether
// to allow or deny an action.
DecisionNoOpinion
)
- DecisionDeny:拒绝该操作
- DecisionAllow:允许该操作
- DecisionNoOpinion:表示无意见,既不允许也不拒绝,可继续执行下一个鉴权模块
2. Authorizer:鉴权接口
每个鉴权模块都要实现该接口的方法,代码如下:
代码语言:javascript复制// staging/src/k8s.io/apiserver/pkg/authorization/authorizer/interfaces.go
type Authorizer interface {
Authorize(ctx context.Context, a Attributes) (authorized Decision, reason string, err error)
}
Authorizer 接口定义了 Authorize 方法,该方法接收一个 Attribute 参数。 Attributes 是决定鉴权模块从 HTTP 请求中获取鉴权信息方法的参数,它是一个方法集合的接口, 例如 GetUser、GetVerb、GetNamespace、GetResource 等鉴权信息方法。
如果鉴权成功,Decision 状态变成 DecisionAllow, 如果鉴权失败,Decision 状态变成 DecisionDeny,并返回失败的原因。被拒绝响应返回 HTTP 状态代码 403。
3. 当开启多个鉴权模块时,分析kubernetes的鉴权机制(以开启RBAC模式和webhook为例)
(1)kubernetes联合鉴权
每一种鉴权机制实例化后,成为一个鉴权模块,被封装在 http.Handler 函数中,他们接受组件或者客户端的请求并鉴权。 假设 kube-apiserver 启用了 鉴权模块 RBAC 和 Webhook 鉴权模块。
请求会进入 Authorization Handler 函数,该函数会遍历已经启用的鉴权模块列表,按照顺序执行每个鉴权模块,如果任何鉴权模块DecisionAllow或DecisionDeny请求,则立即返回该决定,并且不会与其他鉴权模块协商。
如在 RBAC 鉴权模块返回 DecisionNoOpinion 时,会继续执行Webhook 鉴权模块。 如果所有模块对请求鉴权都为DecisionNoOpinion,则拒绝该请求。
代码语言:javascript复制// staging/src/k8s.io/apiserver/pkg/authorization/union/union.go
func (authzHandler unionAuthzHandler) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
var (
errlist []error
reasonlist []string
)
for _, currAuthzHandler := range authzHandler {
decision, reason, err := currAuthzHandler.Authorize(ctx, a)
if err != nil {
errlist = append(errlist, err)
}
if len(reason) != 0 {
reasonlist = append(reasonlist, reason)
}
switch decision {
case authorizer.DecisionAllow, authorizer.DecisionDeny:
return decision, reason, err
case authorizer.DecisionNoOpinion:
// continue to the next authorizer
}
}
return authorizer.DecisionNoOpinion, strings.Join(reasonlist, "n"), utilerrors.NewAggregate(errlist)
}
(2)RBAC鉴权实现(RBAC鉴权简述:https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/rbac/)
代码语言:javascript复制// plugin/pkg/auth/authorizer/rbac/rbac.go
func (r *RBACAuthorizer) Authorize(ctx context.Context, requestAttributes authorizer.Attributes) (authorizer.Decision, string, error) {
ruleCheckingVisitor := &authorizingVisitor{requestAttributes: requestAttributes}
r.authorizationRuleResolver.VisitRulesFor(requestAttributes.GetUser(), requestAttributes.GetNamespace(), ruleCheckingVisitor.visit)
if ruleCheckingVisitor.allowed {
return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
}
// Build a detailed log of the denial.
reason := ""
if len(ruleCheckingVisitor.errors) > 0 {
reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors))
}
return authorizer.DecisionNoOpinion, reason, nil
}
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
if rule != nil && RuleAllows(v.requestAttributes, rule) {
v.allowed = true
v.reason = fmt.Sprintf("RBAC: allowed by %s", source.String())
return false
}
if err != nil {
v.errors = append(v.errors, err)
}
return true
}
func RulesAllow(requestAttributes authorizer.Attributes, rules ...rbacv1.PolicyRule) bool {
for i := range rules {
if RuleAllows(requestAttributes, &rules[i]) {
return true
}
}
return false
}
(3)Webhook鉴权实现(webhook鉴权简述:https://kubernetes.io/zh-cn/docs/reference/access-authn-authz/webhook/)
代码语言:javascript复制// staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go
func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attributes) (decision authorizer.Decision, reason string, err error) {
r := &authorizationv1.SubjectAccessReview{}
if user := attr.GetUser(); user != nil {
r.Spec = authorizationv1.SubjectAccessReviewSpec{
User: user.GetName(),
UID: user.GetUID(),
Groups: user.GetGroups(),
Extra: convertToSARExtra(user.GetExtra()),
}
}
if attr.IsResourceRequest() {
r.Spec.ResourceAttributes = &authorizationv1.ResourceAttributes{
Namespace: attr.GetNamespace(),
Verb: attr.GetVerb(),
Group: attr.GetAPIGroup(),
Version: attr.GetAPIVersion(),
Resource: attr.GetResource(),
Subresource: attr.GetSubresource(),
Name: attr.GetName(),
}
} else {
r.Spec.NonResourceAttributes = &authorizationv1.NonResourceAttributes{
Path: attr.GetPath(),
Verb: attr.GetVerb(),
}
}
key, err := json.Marshal(r.Spec)
if err != nil {
return w.decisionOnError, "", err
}
// 尝试从缓存中查找该请求
if entry, ok := w.responseCache.Get(string(key)); ok {
r.Status = entry.(authorizationv1.SubjectAccessReviewStatus)
} else {
var (
result *authorizationv1.SubjectAccessReview
err error
)
webhook.WithExponentialBackoff(ctx, w.retryBackoff, func() error {
// 首次请求 webhook
result, err = w.subjectAccessReview.Create(ctx, r, metav1.CreateOptions{})
return err
}, webhook.DefaultShouldRetry)
...
r.Status = result.Status
// 写缓存
if shouldCache(attr) {
if r.Status.Allowed {
w.responseCache.Add(string(key), r.Status, w.authorizedTTL)
} else {
w.responseCache.Add(string(key), r.Status, w.unauthorizedTTL)
}
}
}
switch {
case r.Status.Denied && r.Status.Allowed:
return authorizer.DecisionDeny, r.Status.Reason, fmt.Errorf("webhook subject access review returned both allow and deny response")
case r.Status.Denied:
return authorizer.DecisionDeny, r.Status.Reason, nil
case r.Status.Allowed:
return authorizer.DecisionAllow, r.Status.Reason, nil
default:
return authorizer.DecisionNoOpinion, r.Status.Reason, nil
}
}
(3.1)webhook请求内容的例子:
代码语言:javascript复制{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"spec": {
"resourceAttributes": {
"namespace": "kittensandponies",
"verb": "get",
"group": "unicorn.example.org",
"resource": "pods"
},
"user": "jane",
"group": [
"group1",
"group2"
]
}
}
期待远程服务填充请求的 status
字段并响应允许或禁止访问。响应主体的 spec
字段被忽略,可以省略。允许的响应将返回:
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": true
}
}
为了禁止访问,有两种方法。
在大多数情况下,第一种方法是首选方法,它指示授权 webhook 不允许或对请求 “无意见”。 但是,如果配置了其他授权者,则可以给他们机会允许请求。如果没有其他授权者,或者没有一个授权者,则该请求被禁止。webhook 将返回:
代码语言:javascript复制{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"reason": "user does not have read access to the namespace"
}
}
第二种方法立即拒绝其他配置的授权者进行短路评估。仅应由对集群的完整授权者配置有详细了解的 webhook 使用。webhook 将返回:
代码语言:javascript复制{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"denied": true,
"reason": "user does not have read access to the namespace"
}
}
(3.2)SubjectAccessReview鉴权接口:https://kubernetes.io/zh-cn/docs/reference/kubernetes-api/authorization-resources/subject-access-review-v1/#SubjectAccessReview