Merge pull request #119142 from aramase/aramase/f/kep_3331_add_feature_flag

[StructuredAuthenticationConfig] Add feature flag and wire up `--authentication-config` flag
This commit is contained in:
Kubernetes Prow Robot 2023-09-05 13:08:51 -07:00 committed by GitHub
commit f68c66f96d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 676 additions and 90 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -43,6 +43,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
)
scheme.AddKnownTypes(SchemeGroupVersion,
&AdmissionConfiguration{},
&AuthenticationConfiguration{},
&EgressSelectorConfiguration{},
&TracingConfiguration{},
)

View File

@ -53,6 +53,7 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&EgressSelectorConfiguration{},
)
scheme.AddKnownTypes(ConfigSchemeGroupVersion,
&AuthenticationConfiguration{},
&TracingConfiguration{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)

View File

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

View File

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