From 05eaef6f20ec7d1a623b96b6dd06aafdabf569de Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Wed, 14 Feb 2024 15:43:32 -0800 Subject: [PATCH 1/5] api: useForRouting field in JWT Add a field called `useForRouting` that signals to Envoy Gateway that the headers generated from the claims are used to make routing decisions Internally this field will be used to * insert a catch-all route with a 404 direct response identical to https://github.com/envoyproxy/gateway/pull/2586 which makes sure the jwt filter with `claimToHeader` is applied before recomputing routing decision * enable `clear_route_cache` to recompute routing decision https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto#extensions-filters-http-jwt-authn-v3-jwtprovider Relates to https://github.com/envoyproxy/gateway/issues/2452 Signed-off-by: Arko Dasgupta --- api/v1alpha1/jwt_types.go | 4 ++++ api/v1alpha1/zz_generated.deepcopy.go | 9 ++++++++- site/content/en/latest/api/extension_types.md | 1 + 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/jwt_types.go b/api/v1alpha1/jwt_types.go index 43e22274338..ac65254e1e7 100644 --- a/api/v1alpha1/jwt_types.go +++ b/api/v1alpha1/jwt_types.go @@ -85,6 +85,10 @@ type ClaimToHeader struct { // (eg. "claim.nested.key", "sub"). The nested claim name must use dot "." // to separate the JSON name path. Claim string `json:"claim"` + + // UseForRouting must be enabled if this header generated from the claim should be used for + // route matching decisions + UseForRouting *bool `json:"useForRouting,omitempty"` } // JWTExtractor defines a custom JWT token extraction from HTTP request. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index d38d710885d..512878682d5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -366,6 +366,11 @@ func (in *CircuitBreaker) DeepCopy() *CircuitBreaker { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClaimToHeader) DeepCopyInto(out *ClaimToHeader) { *out = *in + if in.UseForRouting != nil { + in, out := &in.UseForRouting, &out.UseForRouting + *out = new(bool) + **out = **in + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimToHeader. @@ -2024,7 +2029,9 @@ func (in *JWTProvider) DeepCopyInto(out *JWTProvider) { if in.ClaimToHeaders != nil { in, out := &in.ClaimToHeaders, &out.ClaimToHeaders *out = make([]ClaimToHeader, len(*in)) - copy(*out, *in) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } } if in.ExtractFrom != nil { in, out := &in.ExtractFrom, &out.ExtractFrom diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 07cfc0dc8ba..8609104dee4 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -245,6 +245,7 @@ _Appears in:_ | --- | --- | --- | --- | | `header` | _string_ | true | Header defines the name of the HTTP request header that the JWT Claim will be saved into. | | `claim` | _string_ | true | Claim is the JWT Claim that should be saved into the header : it can be a nested claim of type (eg. "claim.nested.key", "sub"). The nested claim name must use dot "." to separate the JSON name path. | +| `useForRouting` | _boolean_ | true | UseForRouting must be enabled if this header generated from the claim should be used for route matching decisions | #### ClientIPDetectionSettings From a8dfa6bc406636b0d08ec828bfded626f43ee853 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Wed, 14 Feb 2024 15:54:00 -0800 Subject: [PATCH 2/5] helm Signed-off-by: Arko Dasgupta --- .../generated/gateway.envoyproxy.io_securitypolicies.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 7feb835b938..9c5b7161022 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -397,6 +397,11 @@ spec: description: Header defines the name of the HTTP request header that the JWT Claim will be saved into. type: string + useForRouting: + description: UseForRouting must be enabled if this + header generated from the claim should be used for + route matching decisions + type: boolean required: - claim - header From 9241363d2a84fef349f44bb2e5559ea9b01afa5b Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Wed, 14 Feb 2024 17:59:25 -0800 Subject: [PATCH 3/5] optional Signed-off-by: Arko Dasgupta --- api/v1alpha1/jwt_types.go | 1 + site/content/en/latest/api/extension_types.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/jwt_types.go b/api/v1alpha1/jwt_types.go index ac65254e1e7..99f7e9204b4 100644 --- a/api/v1alpha1/jwt_types.go +++ b/api/v1alpha1/jwt_types.go @@ -88,6 +88,7 @@ type ClaimToHeader struct { // UseForRouting must be enabled if this header generated from the claim should be used for // route matching decisions + // +optional UseForRouting *bool `json:"useForRouting,omitempty"` } diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 8609104dee4..7dea7d47f6b 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -245,7 +245,7 @@ _Appears in:_ | --- | --- | --- | --- | | `header` | _string_ | true | Header defines the name of the HTTP request header that the JWT Claim will be saved into. | | `claim` | _string_ | true | Claim is the JWT Claim that should be saved into the header : it can be a nested claim of type (eg. "claim.nested.key", "sub"). The nested claim name must use dot "." to separate the JSON name path. | -| `useForRouting` | _boolean_ | true | UseForRouting must be enabled if this header generated from the claim should be used for route matching decisions | +| `useForRouting` | _boolean_ | false | UseForRouting must be enabled if this header generated from the claim should be used for route matching decisions | #### ClientIPDetectionSettings From 0276f45207a340d795af07160f6ad9b1c939cae7 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Tue, 20 Feb 2024 13:33:00 -0800 Subject: [PATCH 4/5] rename to recomputeRoute Signed-off-by: Arko Dasgupta --- api/v1alpha1/jwt_types.go | 13 ++++++++----- api/v1alpha1/zz_generated.deepcopy.go | 14 ++++++-------- .../gateway.envoyproxy.io_securitypolicies.yaml | 11 ++++++----- site/content/en/latest/api/extension_types.md | 4 ++-- 4 files changed, 22 insertions(+), 20 deletions(-) diff --git a/api/v1alpha1/jwt_types.go b/api/v1alpha1/jwt_types.go index 99f7e9204b4..6573005fa10 100644 --- a/api/v1alpha1/jwt_types.go +++ b/api/v1alpha1/jwt_types.go @@ -52,8 +52,16 @@ type JWTProvider struct { // For examples, following config: // The claim must be of type; string, int, double, bool. Array type claims are not supported // + // +optional ClaimToHeaders []ClaimToHeader `json:"claimToHeaders,omitempty"` + // RecomputeRoute clears the route cache and recalculates the routing decision. + // This field must be enabled if the headers generated from the claim are used for + // route matching decisions. + // + // +optional + RecomputeRoute *bool `json:"recomputeRoute,omitempty"` + // ExtractFrom defines different ways to extract the JWT token from HTTP request. // If empty, it defaults to extract JWT token from the Authorization HTTP request header using Bearer schema // or access_token from query parameters. @@ -85,11 +93,6 @@ type ClaimToHeader struct { // (eg. "claim.nested.key", "sub"). The nested claim name must use dot "." // to separate the JSON name path. Claim string `json:"claim"` - - // UseForRouting must be enabled if this header generated from the claim should be used for - // route matching decisions - // +optional - UseForRouting *bool `json:"useForRouting,omitempty"` } // JWTExtractor defines a custom JWT token extraction from HTTP request. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 512878682d5..750a39b5915 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -366,11 +366,6 @@ func (in *CircuitBreaker) DeepCopy() *CircuitBreaker { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClaimToHeader) DeepCopyInto(out *ClaimToHeader) { *out = *in - if in.UseForRouting != nil { - in, out := &in.UseForRouting, &out.UseForRouting - *out = new(bool) - **out = **in - } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClaimToHeader. @@ -2029,9 +2024,12 @@ func (in *JWTProvider) DeepCopyInto(out *JWTProvider) { if in.ClaimToHeaders != nil { in, out := &in.ClaimToHeaders, &out.ClaimToHeaders *out = make([]ClaimToHeader, len(*in)) - for i := range *in { - (*in)[i].DeepCopyInto(&(*out)[i]) - } + copy(*out, *in) + } + if in.RecomputeRoute != nil { + in, out := &in.RecomputeRoute, &out.RecomputeRoute + *out = new(bool) + **out = **in } if in.ExtractFrom != nil { in, out := &in.ExtractFrom, &out.ExtractFrom diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index 9c5b7161022..bd900226038 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -397,11 +397,6 @@ spec: description: Header defines the name of the HTTP request header that the JWT Claim will be saved into. type: string - useForRouting: - description: UseForRouting must be enabled if this - header generated from the claim should be used for - route matching decisions - type: boolean required: - claim - header @@ -466,6 +461,12 @@ spec: maxLength: 253 minLength: 1 type: string + recomputeRoute: + description: RecomputeRoute clears the route cache and recalculates + the routing decision. This field must be enabled if the + headers generated from the claim are used for route matching + decisions. + type: boolean remoteJWKS: description: RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 7dea7d47f6b..90ea6aa3da7 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -245,7 +245,6 @@ _Appears in:_ | --- | --- | --- | --- | | `header` | _string_ | true | Header defines the name of the HTTP request header that the JWT Claim will be saved into. | | `claim` | _string_ | true | Claim is the JWT Claim that should be saved into the header : it can be a nested claim of type (eg. "claim.nested.key", "sub"). The nested claim name must use dot "." to separate the JSON name path. | -| `useForRouting` | _boolean_ | false | UseForRouting must be enabled if this header generated from the claim should be used for route matching decisions | #### ClientIPDetectionSettings @@ -1375,7 +1374,8 @@ _Appears in:_ | `issuer` | _string_ | false | Issuer is the principal that issued the JWT and takes the form of a URL or email address. For additional details, see https://tools.ietf.org/html/rfc7519#section-4.1.1 for URL format and https://rfc-editor.org/rfc/rfc5322.html for email format. If not provided, the JWT issuer is not checked. | | `audiences` | _string array_ | false | Audiences is a list of JWT audiences allowed access. For additional details, see https://tools.ietf.org/html/rfc7519#section-4.1.3. If not provided, JWT audiences are not checked. | | `remoteJWKS` | _[RemoteJWKS](#remotejwks)_ | true | RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. | -| `claimToHeaders` | _[ClaimToHeader](#claimtoheader) array_ | true | ClaimToHeaders is a list of JWT claims that must be extracted into HTTP request headers For examples, following config: The claim must be of type; string, int, double, bool. Array type claims are not supported | +| `claimToHeaders` | _[ClaimToHeader](#claimtoheader) array_ | false | ClaimToHeaders is a list of JWT claims that must be extracted into HTTP request headers For examples, following config: The claim must be of type; string, int, double, bool. Array type claims are not supported | +| `recomputeRoute` | _boolean_ | false | RecomputeRoute clears the route cache and recalculates the routing decision. This field must be enabled if the headers generated from the claim are used for route matching decisions. | | `extractFrom` | _[JWTExtractor](#jwtextractor)_ | false | ExtractFrom defines different ways to extract the JWT token from HTTP request. If empty, it defaults to extract JWT token from the Authorization HTTP request header using Bearer schema or access_token from query parameters. | From bc439f03baa0ec881301bac8c13fd07f7e83c600 Mon Sep 17 00:00:00 2001 From: Arko Dasgupta Date: Thu, 22 Feb 2024 17:38:44 -0800 Subject: [PATCH 5/5] address comments Signed-off-by: Arko Dasgupta --- api/v1alpha1/jwt_types.go | 4 +- ...ateway.envoyproxy.io_securitypolicies.yaml | 8 +- site/content/en/latest/api/extension_types.md | 2 +- test/cel-validation/securitypolicy_test.go | 115 ++++++++++++++++++ 4 files changed, 126 insertions(+), 3 deletions(-) diff --git a/api/v1alpha1/jwt_types.go b/api/v1alpha1/jwt_types.go index 6573005fa10..4948669a292 100644 --- a/api/v1alpha1/jwt_types.go +++ b/api/v1alpha1/jwt_types.go @@ -19,6 +19,7 @@ type JWT struct { } // JWTProvider defines how a JSON Web Token (JWT) can be verified. +// +kubebuilder:validation:XValidation:rule="(has(self.recomputeRoute) && self.recomputeRoute) ? size(self.claimToHeaders) > 0 : true", message="claimToHeaders must be specified if recomputeRoute is enabled" type JWTProvider struct { // Name defines a unique name for the JWT provider. A name can have a variety of forms, // including RFC1123 subdomains, RFC 1123 labels, or RFC 1035 labels. @@ -57,7 +58,8 @@ type JWTProvider struct { // RecomputeRoute clears the route cache and recalculates the routing decision. // This field must be enabled if the headers generated from the claim are used for - // route matching decisions. + // route matching decisions. If the recomputation selects a new route, features targeting + // the new matched route will be applied. // // +optional RecomputeRoute *bool `json:"recomputeRoute,omitempty"` diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml index bd900226038..1ed6395b781 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -465,7 +465,8 @@ spec: description: RecomputeRoute clears the route cache and recalculates the routing decision. This field must be enabled if the headers generated from the claim are used for route matching - decisions. + decisions. If the recomputation selects a new route, features + targeting the new matched route will be applied. type: boolean remoteJWKS: description: RemoteJWKS defines how to fetch and cache JSON @@ -485,6 +486,11 @@ spec: - name - remoteJWKS type: object + x-kubernetes-validations: + - message: claimToHeaders must be specified if recomputeRoute + is enabled + rule: '(has(self.recomputeRoute) && self.recomputeRoute) ? + size(self.claimToHeaders) > 0 : true' maxItems: 4 minItems: 1 type: array diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 90ea6aa3da7..fbda31d8a26 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -1375,7 +1375,7 @@ _Appears in:_ | `audiences` | _string array_ | false | Audiences is a list of JWT audiences allowed access. For additional details, see https://tools.ietf.org/html/rfc7519#section-4.1.3. If not provided, JWT audiences are not checked. | | `remoteJWKS` | _[RemoteJWKS](#remotejwks)_ | true | RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. | | `claimToHeaders` | _[ClaimToHeader](#claimtoheader) array_ | false | ClaimToHeaders is a list of JWT claims that must be extracted into HTTP request headers For examples, following config: The claim must be of type; string, int, double, bool. Array type claims are not supported | -| `recomputeRoute` | _boolean_ | false | RecomputeRoute clears the route cache and recalculates the routing decision. This field must be enabled if the headers generated from the claim are used for route matching decisions. | +| `recomputeRoute` | _boolean_ | false | RecomputeRoute clears the route cache and recalculates the routing decision. This field must be enabled if the headers generated from the claim are used for route matching decisions. If the recomputation selects a new route, features targeting the new matched route will be applied. | | `extractFrom` | _[JWTExtractor](#jwtextractor)_ | false | ExtractFrom defines different ways to extract the JWT token from HTTP request. If empty, it defaults to extract JWT token from the Authorization HTTP request header using Bearer schema or access_token from query parameters. | diff --git a/test/cel-validation/securitypolicy_test.go b/test/cel-validation/securitypolicy_test.go index a6107e6531b..8d7d830eee6 100644 --- a/test/cel-validation/securitypolicy_test.go +++ b/test/cel-validation/securitypolicy_test.go @@ -548,6 +548,121 @@ func TestSecurityPolicyTarget(t *testing.T) { "spec.extAuth: Invalid value: \"object\": kind is invalid, only Service (specified by omitting the kind field or setting it to 'Service') is supported", }, }, + // JWT + { + desc: "valid jwt", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "example", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://example.com/jwt/jwks.json", + }, + }, + }, + }, + TargetRef: gwapiv1a2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gwapiv1a2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "jwt with claim to headers", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "example", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://example.com/jwt/jwks.json", + }, + ClaimToHeaders: []egv1a1.ClaimToHeader{ + { + Claim: "name", + Header: "x-claim-name", + }, + }, + }, + }, + }, + TargetRef: gwapiv1a2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gwapiv1a2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + } + }, + wantErrors: []string{}, + }, + { + desc: "jwt with recomputeRoute", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "example", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://example.com/jwt/jwks.json", + }, + RecomputeRoute: ptr.To(true), + }, + }, + }, + TargetRef: gwapiv1a2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gwapiv1a2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + } + }, + wantErrors: []string{"Invalid value: \"object\": no such key: claimToHeaders evaluating rule: claimToHeaders must be specified if recomputeRoute is enabled"}, + }, + { + desc: "jwt with claim to headers and recomputeRoute", + mutate: func(sp *egv1a1.SecurityPolicy) { + sp.Spec = egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "example", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://example.com/jwt/jwks.json", + }, + ClaimToHeaders: []egv1a1.ClaimToHeader{ + { + Claim: "name", + Header: "x-claim-name", + }, + }, + RecomputeRoute: ptr.To(true), + }, + }, + }, + TargetRef: gwapiv1a2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: gwapiv1a2.PolicyTargetReference{ + Group: "gateway.networking.k8s.io", + Kind: "Gateway", + Name: "eg", + }, + }, + } + }, + wantErrors: []string{}, + }, } for _, tc := range cases {