diff --git a/cmd/kube-apiserver/app/options/options_test.go b/cmd/kube-apiserver/app/options/options_test.go index 2fa5da605a6..41fd17523ce 100644 --- a/cmd/kube-apiserver/app/options/options_test.go +++ b/cmd/kube-apiserver/app/options/options_test.go @@ -243,9 +243,7 @@ func TestAddFlags(t *testing.T) { EnableContentionProfiling: true, }, Authentication: &kubeoptions.BuiltInAuthenticationOptions{ - Anonymous: &kubeoptions.AnonymousAuthenticationOptions{ - Allow: false, - }, + Anonymous: s.Authentication.Anonymous, ClientCert: &apiserveroptions.ClientCertAuthenticationOptions{ ClientCA: "/client-ca", }, @@ -335,6 +333,6 @@ func TestAddFlags(t *testing.T) { s.Authorization.AreLegacyFlagsSet = nil if !reflect.DeepEqual(expected, s) { - t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}))) + t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}, kubeoptions.AnonymousAuthenticationOptions{}))) } } diff --git a/cmd/kube-controller-manager/app/options/options_test.go b/cmd/kube-controller-manager/app/options/options_test.go index 8691daef9ff..6c5ba237cca 100644 --- a/cmd/kube-controller-manager/app/options/options_test.go +++ b/cmd/kube-controller-manager/app/options/options_test.go @@ -30,6 +30,7 @@ import ( eventv1 "k8s.io/api/events/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/apis/apiserver" apiserveroptions "k8s.io/apiserver/pkg/server/options" cpconfig "k8s.io/cloud-provider/config" serviceconfig "k8s.io/cloud-provider/controllers/service/config" @@ -430,6 +431,7 @@ func TestAddFlags(t *testing.T) { ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, RemoteKubeConfigFileOptional: true, + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: true}, }, Authorization: &apiserveroptions.DelegatingAuthorizationOptions{ AllowCacheTTL: 10 * time.Second, diff --git a/cmd/kubelet/app/auth.go b/cmd/kubelet/app/auth.go index fcb8afe1946..b5117a72c81 100644 --- a/cmd/kubelet/app/auth.go +++ b/cmd/kubelet/app/auth.go @@ -23,6 +23,7 @@ import ( "reflect" "k8s.io/apimachinery/pkg/types" + "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticatorfactory" "k8s.io/apiserver/pkg/authorization/authorizer" @@ -77,7 +78,7 @@ func BuildAuthn(client authenticationclient.AuthenticationV1Interface, authn kub } authenticatorConfig := authenticatorfactory.DelegatingAuthenticatorConfig{ - Anonymous: authn.Anonymous.Enabled, + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: authn.Anonymous.Enabled}, CacheTTL: authn.Webhook.CacheTTL.Duration, ClientCertificateCAContentProvider: dynamicCAContentFromFile, } diff --git a/pkg/controlplane/apiserver/options/options_test.go b/pkg/controlplane/apiserver/options/options_test.go index 88fe5b961dd..2e3f62c8f3c 100644 --- a/pkg/controlplane/apiserver/options/options_test.go +++ b/pkg/controlplane/apiserver/options/options_test.go @@ -35,9 +35,8 @@ import ( cliflag "k8s.io/component-base/cli/flag" "k8s.io/component-base/logs" "k8s.io/component-base/metrics" - netutils "k8s.io/utils/net" - kubeoptions "k8s.io/kubernetes/pkg/kubeapiserver/options" + netutils "k8s.io/utils/net" ) func TestAddFlags(t *testing.T) { @@ -229,9 +228,7 @@ func TestAddFlags(t *testing.T) { EnableContentionProfiling: true, }, Authentication: &kubeoptions.BuiltInAuthenticationOptions{ - Anonymous: &kubeoptions.AnonymousAuthenticationOptions{ - Allow: false, - }, + Anonymous: s.Authentication.Anonymous, ClientCert: &apiserveroptions.ClientCertAuthenticationOptions{ ClientCA: "/client-ca", }, @@ -290,6 +287,6 @@ func TestAddFlags(t *testing.T) { s.Authorization.AreLegacyFlagsSet = nil if !reflect.DeepEqual(expected, s) { - t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}))) + t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}, kubeoptions.AnonymousAuthenticationOptions{}))) } } diff --git a/pkg/features/kube_features.go b/pkg/features/kube_features.go index a83aa2a27f3..03ae1357c12 100644 --- a/pkg/features/kube_features.go +++ b/pkg/features/kube_features.go @@ -1203,6 +1203,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS genericfeatures.AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.33 + genericfeatures.AnonymousAuthConfigurableEndpoints: {Default: false, PreRelease: featuregate.Alpha}, + genericfeatures.APIListChunking: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.32 genericfeatures.APIPriorityAndFairness: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.31 diff --git a/pkg/kubeapiserver/authenticator/config.go b/pkg/kubeapiserver/authenticator/config.go index bd7ed0e1496..72421fc413c 100644 --- a/pkg/kubeapiserver/authenticator/config.go +++ b/pkg/kubeapiserver/authenticator/config.go @@ -55,7 +55,10 @@ import ( // Config contains the data on how to authenticate a request to the Kube API Server type Config struct { - Anonymous bool + // Anonymous holds the effective anonymous config, specified either via config file + // (hoisted out of AuthenticationConfig) or via flags (constructed from flag-specified values). + Anonymous apiserver.AnonymousAuthConfig + BootstrapToken bool TokenAuthFile string @@ -212,8 +215,8 @@ func (config Config) New(serverLifecycle context.Context) (authenticator.Request } if len(authenticators) == 0 { - if config.Anonymous { - return anonymous.NewAuthenticator(), nil, &securityDefinitionsV2, securitySchemesV3, nil + if config.Anonymous.Enabled { + return anonymous.NewAuthenticator(config.Anonymous.Conditions), nil, &securityDefinitionsV2, securitySchemesV3, nil } return nil, nil, &securityDefinitionsV2, securitySchemesV3, nil } @@ -222,10 +225,10 @@ func (config Config) New(serverLifecycle context.Context) (authenticator.Request authenticator = group.NewAuthenticatedGroupAdder(authenticator) - if config.Anonymous { + if config.Anonymous.Enabled { // If the authenticator chain returns an error, return an error (don't consider a bad bearer token // or invalid username/password combination anonymous). - authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator()) + authenticator = union.NewFailOnError(authenticator, anonymous.NewAuthenticator(config.Anonymous.Conditions)) } return authenticator, updateAuthenticationConfig, &securityDefinitionsV2, securitySchemesV3, nil diff --git a/pkg/kubeapiserver/options/authentication.go b/pkg/kubeapiserver/options/authentication.go index 4b24f28e01f..66926b5b060 100644 --- a/pkg/kubeapiserver/options/authentication.go +++ b/pkg/kubeapiserver/options/authentication.go @@ -22,6 +22,7 @@ import ( "fmt" "net/url" "os" + "reflect" "strings" "sync" "time" @@ -32,6 +33,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/apis/apiserver/install" @@ -95,7 +97,8 @@ type BuiltInAuthenticationOptions struct { // AnonymousAuthenticationOptions contains anonymous authentication options for API Server type AnonymousAuthenticationOptions struct { - Allow bool + Allow bool + areFlagsSet func() bool } // BootstrapTokenAuthenticationOptions contains bootstrap token authentication options for API Server @@ -169,7 +172,10 @@ func (o *BuiltInAuthenticationOptions) WithAll() *BuiltInAuthenticationOptions { // WithAnonymous set default value for anonymous authentication func (o *BuiltInAuthenticationOptions) WithAnonymous() *BuiltInAuthenticationOptions { - o.Anonymous = &AnonymousAuthenticationOptions{Allow: true} + o.Anonymous = &AnonymousAuthenticationOptions{ + Allow: true, + areFlagsSet: func() bool { return false }, + } return o } @@ -294,6 +300,14 @@ func (o *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { return } + fs.StringVar(&o.AuthenticationConfigFile, "authentication-config", o.AuthenticationConfigFile, ""+ + "File with Authentication Configuration to configure the JWT Token authenticator or the anonymous authenticator. "+ + "Note: This feature is in Alpha since v1.29."+ + "--feature-gate=StructuredAuthenticationConfiguration=true needs to be set for enabling this feature."+ + "This feature is mutually exclusive with the oidc-* flags."+ + "To configure anonymous authenticator you need to enable --feature-gate=AnonymousAuthConfigurableEndpoints."+ + "When you configure anonymous authenticator in the authentication config you cannot use the --anonymous-auth flag.") + fs.StringSliceVar(&o.APIAudiences, "api-audiences", o.APIAudiences, ""+ "Identifiers of the API. The service account token authenticator will validate that "+ "tokens used against the API are bound to at least one of these audiences. If the "+ @@ -305,6 +319,10 @@ func (o *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { "Enables anonymous requests to the secure port of the API server. "+ "Requests that are not rejected by another authentication method are treated as anonymous requests. "+ "Anonymous requests have a username of system:anonymous, and a group name of system:unauthenticated.") + + o.Anonymous.areFlagsSet = func() bool { + return fs.Changed("anonymous-auth") + } } if o.BootstrapToken != nil { @@ -358,12 +376,6 @@ func (o *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { "If set, the claim is verified to be present in the ID Token with a matching value. "+ "Repeat this flag to specify multiple claims.") - fs.StringVar(&o.AuthenticationConfigFile, "authentication-config", o.AuthenticationConfigFile, ""+ - "File with Authentication Configuration to configure the JWT Token authenticator. "+ - "Note: This feature is in Alpha since v1.29."+ - "--feature-gate=StructuredAuthenticationConfiguration=true needs to be set for enabling this feature."+ - "This feature is mutually exclusive with the oidc-* flags.") - o.OIDC.areFlagsConfigured = func() bool { return fs.Changed(oidcIssuerURLFlag) || fs.Changed(oidcClientIDFlag) || @@ -452,10 +464,6 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat TokenFailureCacheTTL: o.TokenFailureCacheTTL, } - if o.Anonymous != nil { - ret.Anonymous = o.Anonymous.Allow - } - if o.BootstrapToken != nil { ret.BootstrapToken = o.BootstrapToken.Enable } @@ -469,12 +477,18 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat } // When the StructuredAuthenticationConfiguration feature is enabled and the authentication config file is provided, - // load the authentication config from the file. + // load the authentication config from the file, otherwise set up an empty configuration. if len(o.AuthenticationConfigFile) > 0 { var err error if ret.AuthenticationConfig, ret.AuthenticationConfigData, err = loadAuthenticationConfig(o.AuthenticationConfigFile); err != nil { return kubeauthenticator.Config{}, err } + } else { + ret.AuthenticationConfig = &apiserver.AuthenticationConfiguration{} + } + + // Set up JWT authenticators from config file or from flags + if len(o.AuthenticationConfigFile) > 0 { // all known signing algs are allowed when using authentication config // TODO: what we really want to express is 'any alg is fine as long it matches a public key' ret.OIDCSigningAlgs = oidc.AllValidSigningAlgorithms() @@ -532,20 +546,30 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat jwtAuthenticator.ClaimValidationRules = claimValidationRules } - authConfig := &apiserver.AuthenticationConfiguration{ - JWT: []apiserver.JWTAuthenticator{jwtAuthenticator}, - } + ret.AuthenticationConfig.JWT = []apiserver.JWTAuthenticator{jwtAuthenticator} - ret.AuthenticationConfig = authConfig ret.OIDCSigningAlgs = o.OIDC.SigningAlgs } - if ret.AuthenticationConfig != nil { - if err := apiservervalidation.ValidateAuthenticationConfiguration(ret.AuthenticationConfig, ret.ServiceAccountIssuers).ToAggregate(); err != nil { - return kubeauthenticator.Config{}, err + // Set up anonymous authenticator from config file or flags + if o.Anonymous != nil { + switch { + case ret.AuthenticationConfig.Anonymous != nil && o.Anonymous.areFlagsSet(): + // Flags and config file are mutually exclusive + return kubeauthenticator.Config{}, field.Forbidden(field.NewPath("anonymous"), "--anonynous-auth flag cannot be set when anonymous field is configured in authentication configuration file") + case ret.AuthenticationConfig.Anonymous != nil: + // Use the config-file-specified values + ret.Anonymous = *ret.AuthenticationConfig.Anonymous + default: + // Use the flag-specified values + ret.Anonymous = apiserver.AnonymousAuthConfig{Enabled: o.Anonymous.Allow} } } + if err := apiservervalidation.ValidateAuthenticationConfiguration(ret.AuthenticationConfig, ret.ServiceAccountIssuers).ToAggregate(); err != nil { + return kubeauthenticator.Config{}, err + } + if o.RequestHeader != nil { var err error ret.RequestHeaderConfig, err = o.RequestHeader.ToAuthenticationRequestHeaderConfig() @@ -667,6 +691,10 @@ func (o *BuiltInAuthenticationOptions) ApplyTo( authenticationconfigmetrics.RegisterMetrics() trackedAuthenticationConfigData := authenticatorConfig.AuthenticationConfigData var mu sync.Mutex + + // ensure anonymous config doesn't change on reload + originalFileAnonymousConfig := authenticatorConfig.AuthenticationConfig.DeepCopy().Anonymous + go filesystem.WatchUntil( ctx, time.Minute, @@ -700,7 +728,11 @@ func (o *BuiltInAuthenticationOptions) ApplyTo( return } - if err := apiservervalidation.ValidateAuthenticationConfiguration(authConfig, authenticatorConfig.ServiceAccountIssuers).ToAggregate(); err != nil { + validationErrs := apiservervalidation.ValidateAuthenticationConfiguration(authConfig, authenticatorConfig.ServiceAccountIssuers) + if !reflect.DeepEqual(originalFileAnonymousConfig, authConfig.Anonymous) { + validationErrs = append(validationErrs, field.Forbidden(field.NewPath("anonymous"), "changed from initial configuration file")) + } + if err := validationErrs.ToAggregate(); err != nil { klog.ErrorS(err, "failed to validate authentication config") authenticationconfigmetrics.RecordAuthenticationConfigAutomaticReloadFailure(apiServerID) // this config is not semantically valid and never will be, update the tracker so we stop retrying diff --git a/pkg/kubeapiserver/options/authentication_test.go b/pkg/kubeapiserver/options/authentication_test.go index 9e8e06e5c3e..b1c599d12ec 100644 --- a/pkg/kubeapiserver/options/authentication_test.go +++ b/pkg/kubeapiserver/options/authentication_test.go @@ -46,6 +46,7 @@ import ( func TestAuthenticationValidate(t *testing.T) { testCases := []struct { name string + testAnonymous *AnonymousAuthenticationOptions testOIDC *OIDCAuthenticationOptions testSA *ServiceAccountAuthenticationOptions testWebHook *WebHookAuthenticationOptions @@ -236,11 +237,21 @@ func TestAuthenticationValidate(t *testing.T) { disabledFeatures: []featuregate.Feature{kubefeatures.ServiceAccountTokenNodeBindingValidation}, expectErr: "the \"ServiceAccountTokenNodeBinding\" feature gate can only be enabled if the \"ServiceAccountTokenNodeBindingValidation\" feature gate is also enabled", }, + { + name: "test when authentication config file and anonymous-auth flags are set AnonymousAuthConfigurableEndpoints disabled", + disabledFeatures: []featuregate.Feature{features.AnonymousAuthConfigurableEndpoints}, + testAuthenticationConfigFile: "configfile", + testAnonymous: &AnonymousAuthenticationOptions{ + Allow: true, + areFlagsSet: func() bool { return true }, + }, + }, } for _, testcase := range testCases { t.Run(testcase.name, func(t *testing.T) { options := NewBuiltInAuthenticationOptions() + options.Anonymous = testcase.testAnonymous options.OIDC = testcase.testOIDC options.ServiceAccounts = testcase.testSA options.WebHook = testcase.testWebHook @@ -304,7 +315,7 @@ func TestToAuthenticationConfig(t *testing.T) { expectConfig := kubeauthenticator.Config{ APIAudiences: authenticator.Audiences{"http://foo.bar.com"}, - Anonymous: false, + Anonymous: apiserver.AnonymousAuthConfig{Enabled: false}, BootstrapToken: false, ClientCAContentProvider: nil, // this is nil because you can't compare functions TokenAuthFile: "/testTokenFile", @@ -455,8 +466,290 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { // nil these out because you cannot compare functions opts.OIDC.areFlagsConfigured = nil + if !opts.Anonymous.areFlagsSet() { + t.Fatalf("Anonymous flags should be configured") + } + + // nil these out because you cannot compare functions + opts.Anonymous.areFlagsSet = nil + if !reflect.DeepEqual(opts, expected) { - t.Error(cmp.Diff(opts, expected, cmp.AllowUnexported(OIDCAuthenticationOptions{}))) + t.Error(cmp.Diff(opts, expected, cmp.AllowUnexported(OIDCAuthenticationOptions{}, AnonymousAuthenticationOptions{}))) + } +} + +func TestToAuthenticationConfig_Anonymous(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true) + testCases := []struct { + name string + args []string + expectConfig kubeauthenticator.Config + enableAnonymousEndpoints bool + expectErr string + }{ + { + name: "flag-none", + args: []string{}, + expectConfig: kubeauthenticator.Config{ + Anonymous: apiserver.AnonymousAuthConfig{Enabled: true}, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{}, + TokenSuccessCacheTTL: 10 * time.Second, + }, + }, + { + name: "flag-anonymous-enabled", + args: []string{"--anonymous-auth=True"}, + expectConfig: kubeauthenticator.Config{ + Anonymous: apiserver.AnonymousAuthConfig{Enabled: true}, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{}, + TokenSuccessCacheTTL: 10 * time.Second, + }, + }, + { + name: "flag-anonymous-disabled", + args: []string{"--anonymous-auth=False"}, + expectConfig: kubeauthenticator.Config{ + Anonymous: apiserver.AnonymousAuthConfig{Enabled: false}, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{}, + TokenSuccessCacheTTL: 10 * time.Second, + }, + }, + { + name: "file-anonymous-disabled-AnonymousAuthConfigurableEndpoints-disabled", + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: false +`), + }, + expectErr: "anonymous is not supported when when AnonymousAuthConfigurableEnpoints feature gate is disabled", + }, + { + name: "file-anonymous-disabled-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: false +`), + }, + expectConfig: kubeauthenticator.Config{ + Anonymous: apiserver.AnonymousAuthConfig{Enabled: false}, + TokenSuccessCacheTTL: 10 * time.Second, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{ + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: false}, + }, + AuthenticationConfigData: ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: false +`, + OIDCSigningAlgs: []string{"ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"}, + }, + }, + { + name: "file-anonymous-enabled-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: true +`), + }, + expectConfig: kubeauthenticator.Config{ + Anonymous: apiserver.AnonymousAuthConfig{Enabled: true}, + TokenSuccessCacheTTL: 10 * time.Second, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{ + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: true}, + }, + AuthenticationConfigData: ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: true +`, + OIDCSigningAlgs: []string{"ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"}, + }, + }, + { + name: "file-anonymous-disabled-with-conditions-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: false + conditions: + - path: "/livez" +`), + }, + expectErr: "enabled should be set to true when conditions are defined", + }, + { + name: "file-anonymous-missing-with-conditions-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + conditions: + - path: "/livez" +`), + }, + expectErr: "enabled should be set to true when conditions are defined", + }, + { + name: "file-anonymous-enabled-with-conditions-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: true + conditions: + - path: "/livez" +`), + }, + expectConfig: kubeauthenticator.Config{ + Anonymous: apiserver.AnonymousAuthConfig{ + Enabled: true, + Conditions: []apiserver.AnonymousAuthCondition{ + { + Path: "/livez", + }, + }, + }, + TokenSuccessCacheTTL: 10 * time.Second, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{ + Anonymous: &apiserver.AnonymousAuthConfig{ + Enabled: true, + Conditions: []apiserver.AnonymousAuthCondition{ + { + Path: "/livez", + }, + }, + }, + }, + AuthenticationConfigData: ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: true + conditions: + - path: "/livez" +`, + OIDCSigningAlgs: []string{"ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"}, + }, + }, + { + name: "flag-anonymous-enabled-file-anonymous-enabled-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{"--anonymous-auth=True", + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +anonymous: + enabled: true +`), + }, + expectErr: "--anonynous-auth flag cannot be set when anonymous field is configured in authentication configuration file", + }, + { + name: "flag-anonymous-enabled-file-anonymous-notset-AnonymousAuthConfigurableEndpoints-enabled", + enableAnonymousEndpoints: true, + args: []string{"--anonymous-auth=True", + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +jwt: +- issuer: + url: https://test-issuer + audiences: [ "🐼" ] + claimMappings: + username: + claim: sub + prefix: "" +`), + }, + expectConfig: kubeauthenticator.Config{ + TokenSuccessCacheTTL: 10 * time.Second, + Anonymous: apiserver.AnonymousAuthConfig{Enabled: true}, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{ + JWT: []apiserver.JWTAuthenticator{ + { + Issuer: apiserver.Issuer{ + URL: "https://test-issuer", + Audiences: []string{"🐼"}, + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "sub", + Prefix: pointer.String(""), + }, + }, + }, + }, + }, + AuthenticationConfigData: ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +jwt: +- issuer: + url: https://test-issuer + audiences: [ "🐼" ] + claimMappings: + username: + claim: sub + prefix: "" +`, + OIDCSigningAlgs: []string{"ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"}, + }, + }, + } + + for _, testcase := range testCases { + t.Run(testcase.name, func(t *testing.T) { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.AnonymousAuthConfigurableEndpoints, testcase.enableAnonymousEndpoints) + opts := NewBuiltInAuthenticationOptions().WithAnonymous() + pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError) + opts.AddFlags(pf) + + if err := pf.Parse(testcase.args); err != nil { + t.Fatal(err) + } + + resultConfig, err := opts.ToAuthenticationConfig() + + if testcase.expectErr != "" { + if err == nil { + t.Fatalf("Got err: %v; Want err: %v", err, testcase.expectErr) + } + + if !strings.Contains(err.Error(), testcase.expectErr) { + t.Fatalf("Got err: %v; Want err: %v", err, testcase.expectErr) + } + + return + } + + if err != nil { + t.Fatal(err) + } + + if !reflect.DeepEqual(resultConfig, testcase.expectConfig) { + t.Error(cmp.Diff(resultConfig, testcase.expectConfig)) + } + }) } } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go index a31b8753693..af70fe24460 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/types.go @@ -165,6 +165,25 @@ type AuthenticationConfiguration struct { metav1.TypeMeta JWT []JWTAuthenticator + + // If present --anonymous-auth must not be set + Anonymous *AnonymousAuthConfig +} + +// AnonymousAuthConfig provides the configuration for the anonymous authenticator. +type AnonymousAuthConfig struct { + Enabled bool + + // If set, anonymous auth is only allowed if the request meets one of the + // conditions. + Conditions []AnonymousAuthCondition +} + +// AnonymousAuthCondition describes the condition under which anonymous auth +// should be enabled. +type AnonymousAuthCondition struct { + // Path for which anonymous auth is enabled. + Path string } // JWTAuthenticator provides the configuration for a single JWT authenticator. diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go index fc75c464a29..214ef4e4fca 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/types.go @@ -185,6 +185,25 @@ type AuthenticationConfiguration struct { // "": "username" // } JWT []JWTAuthenticator `json:"jwt"` + + // If present --anonymous-auth must not be set + Anonymous *AnonymousAuthConfig `json:"anonymous,omitempty"` +} + +// AnonymousAuthConfig provides the configuration for the anonymous authenticator. +type AnonymousAuthConfig struct { + Enabled bool `json:"enabled"` + + // If set, anonymous auth is only allowed if the request meets one of the + // conditions. + Conditions []AnonymousAuthCondition `json:"conditions,omitempty"` +} + +// AnonymousAuthCondition describes the condition under which anonymous auth +// should be enabled. +type AnonymousAuthCondition struct { + // Path for which anonymous auth is enabled. + Path string `json:"path"` } // JWTAuthenticator provides the configuration for a single JWT authenticator. diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go index 9ee1ef8a4b5..3a6c66c3aac 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.conversion.go @@ -57,6 +57,26 @@ func RegisterConversions(s *runtime.Scheme) error { }); err != nil { return err } + if err := s.AddGeneratedConversionFunc((*AnonymousAuthCondition)(nil), (*apiserver.AnonymousAuthCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(a.(*AnonymousAuthCondition), b.(*apiserver.AnonymousAuthCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*apiserver.AnonymousAuthCondition)(nil), (*AnonymousAuthCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_apiserver_AnonymousAuthCondition_To_v1alpha1_AnonymousAuthCondition(a.(*apiserver.AnonymousAuthCondition), b.(*AnonymousAuthCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*AnonymousAuthConfig)(nil), (*apiserver.AnonymousAuthConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1alpha1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(a.(*AnonymousAuthConfig), b.(*apiserver.AnonymousAuthConfig), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*apiserver.AnonymousAuthConfig)(nil), (*AnonymousAuthConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_apiserver_AnonymousAuthConfig_To_v1alpha1_AnonymousAuthConfig(a.(*apiserver.AnonymousAuthConfig), b.(*AnonymousAuthConfig), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*AuthenticationConfiguration)(nil), (*apiserver.AuthenticationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(a.(*AuthenticationConfiguration), b.(*apiserver.AuthenticationConfiguration), scope) }); err != nil { @@ -324,6 +344,48 @@ func Convert_apiserver_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginC return autoConvert_apiserver_AdmissionPluginConfiguration_To_v1alpha1_AdmissionPluginConfiguration(in, out, s) } +func autoConvert_v1alpha1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(in *AnonymousAuthCondition, out *apiserver.AnonymousAuthCondition, s conversion.Scope) error { + out.Path = in.Path + return nil +} + +// Convert_v1alpha1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition is an autogenerated conversion function. +func Convert_v1alpha1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(in *AnonymousAuthCondition, out *apiserver.AnonymousAuthCondition, s conversion.Scope) error { + return autoConvert_v1alpha1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(in, out, s) +} + +func autoConvert_apiserver_AnonymousAuthCondition_To_v1alpha1_AnonymousAuthCondition(in *apiserver.AnonymousAuthCondition, out *AnonymousAuthCondition, s conversion.Scope) error { + out.Path = in.Path + return nil +} + +// Convert_apiserver_AnonymousAuthCondition_To_v1alpha1_AnonymousAuthCondition is an autogenerated conversion function. +func Convert_apiserver_AnonymousAuthCondition_To_v1alpha1_AnonymousAuthCondition(in *apiserver.AnonymousAuthCondition, out *AnonymousAuthCondition, s conversion.Scope) error { + return autoConvert_apiserver_AnonymousAuthCondition_To_v1alpha1_AnonymousAuthCondition(in, out, s) +} + +func autoConvert_v1alpha1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(in *AnonymousAuthConfig, out *apiserver.AnonymousAuthConfig, s conversion.Scope) error { + out.Enabled = in.Enabled + out.Conditions = *(*[]apiserver.AnonymousAuthCondition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_v1alpha1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig is an autogenerated conversion function. +func Convert_v1alpha1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(in *AnonymousAuthConfig, out *apiserver.AnonymousAuthConfig, s conversion.Scope) error { + return autoConvert_v1alpha1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(in, out, s) +} + +func autoConvert_apiserver_AnonymousAuthConfig_To_v1alpha1_AnonymousAuthConfig(in *apiserver.AnonymousAuthConfig, out *AnonymousAuthConfig, s conversion.Scope) error { + out.Enabled = in.Enabled + out.Conditions = *(*[]AnonymousAuthCondition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_apiserver_AnonymousAuthConfig_To_v1alpha1_AnonymousAuthConfig is an autogenerated conversion function. +func Convert_apiserver_AnonymousAuthConfig_To_v1alpha1_AnonymousAuthConfig(in *apiserver.AnonymousAuthConfig, out *AnonymousAuthConfig, s conversion.Scope) error { + return autoConvert_apiserver_AnonymousAuthConfig_To_v1alpha1_AnonymousAuthConfig(in, out, s) +} + func autoConvert_v1alpha1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in *AuthenticationConfiguration, out *apiserver.AuthenticationConfiguration, s conversion.Scope) error { if in.JWT != nil { in, out := &in.JWT, &out.JWT @@ -336,6 +398,7 @@ func autoConvert_v1alpha1_AuthenticationConfiguration_To_apiserver_Authenticatio } else { out.JWT = nil } + out.Anonymous = (*apiserver.AnonymousAuthConfig)(unsafe.Pointer(in.Anonymous)) return nil } @@ -356,6 +419,7 @@ func autoConvert_apiserver_AuthenticationConfiguration_To_v1alpha1_Authenticatio } else { out.JWT = nil } + out.Anonymous = (*AnonymousAuthConfig)(unsafe.Pointer(in.Anonymous)) return nil } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go index e618178bfec..81b652254cb 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/zz_generated.deepcopy.go @@ -78,6 +78,43 @@ func (in *AdmissionPluginConfiguration) DeepCopy() *AdmissionPluginConfiguration return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnonymousAuthCondition) DeepCopyInto(out *AnonymousAuthCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonymousAuthCondition. +func (in *AnonymousAuthCondition) DeepCopy() *AnonymousAuthCondition { + if in == nil { + return nil + } + out := new(AnonymousAuthCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnonymousAuthConfig) DeepCopyInto(out *AnonymousAuthConfig) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]AnonymousAuthCondition, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonymousAuthConfig. +func (in *AnonymousAuthConfig) DeepCopy() *AnonymousAuthConfig { + if in == nil { + return nil + } + out := new(AnonymousAuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) { *out = *in @@ -89,6 +126,11 @@ func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfigura (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Anonymous != nil { + in, out := &in.Anonymous, &out.Anonymous + *out = new(AnonymousAuthConfig) + (*in).DeepCopyInto(*out) + } return } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/types.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/types.go index 00a55f7a925..570f3c4682b 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/types.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/types.go @@ -156,6 +156,25 @@ type AuthenticationConfiguration struct { // "": "username" // } JWT []JWTAuthenticator `json:"jwt"` + + // If present --anonymous-auth must not be set + Anonymous *AnonymousAuthConfig `json:"anonymous,omitempty"` +} + +// AnonymousAuthConfig provides the configuration for the anonymous authenticator. +type AnonymousAuthConfig struct { + Enabled bool `json:"enabled"` + + // If set, anonymous auth is only allowed if the request meets one of the + // conditions. + Conditions []AnonymousAuthCondition `json:"conditions,omitempty"` +} + +// AnonymousAuthCondition describes the condition under which anonymous auth +// should be enabled. +type AnonymousAuthCondition struct { + // Path for which anonymous auth is enabled. + Path string `json:"path"` } // JWTAuthenticator provides the configuration for a single JWT authenticator. diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go index 911a331f24c..30ef049d403 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.conversion.go @@ -37,6 +37,26 @@ func init() { // RegisterConversions adds conversion functions to the given scheme. // Public to allow building arbitrary schemes. func RegisterConversions(s *runtime.Scheme) error { + if err := s.AddGeneratedConversionFunc((*AnonymousAuthCondition)(nil), (*apiserver.AnonymousAuthCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(a.(*AnonymousAuthCondition), b.(*apiserver.AnonymousAuthCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*apiserver.AnonymousAuthCondition)(nil), (*AnonymousAuthCondition)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_apiserver_AnonymousAuthCondition_To_v1beta1_AnonymousAuthCondition(a.(*apiserver.AnonymousAuthCondition), b.(*AnonymousAuthCondition), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*AnonymousAuthConfig)(nil), (*apiserver.AnonymousAuthConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_v1beta1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(a.(*AnonymousAuthConfig), b.(*apiserver.AnonymousAuthConfig), scope) + }); err != nil { + return err + } + if err := s.AddGeneratedConversionFunc((*apiserver.AnonymousAuthConfig)(nil), (*AnonymousAuthConfig)(nil), func(a, b interface{}, scope conversion.Scope) error { + return Convert_apiserver_AnonymousAuthConfig_To_v1beta1_AnonymousAuthConfig(a.(*apiserver.AnonymousAuthConfig), b.(*AnonymousAuthConfig), scope) + }); err != nil { + return err + } if err := s.AddGeneratedConversionFunc((*AuthenticationConfiguration)(nil), (*apiserver.AuthenticationConfiguration)(nil), func(a, b interface{}, scope conversion.Scope) error { return Convert_v1beta1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(a.(*AuthenticationConfiguration), b.(*apiserver.AuthenticationConfiguration), scope) }); err != nil { @@ -260,6 +280,48 @@ func RegisterConversions(s *runtime.Scheme) error { return nil } +func autoConvert_v1beta1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(in *AnonymousAuthCondition, out *apiserver.AnonymousAuthCondition, s conversion.Scope) error { + out.Path = in.Path + return nil +} + +// Convert_v1beta1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition is an autogenerated conversion function. +func Convert_v1beta1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(in *AnonymousAuthCondition, out *apiserver.AnonymousAuthCondition, s conversion.Scope) error { + return autoConvert_v1beta1_AnonymousAuthCondition_To_apiserver_AnonymousAuthCondition(in, out, s) +} + +func autoConvert_apiserver_AnonymousAuthCondition_To_v1beta1_AnonymousAuthCondition(in *apiserver.AnonymousAuthCondition, out *AnonymousAuthCondition, s conversion.Scope) error { + out.Path = in.Path + return nil +} + +// Convert_apiserver_AnonymousAuthCondition_To_v1beta1_AnonymousAuthCondition is an autogenerated conversion function. +func Convert_apiserver_AnonymousAuthCondition_To_v1beta1_AnonymousAuthCondition(in *apiserver.AnonymousAuthCondition, out *AnonymousAuthCondition, s conversion.Scope) error { + return autoConvert_apiserver_AnonymousAuthCondition_To_v1beta1_AnonymousAuthCondition(in, out, s) +} + +func autoConvert_v1beta1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(in *AnonymousAuthConfig, out *apiserver.AnonymousAuthConfig, s conversion.Scope) error { + out.Enabled = in.Enabled + out.Conditions = *(*[]apiserver.AnonymousAuthCondition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_v1beta1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig is an autogenerated conversion function. +func Convert_v1beta1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(in *AnonymousAuthConfig, out *apiserver.AnonymousAuthConfig, s conversion.Scope) error { + return autoConvert_v1beta1_AnonymousAuthConfig_To_apiserver_AnonymousAuthConfig(in, out, s) +} + +func autoConvert_apiserver_AnonymousAuthConfig_To_v1beta1_AnonymousAuthConfig(in *apiserver.AnonymousAuthConfig, out *AnonymousAuthConfig, s conversion.Scope) error { + out.Enabled = in.Enabled + out.Conditions = *(*[]AnonymousAuthCondition)(unsafe.Pointer(&in.Conditions)) + return nil +} + +// Convert_apiserver_AnonymousAuthConfig_To_v1beta1_AnonymousAuthConfig is an autogenerated conversion function. +func Convert_apiserver_AnonymousAuthConfig_To_v1beta1_AnonymousAuthConfig(in *apiserver.AnonymousAuthConfig, out *AnonymousAuthConfig, s conversion.Scope) error { + return autoConvert_apiserver_AnonymousAuthConfig_To_v1beta1_AnonymousAuthConfig(in, out, s) +} + func autoConvert_v1beta1_AuthenticationConfiguration_To_apiserver_AuthenticationConfiguration(in *AuthenticationConfiguration, out *apiserver.AuthenticationConfiguration, s conversion.Scope) error { if in.JWT != nil { in, out := &in.JWT, &out.JWT @@ -272,6 +334,7 @@ func autoConvert_v1beta1_AuthenticationConfiguration_To_apiserver_Authentication } else { out.JWT = nil } + out.Anonymous = (*apiserver.AnonymousAuthConfig)(unsafe.Pointer(in.Anonymous)) return nil } @@ -292,6 +355,7 @@ func autoConvert_apiserver_AuthenticationConfiguration_To_v1beta1_Authentication } else { out.JWT = nil } + out.Anonymous = (*AnonymousAuthConfig)(unsafe.Pointer(in.Anonymous)) return nil } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go index 7da9db92725..0d78e51a96b 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1beta1/zz_generated.deepcopy.go @@ -25,6 +25,43 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnonymousAuthCondition) DeepCopyInto(out *AnonymousAuthCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonymousAuthCondition. +func (in *AnonymousAuthCondition) DeepCopy() *AnonymousAuthCondition { + if in == nil { + return nil + } + out := new(AnonymousAuthCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnonymousAuthConfig) DeepCopyInto(out *AnonymousAuthConfig) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]AnonymousAuthCondition, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonymousAuthConfig. +func (in *AnonymousAuthConfig) DeepCopy() *AnonymousAuthConfig { + if in == nil { + return nil + } + out := new(AnonymousAuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) { *out = *in @@ -36,6 +73,11 @@ func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfigura (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Anonymous != nil { + in, out := &in.Anonymous, &out.Anonymous + *out = new(AnonymousAuthConfig) + (*in).DeepCopyInto(*out) + } return } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go index 471eb4a7410..fb148f2c975 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go @@ -78,6 +78,15 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, dis } } + if c.Anonymous != nil { + if !utilfeature.DefaultFeatureGate.Enabled(features.AnonymousAuthConfigurableEndpoints) { + allErrs = append(allErrs, field.Forbidden(field.NewPath("anonymous"), "anonymous is not supported when when AnonymousAuthConfigurableEnpoints feature gate is disabled")) + } + if !c.Anonymous.Enabled && len(c.Anonymous.Conditions) > 0 { + allErrs = append(allErrs, field.Invalid(field.NewPath("anonymous", "conditions"), c.Anonymous.Conditions, "enabled should be set to true when conditions are defined")) + } + } + return allErrs } diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/zz_generated.deepcopy.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/zz_generated.deepcopy.go index b88c47c6721..6439e822081 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/zz_generated.deepcopy.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/zz_generated.deepcopy.go @@ -100,6 +100,43 @@ func (in *AdmissionPluginConfiguration) DeepCopy() *AdmissionPluginConfiguration return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnonymousAuthCondition) DeepCopyInto(out *AnonymousAuthCondition) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonymousAuthCondition. +func (in *AnonymousAuthCondition) DeepCopy() *AnonymousAuthCondition { + if in == nil { + return nil + } + out := new(AnonymousAuthCondition) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AnonymousAuthConfig) DeepCopyInto(out *AnonymousAuthConfig) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]AnonymousAuthCondition, len(*in)) + copy(*out, *in) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AnonymousAuthConfig. +func (in *AnonymousAuthConfig) DeepCopy() *AnonymousAuthConfig { + if in == nil { + return nil + } + out := new(AnonymousAuthConfig) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfiguration) { *out = *in @@ -111,6 +148,11 @@ func (in *AuthenticationConfiguration) DeepCopyInto(out *AuthenticationConfigura (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.Anonymous != nil { + in, out := &in.Anonymous, &out.Anonymous + *out = new(AnonymousAuthConfig) + (*in).DeepCopyInto(*out) + } return } diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go index 8e568c7e4d9..76ef44732ed 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory/delegating.go @@ -21,6 +21,7 @@ import ( "time" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/group" "k8s.io/apiserver/pkg/authentication/request/anonymous" @@ -39,7 +40,7 @@ import ( // DelegatingAuthenticatorConfig is the minimal configuration needed to create an authenticator // built to delegate authentication to a kube API server type DelegatingAuthenticatorConfig struct { - Anonymous bool + Anonymous *apiserver.AnonymousAuthConfig // TokenAccessReviewClient is a client to do token review. It can be nil. Then every token is ignored. TokenAccessReviewClient authenticationclient.AuthenticationV1Interface @@ -112,15 +113,15 @@ func (c DelegatingAuthenticatorConfig) New() (authenticator.Request, *spec.Secur } if len(authenticators) == 0 { - if c.Anonymous { - return anonymous.NewAuthenticator(), &securityDefinitions, nil + if c.Anonymous != nil && c.Anonymous.Enabled { + return anonymous.NewAuthenticator(c.Anonymous.Conditions), &securityDefinitions, nil } - return nil, nil, errors.New("No authentication method configured") + return nil, nil, errors.New("no authentication method configured") } authenticator := group.NewAuthenticatedGroupAdder(unionauth.New(authenticators...)) - if c.Anonymous { - authenticator = unionauth.NewFailOnError(authenticator, anonymous.NewAuthenticator()) + if c.Anonymous != nil && c.Anonymous.Enabled { + authenticator = unionauth.NewFailOnError(authenticator, anonymous.NewAuthenticator(c.Anonymous.Conditions)) } return authenticator, &securityDefinitions, nil } diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous.go b/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous.go index f9177d15137..8a881043530 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous.go @@ -19,25 +19,44 @@ package anonymous import ( "net/http" + "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/user" ) const ( - anonymousUser = user.Anonymous - + anonymousUser = user.Anonymous unauthenticatedGroup = user.AllUnauthenticated ) -func NewAuthenticator() authenticator.Request { - return authenticator.RequestFunc(func(req *http.Request) (*authenticator.Response, bool, error) { - auds, _ := authenticator.AudiencesFrom(req.Context()) - return &authenticator.Response{ - User: &user.DefaultInfo{ - Name: anonymousUser, - Groups: []string{unauthenticatedGroup}, - }, - Audiences: auds, - }, true, nil - }) +type Authenticator struct { + allowedPaths map[string]bool +} + +func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) { + if len(a.allowedPaths) > 0 && !a.allowedPaths[req.URL.Path] { + return nil, false, nil + } + + auds, _ := authenticator.AudiencesFrom(req.Context()) + return &authenticator.Response{ + User: &user.DefaultInfo{ + Name: anonymousUser, + Groups: []string{unauthenticatedGroup}, + }, + Audiences: auds, + }, true, nil +} + +// NewAuthenticator returns a new anonymous authenticator. +// When conditions is empty all requests are authenticated as anonymous. +// When conditions are non-empty only those requests that match the at-least one +// condition are authenticated as anonymous. +func NewAuthenticator(conditions []apiserver.AnonymousAuthCondition) authenticator.Request { + allowedPaths := make(map[string]bool) + for _, c := range conditions { + allowedPaths[c.Path] = true + } + + return &Authenticator{allowedPaths: allowedPaths} } diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous_test.go index 494ab60974c..5fcc602214d 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous_test.go +++ b/staging/src/k8s.io/apiserver/pkg/authentication/request/anonymous/anonymous_test.go @@ -18,15 +18,16 @@ package anonymous import ( "net/http" + "net/url" "testing" "k8s.io/apimachinery/pkg/util/sets" - "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/user" ) func TestAnonymous(t *testing.T) { - var a authenticator.Request = NewAuthenticator() + a := NewAuthenticator(nil) r, ok, err := a.AuthenticateRequest(&http.Request{}) if err != nil { t.Fatalf("Unexpected error %v", err) @@ -41,3 +42,85 @@ func TestAnonymous(t *testing.T) { t.Fatalf("Expected group %s, got %v", user.AllUnauthenticated, r.User.GetGroups()) } } + +func TestAnonymousRestricted(t *testing.T) { + a := NewAuthenticator([]apiserver.AnonymousAuthCondition{ + { + Path: "/healthz", + }, + { + Path: "/readyz", + }, + { + Path: "/livez", + }, + }) + + testCases := []struct { + desc string + path string + want user.DefaultInfo + wantAllowed bool + }{ + { + desc: "/healthz", + path: "https://123.123.123.123/healthz", + want: user.DefaultInfo{ + Name: anonymousUser, + Groups: []string{unauthenticatedGroup}, + }, + wantAllowed: true, + }, + { + desc: "/readyz", + path: "https://123.123.123.123/readyz", + want: user.DefaultInfo{ + Name: anonymousUser, + Groups: []string{unauthenticatedGroup}, + }, + wantAllowed: true, + }, + { + desc: "/livez", + path: "https://123.123.123.123/livez", + want: user.DefaultInfo{ + Name: anonymousUser, + Groups: []string{unauthenticatedGroup}, + }, + wantAllowed: true, + }, + { + desc: "/api", + path: "https://123.123.123.123/api", + wantAllowed: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + u, err := url.Parse(tc.path) + if err != nil { + t.Fatal(err) + } + r, allowed, err := a.AuthenticateRequest(&http.Request{URL: u}) + if err != nil { + t.Fatal(err) + } + + if tc.wantAllowed != allowed { + t.Fatalf("want allowed: %v, got allowed: %v", tc.wantAllowed, allowed) + } + + if !tc.wantAllowed { + return + } + + if r.User.GetName() != tc.want.Name { + t.Fatalf("Expected username %s, got %s", user.Anonymous, r.User.GetName()) + } + if !sets.NewString(r.User.GetGroups()...).Equal(sets.NewString(tc.want.Groups...)) { + t.Fatalf("Expected group %s, got %v", tc.want.Groups, r.User.GetGroups()) + } + }) + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go index f6fcb5ad768..2ef45a6be88 100644 --- a/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go +++ b/staging/src/k8s.io/apiserver/pkg/endpoints/filters/authentication_test.go @@ -487,7 +487,7 @@ func TestUnauthenticatedHTTP2ClientConnectionClose(t *testing.T) { case "error": return nil, false, errors.New("authn err") case "anonymous": - return anonymous.NewAuthenticator().AuthenticateRequest(r) + return anonymous.NewAuthenticator(nil).AuthenticateRequest(r) case "anonymous_group": return &authenticator.Response{User: &user.DefaultInfo{Groups: []string{user.AllUnauthenticated}}}, true, nil default: diff --git a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go index 80dc25cc6b1..6c1c0c2bd0a 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -53,6 +53,13 @@ const ( // caching with ETags containing all APIResources known to the apiserver. AggregatedDiscoveryEndpoint featuregate.Feature = "AggregatedDiscoveryEndpoint" + // owner: @vinayakankugoyal + // kep: https://kep.k8s.io/4633 + // alpha: v1.31 + // + // Allows us to enable anonymous auth for only certain apiserver endpoints. + AnonymousAuthConfigurableEndpoints featuregate.Feature = "AnonymousAuthConfigurableEndpoints" + // owner: @smarterclayton // alpha: v1.8 // beta: v1.9 @@ -329,6 +336,8 @@ func init() { // available throughout Kubernetes binaries. var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{ + AnonymousAuthConfigurableEndpoints: {Default: false, PreRelease: featuregate.Alpha}, + AggregatedDiscoveryEndpoint: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.33 AdmissionWebhookMatchConditions: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, // remove in 1.33 diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go index e9a61d30b96..c40e4cf4344 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go +++ b/staging/src/k8s.io/apiserver/pkg/server/options/authentication.go @@ -26,6 +26,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/wait" + "k8s.io/apiserver/pkg/apis/apiserver" "k8s.io/apiserver/pkg/authentication/authenticatorfactory" "k8s.io/apiserver/pkg/authentication/request/headerrequest" "k8s.io/apiserver/pkg/server" @@ -222,8 +223,8 @@ type DelegatingAuthenticationOptions struct { // CustomRoundTripperFn allows for specifying a middleware function for custom HTTP behaviour for the authentication webhook client. CustomRoundTripperFn transport.WrapperFunc - // DisableAnonymous gives user an option to disable Anonymous authentication. - DisableAnonymous bool + // Anonymous gives user an option to enable/disable Anonymous authentication. + Anonymous *apiserver.AnonymousAuthConfig } func NewDelegatingAuthenticationOptions() *DelegatingAuthenticationOptions { @@ -238,6 +239,7 @@ func NewDelegatingAuthenticationOptions() *DelegatingAuthenticationOptions { }, WebhookRetryBackoff: DefaultAuthWebhookRetryBackoff(), TokenRequestTimeout: 10 * time.Second, + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: true}, } } @@ -305,7 +307,7 @@ func (s *DelegatingAuthenticationOptions) ApplyTo(authenticationInfo *server.Aut } cfg := authenticatorfactory.DelegatingAuthenticatorConfig{ - Anonymous: !s.DisableAnonymous, + Anonymous: &apiserver.AnonymousAuthConfig{Enabled: true}, CacheTTL: s.CacheTTL, WebhookRetryBackoff: s.WebhookRetryBackoff, TokenAccessReviewTimeout: s.TokenRequestTimeout, diff --git a/staging/src/k8s.io/client-go/rest/config_test.go b/staging/src/k8s.io/client-go/rest/config_test.go index 555727d29e6..4fc74f545a1 100644 --- a/staging/src/k8s.io/client-go/rest/config_test.go +++ b/staging/src/k8s.io/client-go/rest/config_test.go @@ -298,7 +298,7 @@ func (fakeAuthProviderConfigPersister) Persist(map[string]string) error { var fakeAuthProviderConfigPersisterError = errors.New("fakeAuthProviderConfigPersisterError") -func TestAnonymousConfig(t *testing.T) { +func TestAnonymousAuthConfig(t *testing.T) { f := fuzz.New().NilChance(0.0).NumElements(1, 1) f.Funcs( func(r *runtime.Codec, f fuzz.Continue) { diff --git a/staging/src/k8s.io/cloud-provider/options/options_test.go b/staging/src/k8s.io/cloud-provider/options/options_test.go index ebc261a4943..ec6135ce04f 100644 --- a/staging/src/k8s.io/cloud-provider/options/options_test.go +++ b/staging/src/k8s.io/cloud-provider/options/options_test.go @@ -26,6 +26,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apiserverapis "k8s.io/apiserver/pkg/apis/apiserver" apiserver "k8s.io/apiserver/pkg/server" apiserveroptions "k8s.io/apiserver/pkg/server/options" appconfig "k8s.io/cloud-provider/app/config" @@ -136,6 +137,7 @@ func TestDefaultFlags(t *testing.T) { ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, RemoteKubeConfigFileOptional: true, + Anonymous: &apiserverapis.AnonymousAuthConfig{Enabled: true}, }, Authorization: &apiserveroptions.DelegatingAuthorizationOptions{ AllowCacheTTL: 10 * time.Second, @@ -295,6 +297,7 @@ func TestAddFlags(t *testing.T) { ExtraHeaderPrefixes: []string{"x-remote-extra-"}, }, RemoteKubeConfigFileOptional: true, + Anonymous: &apiserverapis.AnonymousAuthConfig{Enabled: true}, }, Authorization: &apiserveroptions.DelegatingAuthorizationOptions{ AllowCacheTTL: 10 * time.Second,