From 1bf90f9484c5dbcd941251f0036af65fa25ee193 Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Thu, 10 Aug 2023 22:06:41 +0000 Subject: [PATCH 1/3] add StructuredAuthenticationConfiguration feature flag Signed-off-by: Anish Ramasekar --- .../src/k8s.io/apiserver/pkg/features/kube_features.go | 9 +++++++++ 1 file changed, 9 insertions(+) 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 18b04d234cc..2faeee8172a 100644 --- a/staging/src/k8s.io/apiserver/pkg/features/kube_features.go +++ b/staging/src/k8s.io/apiserver/pkg/features/kube_features.go @@ -198,6 +198,13 @@ const ( // document. StorageVersionHash featuregate.Feature = "StorageVersionHash" + // owner: @aramase, @enj, @nabokihms + // kep: https://kep.k8s.io/3331 + // alpha: v1.29 + // + // Enables Structured Authentication Configuration + StructuredAuthenticationConfiguration featuregate.Feature = "StructuredAuthenticationConfiguration" + // owner: @wojtek-t // alpha: v1.15 // beta: v1.16 @@ -278,6 +285,8 @@ var defaultKubernetesFeatureGates = map[featuregate.Feature]featuregate.FeatureS StorageVersionHash: {Default: true, PreRelease: featuregate.Beta}, + StructuredAuthenticationConfiguration: {Default: false, PreRelease: featuregate.Alpha}, + WatchBookmark: {Default: true, PreRelease: featuregate.GA, LockToDefault: true}, InPlacePodVerticalScaling: {Default: false, PreRelease: featuregate.Alpha}, From 9e1ff1e51201ac41ddb1eed0d5cc015b4b6aa3df Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Thu, 10 Aug 2023 22:45:07 +0000 Subject: [PATCH 2/3] add loading config and wire feature flag Signed-off-by: Anish Ramasekar --- .../app/options/options_test.go | 12 +- .../apiserver/options/options_test.go | 12 +- pkg/kubeapiserver/options/authentication.go | 142 +++++- .../options/authentication_test.go | 482 ++++++++++++++++-- .../apiserver/pkg/apis/apiserver/register.go | 1 + .../pkg/apis/apiserver/v1alpha1/register.go | 1 + 6 files changed, 575 insertions(+), 75 deletions(-) diff --git a/cmd/kube-apiserver/app/options/options_test.go b/cmd/kube-apiserver/app/options/options_test.go index 432530f08e2..9dabfa2dd34 100644 --- a/cmd/kube-apiserver/app/options/options_test.go +++ b/cmd/kube-apiserver/app/options/options_test.go @@ -257,11 +257,8 @@ func TestAddFlags(t *testing.T) { RetryBackoff: apiserveroptions.DefaultAuthWebhookRetryBackoff(), }, BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{}, - OIDC: &kubeoptions.OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - }, - RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{}, + OIDC: s.Authentication.OIDC, + RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{}, ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{ Lookup: true, ExtendExpiration: true, @@ -327,7 +324,10 @@ func TestAddFlags(t *testing.T) { }, } + expected.Authentication.OIDC.UsernameClaim = "sub" + expected.Authentication.OIDC.SigningAlgs = []string{"RS256"} + 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{}))) + t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}))) } } diff --git a/pkg/controlplane/apiserver/options/options_test.go b/pkg/controlplane/apiserver/options/options_test.go index fc237b102f4..f11ee47f676 100644 --- a/pkg/controlplane/apiserver/options/options_test.go +++ b/pkg/controlplane/apiserver/options/options_test.go @@ -243,11 +243,8 @@ func TestAddFlags(t *testing.T) { RetryBackoff: apiserveroptions.DefaultAuthWebhookRetryBackoff(), }, BootstrapToken: &kubeoptions.BootstrapTokenAuthenticationOptions{}, - OIDC: &kubeoptions.OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - }, - RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{}, + OIDC: s.Authentication.OIDC, + RequestHeader: &apiserveroptions.RequestHeaderAuthenticationOptions{}, ServiceAccounts: &kubeoptions.ServiceAccountAuthenticationOptions{ Lookup: true, ExtendExpiration: true, @@ -283,7 +280,10 @@ func TestAddFlags(t *testing.T) { AggregatorRejectForwardingRedirects: true, } + expected.Authentication.OIDC.UsernameClaim = "sub" + expected.Authentication.OIDC.SigningAlgs = []string{"RS256"} + 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{}))) + t.Errorf("Got different run options than expected.\nDifference detected on:\n%s", cmp.Diff(expected, s, cmpopts.IgnoreUnexported(admission.Plugins{}, kubeoptions.OIDCAuthenticationOptions{}))) } } diff --git a/pkg/kubeapiserver/options/authentication.go b/pkg/kubeapiserver/options/authentication.go index fae015e629e..fe7753ab0d1 100644 --- a/pkg/kubeapiserver/options/authentication.go +++ b/pkg/kubeapiserver/options/authentication.go @@ -27,14 +27,19 @@ import ( "github.com/spf13/pflag" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/wait" "k8s.io/apiserver/pkg/apis/apiserver" + "k8s.io/apiserver/pkg/apis/apiserver/install" apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation" "k8s.io/apiserver/pkg/authentication/authenticator" + genericfeatures "k8s.io/apiserver/pkg/features" genericapiserver "k8s.io/apiserver/pkg/server" "k8s.io/apiserver/pkg/server/egressselector" genericoptions "k8s.io/apiserver/pkg/server/options" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" cliflag "k8s.io/component-base/cli/flag" @@ -47,6 +52,18 @@ import ( "k8s.io/utils/pointer" ) +const ( + oidcIssuerURLFlag = "oidc-issuer-url" + oidcClientIDFlag = "oidc-client-id" + oidcCAFileFlag = "oidc-ca-file" + oidcUsernameClaimFlag = "oidc-username-claim" + oidcUsernamePrefixFlag = "oidc-username-prefix" + oidcGroupsClaimFlag = "oidc-groups-claim" + oidcGroupsPrefixFlag = "oidc-groups-prefix" + oidcSigningAlgsFlag = "oidc-signing-algs" + oidcRequiredClaimFlag = "oidc-required-claim" +) + // BuiltInAuthenticationOptions contains all build-in authentication options for API Server type BuiltInAuthenticationOptions struct { APIAudiences []string @@ -59,6 +76,8 @@ type BuiltInAuthenticationOptions struct { TokenFile *TokenFileAuthenticationOptions WebHook *WebHookAuthenticationOptions + AuthenticationConfigFile string + TokenSuccessCacheTTL time.Duration TokenFailureCacheTTL time.Duration } @@ -84,6 +103,9 @@ type OIDCAuthenticationOptions struct { GroupsPrefix string SigningAlgs []string RequiredClaims map[string]string + + // areFlagsConfigured is a function that returns true if any of the oidc-* flags are configured. + areFlagsConfigured func() bool } // ServiceAccountAuthenticationOptions contains service account authentication options for API Server @@ -154,7 +176,7 @@ func (o *BuiltInAuthenticationOptions) WithClientCert() *BuiltInAuthenticationOp // WithOIDC set default value for OIDC authentication func (o *BuiltInAuthenticationOptions) WithOIDC() *BuiltInAuthenticationOptions { - o.OIDC = &OIDCAuthenticationOptions{} + o.OIDC = &OIDCAuthenticationOptions{areFlagsConfigured: func() bool { return false }} return o } @@ -190,9 +212,7 @@ func (o *BuiltInAuthenticationOptions) WithWebHook() *BuiltInAuthenticationOptio func (o *BuiltInAuthenticationOptions) Validate() []error { var allErrors []error - if o.OIDC != nil && (len(o.OIDC.IssuerURL) > 0) != (len(o.OIDC.ClientID) > 0) { - allErrors = append(allErrors, fmt.Errorf("oidc-issuer-url and oidc-client-id should be specified together")) - } + allErrors = append(allErrors, o.validateOIDCOptions()...) if o.ServiceAccounts != nil && len(o.ServiceAccounts.Issuers) > 0 { seen := make(map[string]bool) @@ -274,45 +294,63 @@ func (o *BuiltInAuthenticationOptions) AddFlags(fs *pflag.FlagSet) { } if o.OIDC != nil { - fs.StringVar(&o.OIDC.IssuerURL, "oidc-issuer-url", o.OIDC.IssuerURL, ""+ + fs.StringVar(&o.OIDC.IssuerURL, oidcIssuerURLFlag, o.OIDC.IssuerURL, ""+ "The URL of the OpenID issuer, only HTTPS scheme will be accepted. "+ "If set, it will be used to verify the OIDC JSON Web Token (JWT).") - fs.StringVar(&o.OIDC.ClientID, "oidc-client-id", o.OIDC.ClientID, + fs.StringVar(&o.OIDC.ClientID, oidcClientIDFlag, o.OIDC.ClientID, "The client ID for the OpenID Connect client, must be set if oidc-issuer-url is set.") - fs.StringVar(&o.OIDC.CAFile, "oidc-ca-file", o.OIDC.CAFile, ""+ + fs.StringVar(&o.OIDC.CAFile, oidcCAFileFlag, o.OIDC.CAFile, ""+ "If set, the OpenID server's certificate will be verified by one of the authorities "+ "in the oidc-ca-file, otherwise the host's root CA set will be used.") - fs.StringVar(&o.OIDC.UsernameClaim, "oidc-username-claim", "sub", ""+ + fs.StringVar(&o.OIDC.UsernameClaim, oidcUsernameClaimFlag, "sub", ""+ "The OpenID claim to use as the user name. Note that claims other than the default ('sub') "+ "is not guaranteed to be unique and immutable. This flag is experimental, please see "+ "the authentication documentation for further details.") - fs.StringVar(&o.OIDC.UsernamePrefix, "oidc-username-prefix", "", ""+ + fs.StringVar(&o.OIDC.UsernamePrefix, oidcUsernamePrefixFlag, "", ""+ "If provided, all usernames will be prefixed with this value. If not provided, "+ "username claims other than 'email' are prefixed by the issuer URL to avoid "+ "clashes. To skip any prefixing, provide the value '-'.") - fs.StringVar(&o.OIDC.GroupsClaim, "oidc-groups-claim", "", ""+ + fs.StringVar(&o.OIDC.GroupsClaim, oidcGroupsClaimFlag, "", ""+ "If provided, the name of a custom OpenID Connect claim for specifying user groups. "+ "The claim value is expected to be a string or array of strings. This flag is experimental, "+ "please see the authentication documentation for further details.") - fs.StringVar(&o.OIDC.GroupsPrefix, "oidc-groups-prefix", "", ""+ + fs.StringVar(&o.OIDC.GroupsPrefix, oidcGroupsPrefixFlag, "", ""+ "If provided, all groups will be prefixed with this value to prevent conflicts with "+ "other authentication strategies.") - fs.StringSliceVar(&o.OIDC.SigningAlgs, "oidc-signing-algs", []string{"RS256"}, ""+ + fs.StringSliceVar(&o.OIDC.SigningAlgs, oidcSigningAlgsFlag, []string{"RS256"}, ""+ "Comma-separated list of allowed JOSE asymmetric signing algorithms. JWTs with a "+ "supported 'alg' header values are: RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512. "+ "Values are defined by RFC 7518 https://tools.ietf.org/html/rfc7518#section-3.1.") - fs.Var(cliflag.NewMapStringStringNoSplit(&o.OIDC.RequiredClaims), "oidc-required-claim", ""+ + fs.Var(cliflag.NewMapStringStringNoSplit(&o.OIDC.RequiredClaims), oidcRequiredClaimFlag, ""+ "A key=value pair that describes a required claim in the ID Token. "+ "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) || + fs.Changed(oidcCAFileFlag) || + fs.Changed(oidcUsernameClaimFlag) || + fs.Changed(oidcUsernamePrefixFlag) || + fs.Changed(oidcGroupsClaimFlag) || + fs.Changed(oidcGroupsPrefixFlag) || + fs.Changed(oidcSigningAlgsFlag) || + fs.Changed(oidcRequiredClaimFlag) + } } if o.RequestHeader != nil { @@ -401,7 +439,14 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat } } - if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 { + // When the StructuredAuthenticationConfiguration feature is enabled and the authentication config file is provided, + // load the authentication config from the file. + if len(o.AuthenticationConfigFile) > 0 { + var err error + if ret.AuthenticationConfig, err = loadAuthenticationConfig(o.AuthenticationConfigFile); err != nil { + return kubeauthenticator.Config{}, err + } + } else if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 { usernamePrefix := o.OIDC.UsernamePrefix if o.OIDC.UsernamePrefix == "" && o.OIDC.UsernameClaim != "email" { @@ -458,13 +503,17 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat authConfig := &apiserver.AuthenticationConfiguration{ JWT: []apiserver.JWTAuthenticator{jwtAuthenticator}, } - if err := apiservervalidation.ValidateAuthenticationConfiguration(authConfig).ToAggregate(); err != nil { - return kubeauthenticator.Config{}, err - } + ret.AuthenticationConfig = authConfig ret.OIDCSigningAlgs = o.OIDC.SigningAlgs } + if ret.AuthenticationConfig != nil { + if err := apiservervalidation.ValidateAuthenticationConfiguration(ret.AuthenticationConfig).ToAggregate(); err != nil { + return kubeauthenticator.Config{}, err + } + } + if o.RequestHeader != nil { var err error ret.RequestHeaderConfig, err = o.RequestHeader.ToAuthenticationRequestHeaderConfig() @@ -584,3 +633,62 @@ func (o *BuiltInAuthenticationOptions) ApplyAuthorization(authorization *BuiltIn o.Anonymous.Allow = false } } + +func (o *BuiltInAuthenticationOptions) validateOIDCOptions() []error { + var allErrors []error + + // Existing validation when jwt authenticator is configured with oidc-* flags + if len(o.AuthenticationConfigFile) == 0 { + if o.OIDC != nil && o.OIDC.areFlagsConfigured() && (len(o.OIDC.IssuerURL) == 0 || len(o.OIDC.ClientID) == 0) { + allErrors = append(allErrors, fmt.Errorf("oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set")) + } + + return allErrors + } + + // New validation when authentication config file is provided + + // Authentication config file is only supported when the StructuredAuthenticationConfiguration feature is enabled + if !utilfeature.DefaultFeatureGate.Enabled(genericfeatures.StructuredAuthenticationConfiguration) { + allErrors = append(allErrors, fmt.Errorf("set --feature-gates=%s=true to use authentication-config file", genericfeatures.StructuredAuthenticationConfiguration)) + } + + // Authentication config file and oidc-* flags are mutually exclusive + if o.OIDC != nil && o.OIDC.areFlagsConfigured() { + allErrors = append(allErrors, fmt.Errorf("authentication-config file and oidc-* flags are mutually exclusive")) + } + + return allErrors +} + +var ( + cfgScheme = runtime.NewScheme() + codecs = serializer.NewCodecFactory(cfgScheme, serializer.EnableStrict) +) + +func init() { + install.Install(cfgScheme) +} + +// loadAuthenticationConfig parses the authentication configuration from the given file and returns it. +func loadAuthenticationConfig(configFilePath string) (*apiserver.AuthenticationConfiguration, error) { + // read from file + data, err := os.ReadFile(configFilePath) + if err != nil { + return nil, err + } + if len(data) == 0 { + return nil, fmt.Errorf("empty config file %q", configFilePath) + } + + decodedObj, err := runtime.Decode(codecs.UniversalDecoder(), data) + if err != nil { + return nil, err + } + configuration, ok := decodedObj.(*apiserver.AuthenticationConfiguration) + if !ok { + return nil, fmt.Errorf("expected AuthenticationConfiguration, got %T", decodedObj) + } + + return configuration, nil +} diff --git a/pkg/kubeapiserver/options/authentication_test.go b/pkg/kubeapiserver/options/authentication_test.go index 0cb97e30a93..eed32f1cab8 100644 --- a/pkg/kubeapiserver/options/authentication_test.go +++ b/pkg/kubeapiserver/options/authentication_test.go @@ -32,18 +32,22 @@ import ( "k8s.io/apiserver/pkg/authentication/authenticator" "k8s.io/apiserver/pkg/authentication/authenticatorfactory" "k8s.io/apiserver/pkg/authentication/request/headerrequest" + "k8s.io/apiserver/pkg/features" apiserveroptions "k8s.io/apiserver/pkg/server/options" + utilfeature "k8s.io/apiserver/pkg/util/feature" + featuregatetesting "k8s.io/component-base/featuregate/testing" kubeauthenticator "k8s.io/kubernetes/pkg/kubeapiserver/authenticator" "k8s.io/utils/pointer" ) func TestAuthenticationValidate(t *testing.T) { testCases := []struct { - name string - testOIDC *OIDCAuthenticationOptions - testSA *ServiceAccountAuthenticationOptions - testWebHook *WebHookAuthenticationOptions - expectErr string + name string + testOIDC *OIDCAuthenticationOptions + testSA *ServiceAccountAuthenticationOptions + testWebHook *WebHookAuthenticationOptions + testAuthenticationConfigFile string + expectErr string }{ { name: "test when OIDC and ServiceAccounts are nil", @@ -51,10 +55,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when OIDC and ServiceAccounts are valid", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{"http://foo.bar.com"}, @@ -64,23 +69,25 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when OIDC is invalid", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{"http://foo.bar.com"}, KeyFiles: []string{"testkeyfile1", "testkeyfile2"}, }, - expectErr: "oidc-issuer-url and oidc-client-id should be specified together", + expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set", }, { name: "test when ServiceAccounts doesn't have key file", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{"http://foo.bar.com"}, @@ -90,10 +97,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when ServiceAccounts doesn't have issuer", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{}, @@ -103,10 +111,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when ServiceAccounts has empty string as issuer", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{""}, @@ -116,10 +125,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when ServiceAccounts has duplicate issuers", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{"http://foo.bar.com", "http://foo.bar.com"}, @@ -129,10 +139,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when ServiceAccount has bad issuer", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ Issuers: []string{"http://[::1]:namedport"}, @@ -142,10 +153,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when ServiceAccounts has invalid JWKSURI", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ KeyFiles: []string{"cert", "key"}, @@ -157,10 +169,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when ServiceAccounts has invalid JWKSURI (not https scheme)", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ KeyFiles: []string{"cert", "key"}, @@ -172,10 +185,11 @@ func TestAuthenticationValidate(t *testing.T) { { name: "test when WebHook has invalid retry attempts", testOIDC: &OIDCAuthenticationOptions{ - UsernameClaim: "sub", - SigningAlgs: []string{"RS256"}, - IssuerURL: "https://testIssuerURL", - ClientID: "testClientID", + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, }, testSA: &ServiceAccountAuthenticationOptions{ KeyFiles: []string{"cert", "key"}, @@ -195,6 +209,23 @@ func TestAuthenticationValidate(t *testing.T) { }, expectErr: "number of webhook retry attempts must be greater than 0, but is: 0", }, + { + name: "test when authentication config file is set without feature gate", + testAuthenticationConfigFile: "configfile", + expectErr: "set --feature-gates=StructuredAuthenticationConfiguration=true to use authentication-config file", + }, + { + name: "test when authentication config file and oidc-* flags are set", + testAuthenticationConfigFile: "configfile", + testOIDC: &OIDCAuthenticationOptions{ + UsernameClaim: "sub", + SigningAlgs: []string{"RS256"}, + IssuerURL: "https://testIssuerURL", + ClientID: "testClientID", + areFlagsConfigured: func() bool { return true }, + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, } for _, testcase := range testCases { @@ -203,6 +234,7 @@ func TestAuthenticationValidate(t *testing.T) { options.OIDC = testcase.testOIDC options.ServiceAccounts = testcase.testSA options.WebHook = testcase.testWebHook + options.AuthenticationConfigFile = testcase.testAuthenticationConfigFile errs := options.Validate() if len(errs) > 0 && (!strings.Contains(utilerrors.NewAggregate(errs).Error(), testcase.expectErr) || testcase.expectErr == "") { @@ -402,8 +434,14 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { t.Fatal(err) } + if !opts.OIDC.areFlagsConfigured() { + t.Fatal("OIDC flags should be configured") + } + // nil these out because you cannot compare functions + opts.OIDC.areFlagsConfigured = nil + if !reflect.DeepEqual(opts, expected) { - t.Error(cmp.Diff(opts, expected)) + t.Error(cmp.Diff(opts, expected, cmp.AllowUnexported(OIDCAuthenticationOptions{}))) } } @@ -624,3 +662,355 @@ func TestToAuthenticationConfig_OIDC(t *testing.T) { }) } } + +func TestValidateOIDCOptions(t *testing.T) { + testCases := []struct { + name string + args []string + structuredAuthenticationConfigEnabled bool + expectErr string + }{ + { + name: "issuer url and client id are not set", + args: []string{ + "--oidc-username-claim=testClaim", + }, + expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set", + }, + { + name: "issuer url set, client id is not set", + args: []string{ + "--oidc-issuer-url=https://testIssuerURL", + "--oidc-username-claim=testClaim", + }, + expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set", + }, + { + name: "issuer url is not set, client id is set", + args: []string{ + "--oidc-client-id=testClientID", + "--oidc-username-claim=testClaim", + }, + expectErr: "oidc-issuer-url and oidc-client-id must be specified together when any oidc-* flags are set", + }, + { + name: "issuer url and client id are set", + args: []string{ + "--oidc-client-id=testClientID", + "--oidc-issuer-url=https://testIssuerURL", + }, + expectErr: "", + }, + { + name: "authentication-config file, feature gate is not enabled", + args: []string{ + "--authentication-config=configfile", + }, + expectErr: "set --feature-gates=StructuredAuthenticationConfiguration=true to use authentication-config file", + }, + { + name: "authentication-config file, --oidc-issuer-url is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-issuer-url=https://testIssuerURL", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-client-id is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-client-id=testClientID", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-username-claim is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-username-claim=testClaim", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-username-prefix is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-username-prefix=testPrefix", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-ca-file is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-ca-file=testCAFile", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-groups-claim is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-groups-claim=testClaim", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-groups-prefix is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-groups-prefix=testPrefix", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-required-claim is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-required-claim=foo=bar", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-signature-algs is set", + args: []string{ + "--authentication-config=configfile", + "--oidc-signing-algs=RS512", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "authentication-config file, --oidc-username-claim flag not set, defaulting shouldn't error", + args: []string{ + "--authentication-config=configfile", + }, + expectErr: "", + structuredAuthenticationConfigEnabled: true, + }, + { + name: "authentication-config file, --oidc-username-claim flag explicitly set with default value should error", + args: []string{ + "--authentication-config=configfile", + "--oidc-username-claim=sub", + }, + expectErr: "authentication-config file and oidc-* flags are mutually exclusive", + }, + { + name: "valid authentication-config file", + args: []string{ + "--authentication-config=configfile", + }, + structuredAuthenticationConfigEnabled: true, + expectErr: "", + }, + } + + for _, tt := range testCases { + t.Run(tt.name, func(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, tt.structuredAuthenticationConfigEnabled)() + + opts := NewBuiltInAuthenticationOptions().WithOIDC() + pf := pflag.NewFlagSet("test-builtin-authentication-opts", pflag.ContinueOnError) + opts.AddFlags(pf) + + if err := pf.Parse(tt.args); err != nil { + t.Fatal(err) + } + + errs := opts.Validate() + if len(errs) > 0 && (!strings.Contains(utilerrors.NewAggregate(errs).Error(), tt.expectErr) || tt.expectErr == "") { + t.Errorf("Got err: %v, Expected err: %s", errs, tt.expectErr) + } + if len(errs) == 0 && len(tt.expectErr) != 0 { + t.Errorf("Got err nil, Expected err: %s", tt.expectErr) + } + if len(errs) > 0 && len(tt.expectErr) == 0 { + t.Errorf("Got err: %v, Expected err nil", errs) + } + }) + } +} + +func TestLoadAuthenticationConfig(t *testing.T) { + testCases := []struct { + name string + file func() string + expectErr string + expectedConfig *apiserver.AuthenticationConfiguration + }{ + { + name: "empty file", + file: func() string { return writeTempFile(t, ``) }, + expectErr: "empty config file", + expectedConfig: nil, + }, + { + name: "valid file", + file: func() string { + return writeTempFile(t, + `{ + "apiVersion":"apiserver.config.k8s.io/v1alpha1", + "kind":"AuthenticationConfiguration", + "jwt":[{"issuer":{"url": "https://test-issuer"}}]}`) + }, + expectErr: "", + expectedConfig: &apiserver.AuthenticationConfiguration{ + JWT: []apiserver.JWTAuthenticator{ + { + Issuer: apiserver.Issuer{URL: "https://test-issuer"}, + }, + }, + }, + }, + { + name: "missing file", + file: func() string { return "bogus-missing-file" }, + expectErr: "no such file or directory", + expectedConfig: nil, + }, + { + name: "invalid content file", + file: func() string { + return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthenticationConfiguration","authorizers":{"type":"Webhook"}}`) + }, + expectErr: `no kind "AuthenticationConfiguration" is registered for version "apiserver.config.k8s.io/v99"`, + expectedConfig: nil, + }, + { + name: "missing apiVersion", + file: func() string { return writeTempFile(t, `{"kind":"AuthenticationConfiguration"}`) }, + expectErr: `'apiVersion' is missing`, + }, + { + name: "missing kind", + file: func() string { return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v1alpha1"}`) }, + expectErr: `'Kind' is missing`, + }, + { + name: "unknown group", + file: func() string { + return writeTempFile(t, `{"apiVersion":"apps/v1alpha1","kind":"AuthenticationConfiguration"}`) + }, + expectErr: `apps/v1alpha1`, + }, + { + name: "unknown version", + file: func() string { + return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v99","kind":"AuthenticationConfiguration"}`) + }, + expectErr: `apiserver.config.k8s.io/v99`, + }, + { + name: "unknown kind", + file: func() string { + return writeTempFile(t, `{"apiVersion":"apiserver.config.k8s.io/v1alpha1","kind":"SomeConfiguration"}`) + }, + expectErr: `SomeConfiguration`, + }, + { + name: "unknown field", + file: func() string { + return writeTempFile(t, `{ + "apiVersion":"apiserver.config.k8s.io/v1alpha1", + "kind":"AuthenticationConfiguration", + "jwt1":[{"issuer":{"url": "https://test-issuer"}}]}`) + }, + expectErr: `unknown field "jwt1"`, + }, + { + name: "v1alpha1 - json", + file: func() string { + return writeTempFile(t, `{ + "apiVersion":"apiserver.config.k8s.io/v1alpha1", + "kind":"AuthenticationConfiguration", + "jwt":[{"issuer":{"url": "https://test-issuer"}}]}`) + }, + expectedConfig: &apiserver.AuthenticationConfiguration{ + JWT: []apiserver.JWTAuthenticator{ + { + Issuer: apiserver.Issuer{ + URL: "https://test-issuer", + }, + }, + }, + }, + }, + { + name: "v1alpha1 - yaml", + file: func() string { + return writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +jwt: +- issuer: + url: https://test-issuer + claimMappings: + username: + claim: sub + prefix: "" +`) + }, + expectedConfig: &apiserver.AuthenticationConfiguration{ + JWT: []apiserver.JWTAuthenticator{ + { + Issuer: apiserver.Issuer{ + URL: "https://test-issuer", + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "sub", + Prefix: pointer.String(""), + }, + }, + }, + }, + }, + }, + { + name: "v1alpha1 - no jwt", + file: func() string { + return writeTempFile(t, `{ + "apiVersion":"apiserver.config.k8s.io/v1alpha1", + "kind":"AuthenticationConfiguration"}`) + }, + expectedConfig: &apiserver.AuthenticationConfiguration{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + config, err := loadAuthenticationConfig(tc.file()) + if !strings.Contains(errString(err), tc.expectErr) { + t.Fatalf("expected error %q, got %v", tc.expectErr, err) + } + if !reflect.DeepEqual(config, tc.expectedConfig) { + t.Fatalf("unexpected config:\n%s", cmp.Diff(tc.expectedConfig, config)) + } + }) + } +} + +func writeTempFile(t *testing.T, content string) string { + t.Helper() + file, err := os.CreateTemp("", "config") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Remove(file.Name()); err != nil { + t.Fatal(err) + } + }) + if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil { + t.Fatal(err) + } + return file.Name() +} + +func errString(err error) string { + if err == nil { + return "" + } + return err.Error() +} diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/register.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/register.go index 14ba08482ae..7a5f6e5854f 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/register.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/register.go @@ -43,6 +43,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { ) scheme.AddKnownTypes(SchemeGroupVersion, &AdmissionConfiguration{}, + &AuthenticationConfiguration{}, &EgressSelectorConfiguration{}, &TracingConfiguration{}, ) diff --git a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/register.go b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/register.go index e4e16c01ce4..dc5d3be24bb 100644 --- a/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/register.go +++ b/staging/src/k8s.io/apiserver/pkg/apis/apiserver/v1alpha1/register.go @@ -53,6 +53,7 @@ func addKnownTypes(scheme *runtime.Scheme) error { &EgressSelectorConfiguration{}, ) scheme.AddKnownTypes(ConfigSchemeGroupVersion, + &AuthenticationConfiguration{}, &TracingConfiguration{}, ) metav1.AddToGroupVersion(scheme, SchemeGroupVersion) From 15c02f2a034d23174d7c9ef1bb61982cc36126a5 Mon Sep 17 00:00:00 2001 From: Anish Ramasekar Date: Thu, 10 Aug 2023 22:52:15 +0000 Subject: [PATCH 3/3] add integration tests Signed-off-by: Anish Ramasekar --- test/integration/apiserver/oidc/oidc_test.go | 107 ++++++++++++++++--- 1 file changed, 92 insertions(+), 15 deletions(-) diff --git a/test/integration/apiserver/oidc/oidc_test.go b/test/integration/apiserver/oidc/oidc_test.go index be6d5eaa97b..b8df34bdfc1 100644 --- a/test/integration/apiserver/oidc/oidc_test.go +++ b/test/integration/apiserver/oidc/oidc_test.go @@ -27,23 +27,28 @@ import ( "net" "net/http" "net/url" + "os" "path/filepath" + "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/features" + utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/kubernetes" _ "k8s.io/client-go/plugin/pkg/client/auth/oidc" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd/api" certutil "k8s.io/client-go/util/cert" + featuregatetesting "k8s.io/component-base/featuregate/testing" kubeapiserverapptesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing" "k8s.io/kubernetes/pkg/apis/rbac" - "k8s.io/kubernetes/pkg/kubeapiserver/authorizer/modes" "k8s.io/kubernetes/test/integration/framework" utilsoidc "k8s.io/kubernetes/test/utils/oidc" utilsnet "k8s.io/utils/net" @@ -95,9 +100,21 @@ var ( ) func TestOIDC(t *testing.T) { + t.Log("Testing OIDC authenticator with --oidc-* flags") + runTests(t, false) +} + +func TestStructuredAuthenticationConfig(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)() + + t.Log("Testing OIDC authenticator with authentication config") + runTests(t, true) +} + +func runTests(t *testing.T, useAuthenticationConfig bool) { var tests = []struct { name string - configureInfrastructure func(t *testing.T) ( + configureInfrastructure func(t *testing.T, useAuthenticationConfig bool) ( oidcServer *utilsoidc.TestServer, apiServer *kubeapiserverapptesting.TestServer, signingPrivateKey *rsa.PrivateKey, @@ -207,7 +224,7 @@ func TestOIDC(t *testing.T) { }, { name: "ID token signature can not be verified due to wrong JWKs", - configureInfrastructure: func(t *testing.T) ( + configureInfrastructure: func(t *testing.T, useAuthenticationConfig bool) ( oidcServer *utilsoidc.TestServer, apiServer *kubeapiserverapptesting.TestServer, signingPrivateKey *rsa.PrivateKey, @@ -220,7 +237,13 @@ func TestOIDC(t *testing.T) { require.NoError(t, wantErr) oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath) - apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath) + + if useAuthenticationConfig { + authenticationConfig := generateAuthenticationConfig(t, oidcServer.URL(), defaultOIDCClientID, string(caCertContent), defaultOIDCUsernamePrefix) + apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig) + } else { + apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "") + } adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig) configureRBAC(t, adminClient, defaultRole, defaultRoleBinding) @@ -252,7 +275,7 @@ func TestOIDC(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t) + oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, useAuthenticationConfig) tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey) @@ -304,7 +327,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) { }, } - oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t) + oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, false) tokenURL, err := oidcServer.TokenURL() require.NoError(t, err) @@ -333,7 +356,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) { } } -func configureTestInfrastructure(t *testing.T) ( +func configureTestInfrastructure(t *testing.T, useAuthenticationConfig bool) ( oidcServer *utilsoidc.TestServer, apiServer *kubeapiserverapptesting.TestServer, signingPrivateKey *rsa.PrivateKey, @@ -348,7 +371,13 @@ func configureTestInfrastructure(t *testing.T) ( require.NoError(t, err) oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath) - apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath) + + if useAuthenticationConfig { + authenticationConfig := generateAuthenticationConfig(t, oidcServer.URL(), defaultOIDCClientID, string(caCertContent), defaultOIDCUsernamePrefix) + apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig) + } else { + apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "") + } oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehaviour(t, &signingPrivateKey.PublicKey)) @@ -399,19 +428,26 @@ func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, c return cfg } -func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath string) *kubeapiserverapptesting.TestServer { +func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string) *kubeapiserverapptesting.TestServer { t.Helper() - server, err := kubeapiserverapptesting.StartTestServer( - t, - kubeapiserverapptesting.NewDefaultTestServerOptions(), - []string{ + var customFlags []string + if authenticationConfigYAML != "" { + customFlags = []string{fmt.Sprintf("--authentication-config=%s", writeTempFile(t, authenticationConfigYAML))} + } else { + customFlags = []string{ fmt.Sprintf("--oidc-issuer-url=%s", oidcURL), fmt.Sprintf("--oidc-client-id=%s", oidcClientID), fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath), fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix), - fmt.Sprintf("--authorization-mode=%s", modes.ModeRBAC), - }, + } + } + customFlags = append(customFlags, "--authorization-mode=RBAC") + + server, err := kubeapiserverapptesting.StartTestServer( + t, + kubeapiserverapptesting.NewDefaultTestServerOptions(), + customFlags, framework.SharedEtcd(), ) require.NoError(t, err) @@ -494,3 +530,44 @@ func generateCert(t *testing.T) (cert, key []byte, certFilePath, keyFilePath str return cert, key, certFilePath, keyFilePath } + +func writeTempFile(t *testing.T, content string) string { + t.Helper() + file, err := os.CreateTemp("", "oidc-test") + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { + if err := os.Remove(file.Name()); err != nil { + t.Fatal(err) + } + }) + if err := os.WriteFile(file.Name(), []byte(content), 0600); err != nil { + t.Fatal(err) + } + return file.Name() +} + +func generateAuthenticationConfig(t *testing.T, issuerURL, clientID, caCert, usernamePrefix string) string { + t.Helper() + + // Indent the certificate authority to match the format of the generated + // authentication config. + caCert = strings.ReplaceAll(caCert, "\n", "\n ") + + return fmt.Sprintf(` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +jwt: +- issuer: + url: %s + audiences: + - %s + certificateAuthority: | + %s + claimMappings: + username: + claim: sub + prefix: %s +`, issuerURL, clientID, string(caCert), usernamePrefix) +}