Skip to content

Commit 26600b1

Browse files
authored
Merge pull request kubernetes#123561 from enj/enj/i/validate_jwt_sa_iss
Prevent conflicts between service account and jwt issuers
2 parents a76a3e0 + 05e1eff commit 26600b1

File tree

6 files changed

+117
-22
lines changed

6 files changed

+117
-22
lines changed

pkg/kubeapiserver/authenticator/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ func (config Config) New() (authenticator.Request, *spec.SecurityDefinitions, sp
162162
JWTAuthenticator: jwtAuthenticator,
163163
CAContentProvider: oidcCAContent,
164164
SupportedSigningAlgs: config.OIDCSigningAlgs,
165+
DisallowedIssuers: config.ServiceAccountIssuers,
165166
})
166167
if err != nil {
167168
return nil, nil, nil, err

pkg/kubeapiserver/options/authentication.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -533,7 +533,7 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat
533533
}
534534

535535
if ret.AuthenticationConfig != nil {
536-
if err := apiservervalidation.ValidateAuthenticationConfiguration(ret.AuthenticationConfig).ToAggregate(); err != nil {
536+
if err := apiservervalidation.ValidateAuthenticationConfiguration(ret.AuthenticationConfig, ret.ServiceAccountIssuers).ToAggregate(); err != nil {
537537
return kubeauthenticator.Config{}, err
538538
}
539539
}

staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import (
4141
)
4242

4343
// ValidateAuthenticationConfiguration validates a given AuthenticationConfiguration.
44-
func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) field.ErrorList {
44+
func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration, disallowedIssuers []string) field.ErrorList {
4545
root := field.NewPath("jwt")
4646
var allErrs field.ErrorList
4747

@@ -69,7 +69,7 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) fie
6969
// check and add validation for duplicate issuers.
7070
for i, a := range c.JWT {
7171
fldPath := root.Index(i)
72-
_, errs := validateJWTAuthenticator(a, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
72+
_, errs := validateJWTAuthenticator(a, fldPath, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
7373
allErrs = append(allErrs, errs...)
7474
}
7575

@@ -79,41 +79,41 @@ func ValidateAuthenticationConfiguration(c *api.AuthenticationConfiguration) fie
7979
// CompileAndValidateJWTAuthenticator validates a given JWTAuthenticator and returns a CELMapper with the compiled
8080
// CEL expressions for claim mappings and validation rules.
8181
// This is exported for use in oidc package.
82-
func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator) (authenticationcel.CELMapper, field.ErrorList) {
83-
return validateJWTAuthenticator(authenticator, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
82+
func CompileAndValidateJWTAuthenticator(authenticator api.JWTAuthenticator, disallowedIssuers []string) (authenticationcel.CELMapper, field.ErrorList) {
83+
return validateJWTAuthenticator(authenticator, nil, sets.New(disallowedIssuers...), utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthenticationConfiguration))
8484
}
8585

86-
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
86+
func validateJWTAuthenticator(authenticator api.JWTAuthenticator, fldPath *field.Path, disallowedIssuers sets.Set[string], structuredAuthnFeatureEnabled bool) (authenticationcel.CELMapper, field.ErrorList) {
8787
var allErrs field.ErrorList
8888

8989
compiler := authenticationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
9090
mapper := &authenticationcel.CELMapper{}
9191

92-
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, fldPath.Child("issuer"))...)
92+
allErrs = append(allErrs, validateIssuer(authenticator.Issuer, disallowedIssuers, fldPath.Child("issuer"))...)
9393
allErrs = append(allErrs, validateClaimValidationRules(compiler, mapper, authenticator.ClaimValidationRules, fldPath.Child("claimValidationRules"), structuredAuthnFeatureEnabled)...)
9494
allErrs = append(allErrs, validateClaimMappings(compiler, mapper, authenticator.ClaimMappings, fldPath.Child("claimMappings"), structuredAuthnFeatureEnabled)...)
9595
allErrs = append(allErrs, validateUserValidationRules(compiler, mapper, authenticator.UserValidationRules, fldPath.Child("userValidationRules"), structuredAuthnFeatureEnabled)...)
9696

9797
return *mapper, allErrs
9898
}
9999

100-
func validateIssuer(issuer api.Issuer, fldPath *field.Path) field.ErrorList {
100+
func validateIssuer(issuer api.Issuer, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList {
101101
var allErrs field.ErrorList
102102

103-
allErrs = append(allErrs, validateIssuerURL(issuer.URL, fldPath.Child("url"))...)
103+
allErrs = append(allErrs, validateIssuerURL(issuer.URL, disallowedIssuers, fldPath.Child("url"))...)
104104
allErrs = append(allErrs, validateIssuerDiscoveryURL(issuer.URL, issuer.DiscoveryURL, fldPath.Child("discoveryURL"))...)
105105
allErrs = append(allErrs, validateAudiences(issuer.Audiences, issuer.AudienceMatchPolicy, fldPath.Child("audiences"), fldPath.Child("audienceMatchPolicy"))...)
106106
allErrs = append(allErrs, validateCertificateAuthority(issuer.CertificateAuthority, fldPath.Child("certificateAuthority"))...)
107107

108108
return allErrs
109109
}
110110

111-
func validateIssuerURL(issuerURL string, fldPath *field.Path) field.ErrorList {
111+
func validateIssuerURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList {
112112
if len(issuerURL) == 0 {
113113
return field.ErrorList{field.Required(fldPath, "URL is required")}
114114
}
115115

116-
return validateURL(issuerURL, fldPath)
116+
return validateURL(issuerURL, disallowedIssuers, fldPath)
117117
}
118118

119119
func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *field.Path) field.ErrorList {
@@ -127,13 +127,18 @@ func validateIssuerDiscoveryURL(issuerURL, issuerDiscoveryURL string, fldPath *f
127127
allErrs = append(allErrs, field.Invalid(fldPath, issuerDiscoveryURL, "discoveryURL must be different from URL"))
128128
}
129129

130-
allErrs = append(allErrs, validateURL(issuerDiscoveryURL, fldPath)...)
130+
// issuerDiscoveryURL is not an issuer URL and does not need to validated against any set of disallowed issuers
131+
allErrs = append(allErrs, validateURL(issuerDiscoveryURL, nil, fldPath)...)
131132
return allErrs
132133
}
133134

134-
func validateURL(issuerURL string, fldPath *field.Path) field.ErrorList {
135+
func validateURL(issuerURL string, disallowedIssuers sets.Set[string], fldPath *field.Path) field.ErrorList {
135136
var allErrs field.ErrorList
136137

138+
if disallowedIssuers.Has(issuerURL) {
139+
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, fmt.Sprintf("URL must not overlap with disallowed issuers: %s", sets.List(disallowedIssuers))))
140+
}
141+
137142
u, err := url.Parse(issuerURL)
138143
if err != nil {
139144
allErrs = append(allErrs, field.Invalid(fldPath, issuerURL, err.Error()))

staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go

Lines changed: 43 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,10 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
5050
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)()
5151

5252
testCases := []struct {
53-
name string
54-
in *api.AuthenticationConfiguration
55-
want string
53+
name string
54+
in *api.AuthenticationConfiguration
55+
disallowedIssuers []string
56+
want string
5657
}{
5758
{
5859
name: "jwt authenticator is empty",
@@ -174,6 +175,33 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
174175
},
175176
want: `jwt[0].userValidationRules[1].expression: Duplicate value: "user.username == 'foo'"`,
176177
},
178+
{
179+
name: "valid authentication configuration with disallowed issuer",
180+
in: &api.AuthenticationConfiguration{
181+
JWT: []api.JWTAuthenticator{
182+
{
183+
Issuer: api.Issuer{
184+
URL: "https://issuer-url",
185+
Audiences: []string{"audience"},
186+
},
187+
ClaimValidationRules: []api.ClaimValidationRule{
188+
{
189+
Claim: "foo",
190+
RequiredValue: "bar",
191+
},
192+
},
193+
ClaimMappings: api.ClaimMappings{
194+
Username: api.PrefixedClaimOrExpression{
195+
Claim: "sub",
196+
Prefix: pointer.String("prefix"),
197+
},
198+
},
199+
},
200+
},
201+
},
202+
disallowedIssuers: []string{"a", "b", "https://issuer-url", "c"},
203+
want: `jwt[0].issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [a b c https://issuer-url]`,
204+
},
177205
{
178206
name: "valid authentication configuration",
179207
in: &api.AuthenticationConfiguration{
@@ -204,7 +232,7 @@ func TestValidateAuthenticationConfiguration(t *testing.T) {
204232

205233
for _, tt := range testCases {
206234
t.Run(tt.name, func(t *testing.T) {
207-
got := ValidateAuthenticationConfiguration(tt.in).ToAggregate()
235+
got := ValidateAuthenticationConfiguration(tt.in, tt.disallowedIssuers).ToAggregate()
208236
if d := cmp.Diff(tt.want, errString(got)); d != "" {
209237
t.Fatalf("AuthenticationConfiguration validation mismatch (-want +got):\n%s", d)
210238
}
@@ -216,9 +244,10 @@ func TestValidateIssuerURL(t *testing.T) {
216244
fldPath := field.NewPath("issuer", "url")
217245

218246
testCases := []struct {
219-
name string
220-
in string
221-
want string
247+
name string
248+
in string
249+
disallowedIssuers sets.Set[string]
250+
want string
222251
}{
223252
{
224253
name: "url is empty",
@@ -250,6 +279,12 @@ func TestValidateIssuerURL(t *testing.T) {
250279
in: "https://issuer-url#fragment",
251280
want: `issuer.url: Invalid value: "https://issuer-url#fragment": URL must not contain a fragment`,
252281
},
282+
{
283+
name: "valid url that is disallowed",
284+
in: "https://issuer-url",
285+
disallowedIssuers: sets.New("https://issuer-url"),
286+
want: `issuer.url: Invalid value: "https://issuer-url": URL must not overlap with disallowed issuers: [https://issuer-url]`,
287+
},
253288
{
254289
name: "valid url",
255290
in: "https://issuer-url",
@@ -259,7 +294,7 @@ func TestValidateIssuerURL(t *testing.T) {
259294

260295
for _, tt := range testCases {
261296
t.Run(tt.name, func(t *testing.T) {
262-
got := validateIssuerURL(tt.in, fldPath).ToAggregate()
297+
got := validateIssuerURL(tt.in, tt.disallowedIssuers, fldPath).ToAggregate()
263298
if d := cmp.Diff(tt.want, errString(got)); d != "" {
264299
t.Fatalf("URL validation mismatch (-want +got):\n%s", d)
265300
}

staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ type Options struct {
9494
// https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation
9595
SupportedSigningAlgs []string
9696

97+
DisallowedIssuers []string
98+
9799
// now is used for testing. It defaults to time.Now.
98100
now func() time.Time
99101
}
@@ -227,7 +229,7 @@ var allowedSigningAlgs = map[string]bool{
227229
}
228230

229231
func New(opts Options) (authenticator.Token, error) {
230-
celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(opts.JWTAuthenticator)
232+
celMapper, fieldErr := apiservervalidation.CompileAndValidateJWTAuthenticator(opts.JWTAuthenticator, opts.DisallowedIssuers)
231233
if err := fieldErr.ToAggregate(); err != nil {
232234
return nil, err
233235
}

staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2772,6 +2772,58 @@ func TestToken(t *testing.T) {
27722772
}`, valid.Unix()),
27732773
wantInitErr: `claimMappings.extra[2].key: Duplicate value: "example.org/foo"`,
27742774
},
2775+
{
2776+
name: "disallowed issuer via configured value",
2777+
options: Options{
2778+
JWTAuthenticator: apiserver.JWTAuthenticator{
2779+
Issuer: apiserver.Issuer{
2780+
URL: "https://auth.example.com",
2781+
Audiences: []string{"my-client"},
2782+
},
2783+
ClaimMappings: apiserver.ClaimMappings{
2784+
Username: apiserver.PrefixedClaimOrExpression{
2785+
Expression: "claims.username",
2786+
},
2787+
Groups: apiserver.PrefixedClaimOrExpression{
2788+
Expression: "claims.groups",
2789+
},
2790+
UID: apiserver.ClaimOrExpression{
2791+
Expression: "claims.uid",
2792+
},
2793+
Extra: []apiserver.ExtraMapping{
2794+
{
2795+
Key: "example.org/foo",
2796+
ValueExpression: "claims.foo",
2797+
},
2798+
{
2799+
Key: "example.org/bar",
2800+
ValueExpression: "claims.bar",
2801+
},
2802+
},
2803+
},
2804+
},
2805+
DisallowedIssuers: []string{"https://auth.example.com"},
2806+
now: func() time.Time { return now },
2807+
},
2808+
signingKey: loadRSAPrivKey(t, "testdata/rsa_1.pem", jose.RS256),
2809+
pubKeys: []*jose.JSONWebKey{
2810+
loadRSAKey(t, "testdata/rsa_1.pem", jose.RS256),
2811+
},
2812+
claims: fmt.Sprintf(`{
2813+
"iss": "https://auth.example.com",
2814+
"aud": "my-client",
2815+
"username": "jane",
2816+
"groups": ["team1", "team2"],
2817+
"exp": %d,
2818+
"uid": "1234",
2819+
"foo": "bar",
2820+
"bar": [
2821+
"baz",
2822+
"qux"
2823+
]
2824+
}`, valid.Unix()),
2825+
wantInitErr: `issuer.url: Invalid value: "https://auth.example.com": URL must not overlap with disallowed issuers: [https://auth.example.com]`,
2826+
},
27752827
{
27762828
name: "extra claim mapping, empty string value for key",
27772829
options: Options{

0 commit comments

Comments
 (0)