kubernetes/pkg/kubeapiserver/authorizer/reload.go

242 lines
9.3 KiB
Go

/*
Copyright 2024 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package authorizer
import (
"bytes"
"context"
"errors"
"fmt"
"os"
"reflect"
"sync"
"sync/atomic"
"time"
"k8s.io/apimachinery/pkg/util/sets"
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/authorization/authorizerfactory"
"k8s.io/apiserver/pkg/authorization/cel"
authorizationmetrics "k8s.io/apiserver/pkg/authorization/metrics"
"k8s.io/apiserver/pkg/authorization/union"
"k8s.io/apiserver/pkg/server/options/authorizationconfig/metrics"
webhookutil "k8s.io/apiserver/pkg/util/webhook"
"k8s.io/apiserver/plugin/pkg/authorizer/webhook"
webhookmetrics "k8s.io/apiserver/plugin/pkg/authorizer/webhook/metrics"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/auth/authorizer/abac"
"k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes"
"k8s.io/kubernetes/pkg/util/filesystem"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/node"
"k8s.io/kubernetes/plugin/pkg/auth/authorizer/rbac"
)
type reloadableAuthorizerResolver struct {
// initialConfig holds the ReloadFile used to initiate background reloading,
// and information used to construct webhooks that isn't exposed in the authorization
// configuration file (dial function, backoff settings, etc)
initialConfig Config
apiServerID string
reloadInterval time.Duration
requireNonWebhookTypes sets.Set[authzconfig.AuthorizerType]
nodeAuthorizer *node.NodeAuthorizer
rbacAuthorizer *rbac.RBACAuthorizer
abacAuthorizer abac.PolicyList
lastLoadedLock sync.Mutex
lastLoadedConfig *authzconfig.AuthorizationConfiguration
lastReadData []byte
current atomic.Pointer[authorizerResolver]
}
type authorizerResolver struct {
authorizer authorizer.Authorizer
ruleResolver authorizer.RuleResolver
}
func (r *reloadableAuthorizerResolver) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return r.current.Load().authorizer.Authorize(ctx, a)
}
func (r *reloadableAuthorizerResolver) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) {
return r.current.Load().ruleResolver.RulesFor(user, namespace)
}
// newForConfig constructs
func (r *reloadableAuthorizerResolver) newForConfig(authzConfig *authzconfig.AuthorizationConfiguration) (authorizer.Authorizer, authorizer.RuleResolver, error) {
if len(authzConfig.Authorizers) == 0 {
return nil, nil, fmt.Errorf("at least one authorization mode must be passed")
}
var (
authorizers []authorizer.Authorizer
ruleResolvers []authorizer.RuleResolver
)
// Add SystemPrivilegedGroup as an authorizing group
superuserAuthorizer := authorizerfactory.NewPrivilegedGroups(user.SystemPrivilegedGroup)
authorizers = append(authorizers, superuserAuthorizer)
for _, configuredAuthorizer := range authzConfig.Authorizers {
// Keep cases in sync with constant list in k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes/modes.go.
switch configuredAuthorizer.Type {
case authzconfig.AuthorizerType(modes.ModeNode):
if r.nodeAuthorizer == nil {
return nil, nil, fmt.Errorf("authorizer type Node is not allowed if it was not enabled at initial server startup")
}
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, r.nodeAuthorizer))
ruleResolvers = append(ruleResolvers, r.nodeAuthorizer)
case authzconfig.AuthorizerType(modes.ModeAlwaysAllow):
alwaysAllowAuthorizer := authorizerfactory.NewAlwaysAllowAuthorizer()
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, alwaysAllowAuthorizer))
ruleResolvers = append(ruleResolvers, alwaysAllowAuthorizer)
case authzconfig.AuthorizerType(modes.ModeAlwaysDeny):
alwaysDenyAuthorizer := authorizerfactory.NewAlwaysDenyAuthorizer()
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, alwaysDenyAuthorizer))
ruleResolvers = append(ruleResolvers, alwaysDenyAuthorizer)
case authzconfig.AuthorizerType(modes.ModeABAC):
if r.abacAuthorizer == nil {
return nil, nil, fmt.Errorf("authorizer type ABAC is not allowed if it was not enabled at initial server startup")
}
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, r.abacAuthorizer))
ruleResolvers = append(ruleResolvers, r.abacAuthorizer)
case authzconfig.AuthorizerType(modes.ModeWebhook):
if r.initialConfig.WebhookRetryBackoff == nil {
return nil, nil, errors.New("retry backoff parameters for authorization webhook has not been specified")
}
clientConfig, err := webhookutil.LoadKubeconfig(*configuredAuthorizer.Webhook.ConnectionInfo.KubeConfigFile, r.initialConfig.CustomDial)
if err != nil {
return nil, nil, err
}
var decisionOnError authorizer.Decision
switch configuredAuthorizer.Webhook.FailurePolicy {
case authzconfig.FailurePolicyNoOpinion:
decisionOnError = authorizer.DecisionNoOpinion
case authzconfig.FailurePolicyDeny:
decisionOnError = authorizer.DecisionDeny
default:
return nil, nil, fmt.Errorf("unknown failurePolicy %q", configuredAuthorizer.Webhook.FailurePolicy)
}
webhookAuthorizer, err := webhook.New(clientConfig,
configuredAuthorizer.Webhook.SubjectAccessReviewVersion,
configuredAuthorizer.Webhook.AuthorizedTTL.Duration,
configuredAuthorizer.Webhook.UnauthorizedTTL.Duration,
*r.initialConfig.WebhookRetryBackoff,
decisionOnError,
configuredAuthorizer.Webhook.MatchConditions,
configuredAuthorizer.Name,
kubeapiserverWebhookMetrics{WebhookMetrics: webhookmetrics.NewWebhookMetrics(), MatcherMetrics: cel.NewMatcherMetrics()},
)
if err != nil {
return nil, nil, err
}
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, webhookAuthorizer))
ruleResolvers = append(ruleResolvers, webhookAuthorizer)
case authzconfig.AuthorizerType(modes.ModeRBAC):
if r.rbacAuthorizer == nil {
return nil, nil, fmt.Errorf("authorizer type RBAC is not allowed if it was not enabled at initial server startup")
}
authorizers = append(authorizers, authorizationmetrics.InstrumentedAuthorizer(string(configuredAuthorizer.Type), configuredAuthorizer.Name, r.rbacAuthorizer))
ruleResolvers = append(ruleResolvers, r.rbacAuthorizer)
default:
return nil, nil, fmt.Errorf("unknown authorization mode %s specified", configuredAuthorizer.Type)
}
}
return union.New(authorizers...), union.NewRuleResolvers(ruleResolvers...), nil
}
type kubeapiserverWebhookMetrics struct {
// kube-apiserver doesn't report request metrics
webhookmetrics.NoopRequestMetrics
// kube-apiserver does report webhook metrics
webhookmetrics.WebhookMetrics
// kube-apiserver does report matchCondition metrics
cel.MatcherMetrics
}
// runReload starts checking the config file for changes and reloads the authorizer when it changes.
// Blocks until ctx is complete.
func (r *reloadableAuthorizerResolver) runReload(ctx context.Context) {
metrics.RegisterMetrics()
metrics.RecordAuthorizationConfigAutomaticReloadSuccess(r.apiServerID)
filesystem.WatchUntil(
ctx,
r.reloadInterval,
r.initialConfig.ReloadFile,
func() {
r.checkFile(ctx)
},
func(err error) {
klog.ErrorS(err, "watching authorization config file")
},
)
}
func (r *reloadableAuthorizerResolver) checkFile(ctx context.Context) {
r.lastLoadedLock.Lock()
defer r.lastLoadedLock.Unlock()
data, err := os.ReadFile(r.initialConfig.ReloadFile)
if err != nil {
klog.ErrorS(err, "reloading authorization config")
metrics.RecordAuthorizationConfigAutomaticReloadFailure(r.apiServerID)
return
}
if bytes.Equal(data, r.lastReadData) {
// no change
return
}
klog.InfoS("found new authorization config data")
r.lastReadData = data
config, err := LoadAndValidateData(data, r.requireNonWebhookTypes)
if err != nil {
klog.ErrorS(err, "reloading authorization config")
metrics.RecordAuthorizationConfigAutomaticReloadFailure(r.apiServerID)
return
}
if reflect.DeepEqual(config, r.lastLoadedConfig) {
// no change
return
}
klog.InfoS("found new authorization config")
r.lastLoadedConfig = config
authorizer, ruleResolver, err := r.newForConfig(config)
if err != nil {
klog.ErrorS(err, "reloading authorization config")
metrics.RecordAuthorizationConfigAutomaticReloadFailure(r.apiServerID)
return
}
klog.InfoS("constructed new authorizer")
r.current.Store(&authorizerResolver{
authorizer: authorizer,
ruleResolver: ruleResolver,
})
klog.InfoS("reloaded authz config")
metrics.RecordAuthorizationConfigAutomaticReloadSuccess(r.apiServerID)
}