KEP-4633: Allow health-only anonymous auth mode.

Signed-off-by: Vinayak Goyal <vinaygo@google.com>
This commit is contained in:
Vinayak Goyal 2024-05-16 21:18:34 +00:00
parent 85ede67ac9
commit 5e6a4937f5
25 changed files with 830 additions and 65 deletions

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.

View File

@ -185,6 +185,25 @@ type AuthenticationConfiguration struct {
// "<username claim>": "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.

View File

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

View File

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

View File

@ -156,6 +156,25 @@ type AuthenticationConfiguration struct {
// "<username claim>": "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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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:

View File

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

View File

@ -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,

View File

@ -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) {

View File

@ -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,