前文分析了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
}