Kubernetes:kube-apiserver 之鑑權

lubanseven發表於2023-11-09

kubernetes:kube-apiserver 系列文章:

0. 前言

上一篇文章介紹了 kube-apiserver 的認證機制。這裡繼續往下走,介紹 kube-apiserver 的鑑權。kube-apiserver 處理認證和鑑權非常類似,建議閱讀鑑權機制前先看看 kube-apiserver認證

1. 鑑權 Authorization

1.1 Authorization handler

進入 DefaultBuildHandlerChainAuthorization handler 建立過程。

# kubernetes/vendor/k8s.io/apiserver/pkg/server/config.go
func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
	handler := apiHandler

	handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
	handler = filterlatency.TrackStarted(handler, c.TracerProvider, "authorization")

    handler = genericapifilters.WithAuthentication(handler, c.Authentication.Authenticator, failedHandler, c.Authentication.APIAudiences, c.Authentication.RequestHeaderConfig)
	handler = filterlatency.TrackStarted(handler, c.TracerProvider, "authentication")
}

這裡 handler chainhandler 處理順序是由下往上的,即處理完 authentication handler 在處理 authorization handler

進入 genericapifilters.WithAuthorization 檢視鑑權 handler 的建立過程。

# kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/filters/authorization.go
func WithAuthorization(hhandler http.Handler, auth authorizer.Authorizer, s runtime.NegotiatedSerializer) http.Handler {
	return withAuthorization(hhandler, auth, s, recordAuthorizationMetrics)
}

func withAuthorization(handler http.Handler, a authorizer.Authorizer, s runtime.NegotiatedSerializer, metrics recordAuthorizationMetricsFunc) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
		ctx := req.Context()
		authorizationStart := time.Now()

        // 獲取 request 攜帶的使用者資訊
		attributes, err := GetAuthorizerAttributes(ctx)
		if err != nil {
			responsewriters.InternalError(w, req, err)
			return
		}

        // 對使用者資訊進行鑑權
		authorized, reason, err := a.Authorize(ctx, attributes)

		...
	})
}

鑑權過程包括兩部分。

一部分透過 GetAuthorizerAttributes 獲取 RESTful API 請求中攜帶的使用者資訊。

# kubernetes/vendor/k8s.io/apiserver/pkg/endpoints/filters/authorization.go
func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) {
	attribs := authorizer.AttributesRecord{}

	user, ok := request.UserFrom(ctx)
	if ok {
		attribs.User = user
	}

	requestInfo, found := request.RequestInfoFrom(ctx)
	if !found {
		return nil, errors.New("no RequestInfo found in the context")
	}

	// Start with common attributes that apply to resource and non-resource requests
	attribs.ResourceRequest = requestInfo.IsResourceRequest
	attribs.Path = requestInfo.Path
	attribs.Verb = requestInfo.Verb

	attribs.APIGroup = requestInfo.APIGroup
	attribs.APIVersion = requestInfo.APIVersion
	attribs.Resource = requestInfo.Resource
	attribs.Subresource = requestInfo.Subresource
	attribs.Namespace = requestInfo.Namespace
	attribs.Name = requestInfo.Name

	return &attribs, nil
}

獲取到使用者資訊後透過 a.Authorize(ctx, attributes) 對使用者及其請求進行鑑權。其中 a 是實現了 Authorizer 鑑權器介面的例項。

type Authorizer interface {
	Authorize(ctx context.Context, a Attributes) (authorized Decision, reason string, err error)
}

檢視 a.Authorize(ctx, attributes) 實際是看 Config.Authorization.Authorizer 中的例項。

func DefaultBuildHandlerChain(apiHandler http.Handler, c *Config) http.Handler {
	handler = genericapifilters.WithAuthorization(handler, c.Authorization.Authorizer, c.Serializer)
}

1.2 Authorization authorizer

Config.Authorization.AuthorizerBuildGenericConfigBuildAuthorizer 函式內建立。

# kubernetes/pkg/controlplane/apiserver/config.go
func BuildGenericConfig(
	s controlplaneapiserver.CompletedOptions,
	schemes []*runtime.Scheme,
	getOpenAPIDefinitions func(ref openapicommon.ReferenceCallback) map[string]openapicommon.OpenAPIDefinition,
) (
	genericConfig *genericapiserver.Config,
	versionedInformers clientgoinformers.SharedInformerFactory,
	storageFactory *serverstorage.DefaultStorageFactory,

	lastErr error,
) {
    genericConfig.Authorization.Authorizer, genericConfig.RuleResolver, err = BuildAuthorizer(s, genericConfig.EgressSelector, versionedInformers)
	if err != nil {
		lastErr = fmt.Errorf("invalid authorization config: %v", err)
		return
	}
}

進入 BuildAuthorizer 檢視 Authorizer 是怎麼建立的。

# kubernetes/pkg/controlplane/apiserver/config.go
func BuildAuthorizer(s controlplaneapiserver.CompletedOptions, EgressSelector *egressselector.EgressSelector, versionedInformers clientgoinformers.SharedInformerFactory) (authorizer.Authorizer, authorizer.RuleResolver, error) {
	authorizationConfig := s.Authorization.ToAuthorizationConfig(versionedInformers)

	if EgressSelector != nil {
		egressDialer, err := EgressSelector.Lookup(egressselector.ControlPlane.AsNetworkContext())
		if err != nil {
			return nil, nil, err
		}
		authorizationConfig.CustomDial = egressDialer
	}

	return authorizationConfig.New()
}

建立 Authorizer 分為兩塊。首先建立 authorizationConfig,接著透過 authorizationConfig.New() 例項化 authorizer

# kubernetes/pkg/kubeapiserver/authorizer/config.go
func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, error) {
	var (
		authorizers   []authorizer.Authorizer
		ruleResolvers []authorizer.RuleResolver
	)

	for _, authorizationMode := range config.AuthorizationModes {
		switch authorizationMode {
		case modes.ModeNode:
			...
		case modes.ModeAlwaysAllow:
			...
		case modes.ModeAlwaysDeny:
            ...
		case modes.ModeABAC:
			...
		case modes.ModeWebhook:
			...
		case modes.ModeRBAC:
			...
		default:
			return nil, nil, fmt.Errorf("unknown authorization mode %s specified", authorizationMode)
		}
	}

	return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil
}

可以看到,authorizationConfig.New() 內根據 config.AuthorizationModes 確定需要建立的鑑權器型別。這裡有 modes.ModeNodemodes.ModeAlwaysAllowmodes.ModeAlwaysDenymodes.ModeABACmodes.ModeWebhookmodes.ModeRBAC 六種鑑權器。

config.AuthorizationModes 是在建立選項 NewOptions 中定義的,例項化過程如下:

func (o *BuiltInAuthorizationOptions) ToAuthorizationConfig(versionedInformerFactory versionedinformers.SharedInformerFactory) authorizer.Config {
	return authorizer.Config{
		AuthorizationModes:          o.Modes,
	}
}

# kubernetes/pkg/controlplane/apiserver/options/options.go
func NewOptions() *Options {
	s := Options{
		Authorization:           kubeoptions.NewBuiltInAuthorizationOptions()
	}

	return &s
}

# kubernetes/pkg/kubeapiserver/options/authorization.go
func NewBuiltInAuthorizationOptions() *BuiltInAuthorizationOptions {
	return &BuiltInAuthorizationOptions{
		Modes:                       []string{authzmodes.ModeAlwaysAllow},
		WebhookVersion:              "v1beta1",
		WebhookCacheAuthorizedTTL:   5 * time.Minute,
		WebhookCacheUnauthorizedTTL: 30 * time.Second,
		WebhookRetryBackoff:         genericoptions.DefaultAuthWebhookRetryBackoff(),
	}
}

這裡的 config.AuthorizationModesauthzmodes.ModeAlwaysAllow。那麼,將建立 alwaysAllowAuthorizer 鑑權器。

# kubernetes/pkg/kubeapiserver/authorizer/config.go
func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, error) {
	var (
		authorizers   []authorizer.Authorizer
		ruleResolvers []authorizer.RuleResolver
	)

	for _, authorizationMode := range config.AuthorizationModes {
		switch authorizationMode {
		case modes.ModeAlwaysAllow:
			alwaysAllowAuthorizer := authorizerfactory.NewAlwaysAllowAuthorizer()
			authorizers = append(authorizers, alwaysAllowAuthorizer)
			ruleResolvers = append(ruleResolvers, alwaysAllowAuthorizer)
        }
    }

    return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil
}

# kubernetes/vendor/k8s.io/apiserver/pkg/authorization/authorizerfactory/builtin.go
func NewAlwaysAllowAuthorizer() *alwaysAllowAuthorizer {
	return new(alwaysAllowAuthorizer)
}

type alwaysAllowAuthorizer struct{}

func (alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
	return authorizer.DecisionAllow, "", nil
}

func (alwaysAllowAuthorizer) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
	return []authorizer.ResourceRuleInfo{
			&authorizer.DefaultResourceRuleInfo{
				Verbs:     []string{"*"},
				APIGroups: []string{"*"},
				Resources: []string{"*"},
			},
		}, []authorizer.NonResourceRuleInfo{
			&authorizer.DefaultNonResourceRuleInfo{
				Verbs:           []string{"*"},
				NonResourceURLs: []string{"*"},
			},
		}, false, nil
}

alwaysAllowAuthorizer 鑑權器實現了 Authorizer 介面,其總是返回 authorizer.DecisionAllow

每種鑑權器透過 union.New 加到鑑權器組中。

func New(authorizationHandlers ...authorizer.Authorizer) authorizer.Authorizer {
	return unionAuthzHandler(authorizationHandlers)
}

// 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
	)

	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)
}

前面 handlera.Authorize(ctx, attributes) 實際執行的是鑑權器組 unionAuthzHandlerAuthorize 方法。在 unionAuthzHandler.Authorize 內遍歷執行每種鑑權器的 Authorize 方法,如果有一種鑑權器鑑權透過,則返回鑑權成功。如果鑑權器返回 authorizer.DecisionNoOpinion 則執行下一個鑑權器。如果鑑權器鑑權失敗則返回鑑權失敗。

1.3 authorization rules

前面介紹 alwaysAllowAuthorizer 鑑權器的時候我們看到 alwaysAllowAuthorizer.RulesFor 方法,該方法內定義了使用者可以訪問的 RESTful API 資源和非 RESTful API 資源。如 RESTful API 資源定義了訪問資源的動作 Verbs,資源組APIGroups 和資源 Resources

上例的 alwaysAllowAuthorizer 並沒有看出 RulesFor 的運用是因為 alwaysAllowAuthorizer 總是允許請求訪問 RESTful API 資源和非 RESTful API 資源。

我們以 rbacAuthorizer 鑑權器為例,看 RulesFor 是怎麼被用上的。

func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, error) {
	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.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)
        }
    }
}

func New(roles rbacregistryvalidation.RoleGetter, roleBindings rbacregistryvalidation.RoleBindingLister, clusterRoles rbacregistryvalidation.ClusterRoleGetter, clusterRoleBindings rbacregistryvalidation.ClusterRoleBindingLister) *RBACAuthorizer {
	authorizer := &RBACAuthorizer{
		authorizationRuleResolver: rbacregistryvalidation.NewDefaultRuleResolver(
			roles, roleBindings, clusterRoles, clusterRoleBindings,
		),
	}
	return authorizer
}

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
	}
}

可以看到,rbacAuthorizer 鑑權器的 Authorize 方法內的 r.authorizationRuleResolver.VisitRulesFor 結合使用者資訊和鑑權器的 rules 判斷鑑權是否透過。

2. 總結

透過本篇文章介紹了 kube-apiserver 中的 Authorization 鑑權流程,下一篇將繼續介紹 kube-apiserverAdimission 准入流程。


相關文章