APIServer是访问kubernetes集群资源的统一入口,每个请求在APIServer中都需要经过3个阶段才能访问到目标资源,分别是:认证、鉴权和准入控制。本文主要分析鉴权部分,kubernetes不仅支持多种鉴权方式,还支持同时开启多个鉴权模块,进行联合鉴权。
rbac.authorization.k8s.io
API 组来驱动鉴权决策,从而允许管理员通过 Kubernetes API 动态配置权限策略。--authorization-mode = RBAC
启动 API 服务器。Decision 决策状态类似于认证中的 true 和 false,用于决定是否鉴权成功。 鉴权支持三种 Decision 决策状态,例如鉴权成功,则返回 DecisionAllow,代码如下:
// 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
)
每个鉴权模块都要实现该接口的方法,代码如下:
// 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。
(1)kubernetes联合鉴权
每一种鉴权机制实例化后,成为一个鉴权模块,被封装在 http.Handler 函数中,他们接受组件或者客户端的请求并鉴权。 假设 kube-apiserver 启用了 鉴权模块 RBAC 和 Webhook 鉴权模块。
请求会进入 Authorization Handler 函数,该函数会遍历已经启用的鉴权模块列表,按照顺序执行每个鉴权模块,如果任何鉴权模块DecisionAllow或DecisionDeny请求,则立即返回该决定,并且不会与其他鉴权模块协商。
如在 RBAC 鉴权模块返回 DecisionNoOpinion 时,会继续执行Webhook 鉴权模块。 如果所有模块对请求鉴权都为DecisionNoOpinion,则拒绝该请求。
// 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/)
// 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/)
// 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请求内容的例子:
{
"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 将返回:
{
"apiVersion": "authorization.k8s.io/v1beta1",
"kind": "SubjectAccessReview",
"status": {
"allowed": false,
"reason": "user does not have read access to the namespace"
}
}
第二种方法立即拒绝其他配置的授权者进行短路评估。仅应由对集群的完整授权者配置有详细了解的 webhook 使用。webhook 将返回:
{
"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
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。