k8s 鉴权机制源码分析

发布于:2022-12-03 ⋅ 阅读:(607) ⋅ 点赞:(0)

前文分析了k8s的认证机制,只有通过认证的请求才能继续后面的处理,而且会将此请求所属的user信息保存到请求上下文中,本文要说的鉴权机制就会根据user信息来判断是否允许请求通过,具体的可查看官网

如何配置
kube-apiserver进程启动时,通过配置选项–authorization-mode来使能鉴权,可选参数为:AlwaysAllow,AlwaysDeny,ABAC,Webhook,RBAC和Node,可同时配置多个鉴权方法,在处理请求时只要有一个鉴权成功即可。

命令行选项
命令行选项初始化如下所示

//pkg/kubeapiserver/options/authorization.go
// AddFlags returns flags of authorization for a API Server
func (o *BuiltInAuthorizationOptions) AddFlags(fs *pflag.FlagSet) {
	//var AuthorizationModeChoices = []string{ModeAlwaysAllow, ModeAlwaysDeny, ModeABAC, ModeWebhook, ModeRBAC, ModeNode}
	fs.StringSliceVar(&o.Modes, "authorization-mode", o.Modes, ""+
		"Ordered list of plug-ins to do authorization on secure port. Comma-delimited list of: "+
		strings.Join(authzmodes.AuthorizationModeChoices, ",")+".")

	//指定策略文件,只在ABAC下使用
	fs.StringVar(&o.PolicyFile, "authorization-policy-file", o.PolicyFile, ""+
		"File with authorization policy in json line by line format, used with --authorization-mode=ABAC, on the secure port.")
	...
}

命令行传递的参数最终保存到结构体BuiltInAuthorizationOptions中

// BuiltInAuthorizationOptions contains all build-in authorization options for API Server
type BuiltInAuthorizationOptions struct {
	Modes                       []string
	PolicyFile                  string
	...
}

命令行选项默认值,可看到默认鉴权方法为ModeAlwaysAllow

// NewBuiltInAuthorizationOptions create a BuiltInAuthorizationOptions with default value
func NewBuiltInAuthorizationOptions() *BuiltInAuthorizationOptions {
	return &BuiltInAuthorizationOptions{
		Modes:                       []string{authzmodes.ModeAlwaysAllow},
		...
	}
}

鉴权模块初始化

//cmd/kube-apiserver/app/server.go
buildGenericConfig(...)
	//创建Authorizer
	genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, err = BuildAuthorizer(s, genericConfig.EgressSelector, versionedInformers)

// BuildAuthorizer constructs the authorizer
func BuildAuthorizer(s *options.ServerRunOptions, EgressSelector *egressselector.EgressSelector, versionedInformers clientgoinformers.SharedInformerFactory) (authorizer.Authorizer, authorizer.RuleResolver, error) {
	//将命令行选项保存到authorizationConfig
	authorizationConfig := s.Authorization.ToAuthorizationConfig(versionedInformers)
	...
	//根据authorizationConfig配置初始化鉴权模块
	return authorizationConfig.New()

遍历配置的鉴权模块AuthorizationModes进行初始化

//pkg/kubeapiserver/authorizer/config.go
// New returns the right sort of union of multiple authorizer.Authorizer objects
// based on the authorizationMode or an error.
func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, error) {
	if len(config.AuthorizationModes) == 0 {
		return nil, nil, fmt.Errorf("at least one authorization mode must be passed")
	}

	var (
		authorizers   []authorizer.Authorizer
		ruleResolvers []authorizer.RuleResolver
	)

	for _, authorizationMode := range config.AuthorizationModes {
		// Keep cases in sync with constant list in k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes/modes.go.
		switch authorizationMode {
		...
		case modes.ModeAlwaysAllow:
			alwaysAllowAuthorizer := authorizerfactory.NewAlwaysAllowAuthorizer()
			authorizers = append(authorizers, alwaysAllowAuthorizer)
			ruleResolvers = append(ruleResolvers, alwaysAllowAuthorizer)
		case modes.ModeAlwaysDeny:
			alwaysDenyAuthorizer := authorizerfactory.NewAlwaysDenyAuthorizer()
			authorizers = append(authorizers, alwaysDenyAuthorizer)
			ruleResolvers = append(ruleResolvers, alwaysDenyAuthorizer)
		case modes.ModeABAC:
			//读取策略配置文件
			abacAuthorizer, err := abac.NewFromFile(config.PolicyFile)
			authorizers = append(authorizers, abacAuthorizer)
			ruleResolvers = append(ruleResolvers, abacAuthorizer)
		...
		case modes.ModeRBAC:
			rbacAuthorizer := rbac.New(
				&rbac.RoleGetter{Lister: config.VersionedInformerFactory.Rbac().V1().Roles().Lister()},
				&rbac.RoleBindingLister{Lister: config.VersionedInformerFactory.Rbac().V1().RoleBindings().Lister()},
				&rbac.ClusterRoleGetter{Lister: config.VersionedInformerFactory.Rbac().V1().ClusterRoles().Lister()},
				&rbac.ClusterRoleBindingLister{Lister: config.VersionedInformerFactory.Rbac().V1().ClusterRoleBindings().Lister()},
			)
			authorizers = append(authorizers, rbacAuthorizer)
			ruleResolvers = append(ruleResolvers, rbacAuthorizer)
		default:
			return nil, nil, fmt.Errorf("unknown authorization mode %s specified", authorizationMode)
		}
	}

	//将配置的authorizers统一到unionAuthzHandler中
	return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil
}

上述过程总结如下图
在这里插入图片描述
Authorizer是一个接口类型,每个鉴权模块必须实现此接口。直接实现此接口的是结构体unionAuthzHandler,它的成员变量是authorizer.Authorizer类型的数组,每个数组元素保存一个鉴权模块。

执行鉴权
处理http请求流程如下,鉴权流程是在FullHandlerChain中完成的

aggregatorserver FullHandlerChain -> aggregatorserver director -> apiserver director -> extensionserver director -> NotFound

鉴权处理函数是在DefaultBuildHandlerChain中注册,其为WithAuthorization的返回值http.HandlerFunc```

DefaultBuildHandlerChain()
	handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)

// WithAuthorizationCheck passes all authorized requests on to handler, and returns a forbidden error otherwise.
func WithAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler {
	if a == nil {
		klog.Warning("Authorization is disabled")
		return handler
	}
	//鉴权处理函数
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		ae := request.AuditEventFrom(ctx)
		//将请求相关属性保存到attributes中,比如请求的path,verb,apigroup,version等信息
		attributes, err := GetAuthorizerAttributes(ctx)
		...
		//调用unionAuthzHandler.Authorize执行鉴权处理流程,见后面注释
		authorized, reason, err := a.Authorize(ctx, attributes)
		// an authorizer like RBAC could encounter evaluation errors and still allow the request, so authorizer decision is checked before error here.
		//鉴权成功,调用下一个handler
		if authorized == authorizer.DecisionAllow {
			audit.LogAnnotation(ae, decisionAnnotationKey, decisionAllow)
			audit.LogAnnotation(ae, reasonAnnotationKey, reason)
			handler.ServeHTTP(w, req)
			return
		}
		...
		//鉴权失败,返回forbidden
		klog.V(4).InfoS("Forbidden", "URI", req.RequestURI, "Reason", reason)
		audit.LogAnnotation(ae, decisionAnnotationKey, decisionForbid)
		audit.LogAnnotation(ae, reasonAnnotationKey, reason)
		responsewriters.Forbidden(ctx, attributes, w, req, reason, s)
	})
}

鉴权模块处理完后返回值只能是如下值

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
)

遍历所有的鉴权插件,执行其Authorize,有一个成功就返回成功

//k8s.io/apiserver/pkg/authorization/union/unios.go: Authorize
// Authorizes against a chain of authorizer.Authorizer objects and returns nil if successful and returns error if unsuccessful
func (authzHandler unionAuthzHandler) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
	var (
		errlist    []error
		reasonlist []string
	)
	//遍历所有的鉴权插件,执行其Authorize,有一个鉴权成功就返回,鉴权成功意味着结果可能为允许或者拒绝,不能为DecisionNoOpinion
	for _, currAuthzHandler := range authzHandler {
		decision, reason, err := currAuthzHandler.Authorize(ctx, a)
		...
		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)
}

下面看几个鉴权模块的实现。
a. alwaysAllow
可看到对于alwaysAllow鉴权,直接返回DecisionAllow

//k8s.io/apiserver/pkg/authorization/authorizerfactory/buildin.go
func (alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
	return authorizer.DecisionAllow, "", nil
}

b. abac基于属性的访问控制
遍历策略配置,如果请求匹配到策略,则返回DecisionAllow,否则返回DecisionNoOpinion。如果想修改策略配置,需要重启apiserver重启读取配置文件。

//pkg/auth/authorizer/abac/abac.go
// Authorize implements authorizer.Authorize
func (pl PolicyList) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
	for _, p := range pl {
		if matches(*p, a) {
			return authorizer.DecisionAllow, "", nil
		}
	}
	return authorizer.DecisionNoOpinion, "No policy matched.", nil
	// TODO: Benchmark how much time policy matching takes with a medium size
	// policy file, compared to other steps such as encoding/decoding.
	// Then, add Caching only if needed.
}

c. rbac基于角色的访问控制
rbac是目前用的最广泛的鉴权方式,其可restful api动态修改。

//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
	}
	...
	reason := ""
	if len(ruleCheckingVisitor.errors) > 0 {
		reason = fmt.Sprintf("RBAC: %v", utilerrors.NewAggregate(ruleCheckingVisitor.errors))
	}
	return authorizer.DecisionNoOpinion, reason, nil
}

//pkg/registry/rbac/validation/rule.go
func (r *DefaultRuleResolver) VisitRulesFor(user user.Info, namespace string, visitor func(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool) {
	//获取ClusterRoleBinding列表
	//不管请求是不是基于namespace,都要先判断基于集群的规则是否允许请求通过
	if clusterRoleBindings, err := r.clusterRoleBindingLister.ListClusterRoleBindings(); err != nil {
		if !visitor(nil, nil, err) {
			return
		}
	} else {
		sourceDescriber := &clusterRoleBindingDescriber{}
		//遍历ClusterRoleBinding
		for _, clusterRoleBinding := range clusterRoleBindings {
			//判断user是否属于Subjects,分三种情况
			//a. 如果Subjects为user类型,则直接判断user是否等于Subjects.Name
			//b. 如果Subjects为group类型,则判断user的group是否等于Subjects.Name
			//c. 如果Subjects为ServiceAccount类型,则判断user是否等于Subjects.Name,user的格式为system:serviceaccount:namespace:name
			subjectIndex, applies := appliesTo(user, clusterRoleBinding.Subjects, "")
			if !applies {
				continue
			}
			//获取clusterRoleBinding指定的clusterRole
			rules, err := r.GetRoleReferenceRules(clusterRoleBinding.RoleRef, "")
			if err != nil {
				if !visitor(nil, nil, err) {
					return
				}
				continue
			}
			sourceDescriber.binding = clusterRoleBinding
			sourceDescriber.subject = &clusterRoleBinding.Subjects[subjectIndex]
			//遍历规则进行匹配,如果匹配上visitor设置v.allowed为true,并返回false
			for i := range rules {
				if !visitor(sourceDescriber, &rules[i], nil) {
					return
				}
			}
		}
	}
	//如果请求是基于namespace的,则获取roleBinding
	if len(namespace) > 0 {
		if roleBindings, err := r.roleBindingLister.ListRoleBindings(namespace); err != nil {
			if !visitor(nil, nil, err) {
				return
			}
		} else {
			sourceDescriber := &roleBindingDescriber{}
			for _, roleBinding := range roleBindings {
				//判断user是否属于Subjects,同上
				subjectIndex, applies := appliesTo(user, roleBinding.Subjects, namespace)
				if !applies {
					continue
				}
				//获取roleBinding指定的role
				rules, err := r.GetRoleReferenceRules(roleBinding.RoleRef, namespace)
				if err != nil {
					if !visitor(nil, nil, err) {
						return
					}
					continue
				}
				sourceDescriber.binding = roleBinding
				sourceDescriber.subject = &roleBinding.Subjects[subjectIndex]
				//遍历规则进行匹配,如果匹配上visitor设置v.allowed为true,并返回false
				for i := range rules {
					if !visitor(sourceDescriber, &rules[i], nil) {
						return
					}
				}
			}
		}
	}
}

visit用来进行具体的规则匹配

//plugin/pkg/auth/authorizer/rbac/rbac.go
func (v *authorizingVisitor) visit(source fmt.Stringer, rule *rbacv1.PolicyRule, err error) bool {
	if rule != nil && RuleAllows(v.requestAttributes, rule) {
		//匹配成功,设置allowed为true
		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
}

func RuleAllows(requestAttributes authorizer.Attributes, rule *rbacv1.PolicyRule) bool {
	//如果是对资源的请求,比如/api/v1/nodes
	if requestAttributes.IsResourceRequest() {
		combinedResource := requestAttributes.GetResource()
		if len(requestAttributes.GetSubresource()) > 0 {
			combinedResource = requestAttributes.GetResource() + "/" + requestAttributes.GetSubresource()
		}
		//判断verb是否允许
		//判断apigroup是否允许
		//判断资源是否允许
		//判断资源名字是否允许
		return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
			rbacv1helpers.APIGroupMatches(rule, requestAttributes.GetAPIGroup()) &&
			rbacv1helpers.ResourceMatches(rule, combinedResource, requestAttributes.GetSubresource()) &&
			rbacv1helpers.ResourceNameMatches(rule, requestAttributes.GetName())
	}
	//对非资源的请求,比如/healthz
	return rbacv1helpers.VerbMatches(rule, requestAttributes.GetVerb()) &&
		rbacv1helpers.NonResourceURLMatches(rule, requestAttributes.GetPath())
}

对verb的匹配

//pkg/apis/rbac/v1/evaluation_helpers.go
func VerbMatches(rule *rbacv1.PolicyRule, requestedVerb string) bool {
	for _, ruleVerb := range rule.Verbs {
		//如果ruleVerb为*,则表示允许所有verb
		if ruleVerb == rbacv1.VerbAll {
			return true
		}
		if ruleVerb == requestedVerb {
			return true
		}
	}

	return false
}

对apigroup匹配

func APIGroupMatches(rule *rbacv1.PolicyRule, requestedGroup string) bool {
	for _, ruleGroup := range rule.APIGroups {
		//如果ruleGroup为*,则表示允许所有apigroup
		if ruleGroup == rbacv1.APIGroupAll {
			return true
		}
		if ruleGroup == requestedGroup {
			return true
		}
	}

	return false
}
本文含有隐藏内容,请 开通VIP 后查看