From c3ee9605f682e58369aa5bb576f06d2b2b059d34 Mon Sep 17 00:00:00 2001 From: Huabing Zhao Date: Fri, 27 Oct 2023 07:33:35 +0800 Subject: [PATCH] feat: add jwt authn to SecurityPolicy (#2079) * add jwt authn to SecurityPolicy Signed-off-by: huabing zhao * rename JWTAuthentication to JWT Signed-off-by: huabing zhao * fix test Signed-off-by: huabing zhao --------- Signed-off-by: huabing zhao --- api/v1alpha1/securitypolicy_types.go | 63 ++- .../validation/authenticationfilter.go | 1 + .../validation/authenticationfilter_test.go | 1 + .../validation/securitypolicy_validate.go | 117 +++++ .../securitypolicy_validate_test.go | 479 ++++++++++++++++++ api/v1alpha1/zz_generated.deepcopy.go | 53 ++ ...ateway.envoyproxy.io_securitypolicies.yaml | 86 ++++ internal/gatewayapi/securitypolicy.go | 34 +- .../testdata/securitypolicy-with-jwt.in.yaml | 118 +++++ .../testdata/securitypolicy-with-jwt.out.yaml | 337 ++++++++++++ internal/ir/xds.go | 30 +- internal/ir/xds_test.go | 26 +- internal/ir/zz_generated.deepcopy.go | 27 + internal/xds/translator/authentication.go | 53 +- internal/xds/translator/httpfilters.go | 16 +- internal/xds/translator/httpfilters_test.go | 4 +- internal/xds/translator/jwt_authn.go | 359 +++++++++++++ .../jwt-authn-multi-route-multi-provider.yaml | 68 +++ ...jwt-authn-multi-route-single-provider.yaml | 49 ++ .../in/xds-ir/jwt-authn-ratelimit.yaml | 69 +++ .../jwt-authn-single-route-single-match.yaml | 25 + ...n-multi-route-multi-provider.clusters.yaml | 81 +++ ...-multi-route-multi-provider.endpoints.yaml | 33 ++ ...-multi-route-multi-provider.listeners.yaml | 113 +++++ ...thn-multi-route-multi-provider.routes.yaml | 25 + ...-multi-route-single-provider.clusters.yaml | 59 +++ ...multi-route-single-provider.endpoints.yaml | 22 + ...multi-route-single-provider.listeners.yaml | 93 ++++ ...hn-multi-route-single-provider.routes.yaml | 25 + .../xds-ir/jwt-authn-ratelimit.clusters.yaml | 105 ++++ .../xds-ir/jwt-authn-ratelimit.endpoints.yaml | 44 ++ .../xds-ir/jwt-authn-ratelimit.listeners.yaml | 63 +++ .../xds-ir/jwt-authn-ratelimit.routes.yaml | 46 ++ ...hn-single-route-single-match.clusters.yaml | 45 ++ ...n-single-route-single-match.endpoints.yaml | 11 + ...n-single-route-single-match.listeners.yaml | 53 ++ ...uthn-single-route-single-match.routes.yaml | 16 + internal/xds/translator/translator.go | 8 +- internal/xds/translator/translator_test.go | 12 + site/content/en/latest/api/extension_types.md | 36 ++ 40 files changed, 2832 insertions(+), 73 deletions(-) create mode 100644 api/v1alpha1/validation/securitypolicy_validate.go create mode 100644 api/v1alpha1/validation/securitypolicy_validate_test.go create mode 100644 internal/gatewayapi/testdata/securitypolicy-with-jwt.in.yaml create mode 100755 internal/gatewayapi/testdata/securitypolicy-with-jwt.out.yaml create mode 100644 internal/xds/translator/jwt_authn.go create mode 100644 internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-multi-provider.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-single-provider.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/jwt-authn-ratelimit.yaml create mode 100644 internal/xds/translator/testdata/in/xds-ir/jwt-authn-single-route-single-match.yaml create mode 100755 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.clusters.yaml create mode 100755 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.endpoints.yaml create mode 100755 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.listeners.yaml create mode 100755 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.routes.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.clusters.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.endpoints.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.listeners.yaml create mode 100644 internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.routes.yaml diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go index 05117a77278..c9d346f100f 100644 --- a/api/v1alpha1/securitypolicy_types.go +++ b/api/v1alpha1/securitypolicy_types.go @@ -43,17 +43,24 @@ type SecurityPolicySpec struct { TargetRef gwapiv1a2.PolicyTargetReferenceWithSectionName `json:"targetRef"` // CORS defines the configuration for Cross-Origin Resource Sharing (CORS). + // + // +optional CORS *CORS `json:"cors,omitempty"` + + // JWT defines the configuration for JSON Web Token (JWT) authentication. + // + // +optional + JWT *JWT `json:"jwt,omitempty"` } // CORS defines the configuration for Cross-Origin Resource Sharing (CORS). type CORS struct { // AllowOrigins defines the origins that are allowed to make requests. // +kubebuilder:validation:MinItems=1 - AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins,omitempty"` + AllowOrigins []StringMatch `json:"allowOrigins,omitempty" yaml:"allowOrigins"` // AllowMethods defines the methods that are allowed to make requests. // +kubebuilder:validation:MinItems=1 - AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods,omitempty"` + AllowMethods []string `json:"allowMethods,omitempty" yaml:"allowMethods"` // AllowHeaders defines the headers that are allowed to be sent with requests. AllowHeaders []string `json:"allowHeaders,omitempty" yaml:"allowHeaders,omitempty"` // ExposeHeaders defines the headers that can be exposed in the responses. @@ -62,6 +69,58 @@ type CORS struct { MaxAge *metav1.Duration `json:"maxAge,omitempty" yaml:"maxAge,omitempty"` } +// JWT defines the configuration for JSON Web Token (JWT) authentication. +type JWT struct { + + // Providers defines the JSON Web Token (JWT) authentication provider type. + // + // When multiple JWT providers are specified, the JWT is considered valid if + // any of the providers successfully validate the JWT. For additional details, + // see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html. + // + // +kubebuilder:validation:MinItems=1 + // +kubebuilder:validation:MaxItems=4 + Providers []JWTProvider `json:"providers"` +} + +// JWTProvider defines how a JSON Web Token (JWT) can be verified. +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. + // + // +kubebuilder:validation:MinLength=1 + // +kubebuilder:validation:MaxLength=253 + Name string `json:"name"` + + // 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. + // + // +kubebuilder:validation:MaxLength=253 + // +optional + Issuer string `json:"issuer,omitempty"` + + // 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. + // + // +kubebuilder:validation:MaxItems=8 + // +optional + Audiences []string `json:"audiences,omitempty"` + + // RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote + // HTTP/HTTPS endpoint. + RemoteJWKS RemoteJWKS `json:"remoteJWKS"` + + // 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 `json:"claimToHeaders,omitempty"` + // TODO: Add TBD JWT fields based on defined use cases. +} + // StringMatch defines how to match any strings. // This is a general purpose match condition that can be used by other EG APIs // that need to match against a string. diff --git a/api/v1alpha1/validation/authenticationfilter.go b/api/v1alpha1/validation/authenticationfilter.go index 684d473f12e..378a63459fe 100644 --- a/api/v1alpha1/validation/authenticationfilter.go +++ b/api/v1alpha1/validation/authenticationfilter.go @@ -17,6 +17,7 @@ import ( egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) +// TODO zhaohuabing remove this file after deprecating authentication filter // ValidateAuthenticationFilter validates the provided filter. The only supported // ValidateAuthenticationFilter type is "JWT". func ValidateAuthenticationFilter(filter *egv1a1.AuthenticationFilter) error { diff --git a/api/v1alpha1/validation/authenticationfilter_test.go b/api/v1alpha1/validation/authenticationfilter_test.go index 2ce3f272d0d..86470be158f 100644 --- a/api/v1alpha1/validation/authenticationfilter_test.go +++ b/api/v1alpha1/validation/authenticationfilter_test.go @@ -14,6 +14,7 @@ import ( egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) +// TODO zhaohuabing remove this file after deprecating authentication filter func TestValidateAuthenticationFilter(t *testing.T) { testCases := []struct { name string diff --git a/api/v1alpha1/validation/securitypolicy_validate.go b/api/v1alpha1/validation/securitypolicy_validate.go new file mode 100644 index 00000000000..66bee6a8eb5 --- /dev/null +++ b/api/v1alpha1/validation/securitypolicy_validate.go @@ -0,0 +1,117 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package validation + +import ( + "errors" + "fmt" + "net/mail" + "net/url" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/validation" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +// ValidateSecurityPolicy validates the provided SecurityPolicy. +func ValidateSecurityPolicy(policy *egv1a1.SecurityPolicy) error { + var errs []error + if policy == nil { + return errors.New("policy is nil") + } + if err := validateSecurityPolicySpec(&policy.Spec); err != nil { + errs = append(errs, errors.New("policy is nil")) + } + + return utilerrors.NewAggregate(errs) +} + +// validateSecurityPolicySpec validates the provided spec. +func validateSecurityPolicySpec(spec *egv1a1.SecurityPolicySpec) error { + var errs []error + + sum := 0 + switch { + case spec == nil: + errs = append(errs, errors.New("spec is nil")) + case spec.CORS != nil: + sum++ + case spec.JWT != nil: + sum++ + } + if sum == 0 { + errs = append(errs, errors.New("no security policy is specified")) + } + + // Return early if any errors exist. + if len(errs) != 0 { + return utilerrors.NewAggregate(errs) + } + + if err := ValidateJWTProvider(spec.JWT.Providers); err != nil { + errs = append(errs, err) + } + + return utilerrors.NewAggregate(errs) +} + +// ValidateJWTProvider validates the provided JWT authentication configuration. +func ValidateJWTProvider(providers []egv1a1.JWTProvider) error { + var errs []error + + var names []string + for _, provider := range providers { + switch { + case len(provider.Name) == 0: + errs = append(errs, errors.New("jwt provider cannot be an empty string")) + case len(provider.Issuer) != 0: + // Issuer can take the format of a URL or an email address. + if _, err := url.ParseRequestURI(provider.Issuer); err != nil { + _, err := mail.ParseAddress(provider.Issuer) + if err != nil { + errs = append(errs, fmt.Errorf("invalid issuer; must be a URL or email address: %v", err)) + } + } + case len(provider.RemoteJWKS.URI) == 0: + errs = append(errs, fmt.Errorf("uri must be set for remote JWKS provider: %s", provider.Name)) + } + if _, err := url.ParseRequestURI(provider.RemoteJWKS.URI); err != nil { + errs = append(errs, fmt.Errorf("invalid remote JWKS URI: %v", err)) + } + + if len(errs) == 0 { + if strErrs := validation.IsQualifiedName(provider.Name); len(strErrs) != 0 { + for _, strErr := range strErrs { + errs = append(errs, errors.New(strErr)) + } + } + // Ensure uniqueness among provider names. + if names == nil { + names = append(names, provider.Name) + } else { + for _, name := range names { + if name == provider.Name { + errs = append(errs, fmt.Errorf("provider name %s must be unique", provider.Name)) + } else { + names = append(names, provider.Name) + } + } + } + } + + for _, claimToHeader := range provider.ClaimToHeaders { + switch { + case len(claimToHeader.Header) == 0: + errs = append(errs, fmt.Errorf("header must be set for claimToHeader provider: %s", claimToHeader.Header)) + case len(claimToHeader.Claim) == 0: + errs = append(errs, fmt.Errorf("claim must be set for claimToHeader provider: %s", claimToHeader.Claim)) + } + } + } + + return utilerrors.NewAggregate(errs) +} diff --git a/api/v1alpha1/validation/securitypolicy_validate_test.go b/api/v1alpha1/validation/securitypolicy_validate_test.go new file mode 100644 index 00000000000..489c7644f8b --- /dev/null +++ b/api/v1alpha1/validation/securitypolicy_validate_test.go @@ -0,0 +1,479 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package validation + +import ( + "testing" + + "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" +) + +func TestValidateSecurityPolicy(t *testing.T) { + testCases := []struct { + name string + policy *egv1a1.SecurityPolicy + expected bool + }{ + { + name: "nil security policy", + policy: nil, + expected: false, + }, + { + name: "empty security policy", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{}, + }, + expected: false, + }, + { + name: "valid security policy with url", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "valid security policy with email", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "test@test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "valid security policy with jwtClaimToHeader", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "test@test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + ClaimToHeaders: []egv1a1.ClaimToHeader{ + { + Header: "test", + Claim: "test", + }, + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "unqualified authentication provider name", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "unqualified_...", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "unspecified provider name", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "non unique provider names", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "unique", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + { + Name: "non-unique", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + { + Name: "non-unique", + Issuer: "https://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "invalid issuer uri", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "http://invalid url.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "http://www.test.local", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "inivalid issuer email", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "test@!123...", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "invalid remote jwks uri", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "http://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "invalid/local", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "unspecified remote jwks uri", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "", + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "unspecified jwtClaimToHeader headerName", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "test@test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + ClaimToHeaders: []egv1a1.ClaimToHeader{ + { + Header: "", + Claim: "test", + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "unspecified jwtClaimToHeader claimName", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "test@test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + ClaimToHeaders: []egv1a1.ClaimToHeader{ + { + Header: "test", + Claim: "", + }, + }, + }, + }, + }, + }, + }, + expected: false, + }, + { + name: "unspecified issuer", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: true, + }, + { + name: "unspecified audiences", + policy: &egv1a1.SecurityPolicy{ + TypeMeta: metav1.TypeMeta{ + Kind: egv1a1.KindSecurityPolicy, + APIVersion: egv1a1.GroupVersion.String(), + }, + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "test", + }, + Spec: egv1a1.SecurityPolicySpec{ + JWT: &egv1a1.JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "https://www.test.local", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + }, + }, + expected: true, + }, + } + + for i := range testCases { + tc := testCases[i] + t.Run(tc.name, func(t *testing.T) { + err := ValidateSecurityPolicy(tc.policy) + if tc.expected { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 0854f1f8fe5..be47b98c57d 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -1298,6 +1298,54 @@ func (in *JSONPatchOperation) DeepCopy() *JSONPatchOperation { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWT) DeepCopyInto(out *JWT) { + *out = *in + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]JWTProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWT. +func (in *JWT) DeepCopy() *JWT { + if in == nil { + return nil + } + out := new(JWT) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWTProvider) DeepCopyInto(out *JWTProvider) { + *out = *in + if in.Audiences != nil { + in, out := &in.Audiences, &out.Audiences + *out = make([]string, len(*in)) + copy(*out, *in) + } + out.RemoteJWKS = in.RemoteJWKS + if in.ClaimToHeaders != nil { + in, out := &in.ClaimToHeaders, &out.ClaimToHeaders + *out = make([]ClaimToHeader, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWTProvider. +func (in *JWTProvider) DeepCopy() *JWTProvider { + if in == nil { + return nil + } + out := new(JWTProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JwtAuthenticationFilterProvider) DeepCopyInto(out *JwtAuthenticationFilterProvider) { *out = *in @@ -2223,6 +2271,11 @@ func (in *SecurityPolicySpec) DeepCopyInto(out *SecurityPolicySpec) { *out = new(CORS) (*in).DeepCopyInto(*out) } + if in.JWT != nil { + in, out := &in.JWT, &out.JWT + *out = new(JWT) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicySpec. 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 e3bf0d73926..d371e6b02ca 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -98,6 +98,92 @@ spec: request can be cached. type: string type: object + jwt: + description: JWT defines the configuration for JSON Web Token (JWT) + authentication. + properties: + providers: + description: "Providers defines the JSON Web Token (JWT) authentication + provider type. \n When multiple JWT providers are specified, + the JWT is considered valid if any of the providers successfully + validate the JWT. For additional details, see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html." + items: + description: JWTProvider defines how a JSON Web Token (JWT) + can be verified. + properties: + audiences: + description: 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. + items: + type: string + maxItems: 8 + type: array + claimToHeaders: + description: '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' + items: + description: ClaimToHeader defines a configuration to + convert JWT claims into HTTP headers + properties: + claim: + description: '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.' + type: string + header: + description: Header defines the name of the HTTP request + header that the JWT Claim will be saved into. + type: string + required: + - claim + - header + type: object + type: array + issuer: + description: 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. + maxLength: 253 + type: string + name: + description: 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. + maxLength: 253 + minLength: 1 + type: string + remoteJWKS: + description: RemoteJWKS defines how to fetch and cache JSON + Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. + properties: + uri: + description: URI is the HTTPS URI to fetch the JWKS. + Envoy's system trust bundle is used to validate the + server certificate. + maxLength: 253 + minLength: 1 + type: string + required: + - uri + type: object + required: + - name + - remoteJWKS + type: object + maxItems: 4 + minItems: 1 + type: array + required: + - providers + type: object targetRef: description: TargetRef is the name of the Gateway resource this policy is being attached to. This Policy and the TargetRef MUST be in the diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 57df03abee4..1b52af2bbf9 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -222,19 +222,30 @@ func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes m func (t *Translator) translateSecurityPolicyForRoute(policy *egv1a1.SecurityPolicy, route RouteContext, xdsIR XdsIRMap) { // Build IR - var cors *ir.CORS + var ( + cors *ir.CORS + jwt *ir.JWT + ) + if policy.Spec.CORS != nil { cors = t.buildCORS(policy) } + if policy.Spec.JWT != nil { + jwt = t.buildJWT(policy) + } + // Apply IR to all relevant routes prefix := irRoutePrefix(route) for _, ir := range xdsIR { for _, http := range ir.HTTP { for _, r := range http.Routes { // Apply if there is a match + // TODO zhaohuabing: extract a utils function to check if an HTTP + // route is associated with a Gateway API xRoute if strings.HasPrefix(r.Name, prefix) { r.CORS = cors + r.JWT = jwt } } } @@ -244,13 +255,21 @@ func (t *Translator) translateSecurityPolicyForRoute(policy *egv1a1.SecurityPoli func (t *Translator) translateSecurityPolicyForGateway(policy *egv1a1.SecurityPolicy, gateway *GatewayContext, xdsIR XdsIRMap) { // Build IR - var cors *ir.CORS + var ( + cors *ir.CORS + jwt *ir.JWT + ) + if policy.Spec.CORS != nil { cors = t.buildCORS(policy) } + if policy.Spec.JWT != nil { + jwt = t.buildJWT(policy) + } + // Apply IR to all the routes within the specific Gateway - // If the feature is already set, then skip it, since it must be have + // If the feature is already set, then skip it, since it must have be // set by a policy attaching to the route irKey := t.getIRKey(gateway.Gateway) // Should exist since we've validated this @@ -262,6 +281,9 @@ func (t *Translator) translateSecurityPolicyForGateway(policy *egv1a1.SecurityPo if r.CORS == nil { r.CORS = cors } + if r.JWT == nil { + r.JWT = jwt + } } } @@ -308,3 +330,9 @@ func (t *Translator) buildCORS(policy *egv1a1.SecurityPolicy) *ir.CORS { MaxAge: policy.Spec.CORS.MaxAge, } } + +func (t *Translator) buildJWT(policy *egv1a1.SecurityPolicy) *ir.JWT { + return &ir.JWT{ + Providers: policy.Spec.JWT.Providers, + } +} diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt.in.yaml new file mode 100644 index 00000000000..b7eec111123 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt.in.yaml @@ -0,0 +1,118 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: envoy-gateway + name: gateway-2 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + namespace: default + name: grpcroute-1 + spec: + parentRefs: + - namespace: envoy-gateway + name: gateway-1 + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: envoy-gateway + name: gateway-2 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: envoy-gateway + name: policy-for-gateway + spec: + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + jwt: + providers: + - name: example1 + issuer: https://one.example.com + audiences: + - one.foo.com + remoteJWKS: + uri: https://one.example.com/jwt/public-key/jwks.json + claimToHeaders: + - header: one-route-example-key + claim: claim1 + - name: example2 + issuer: https://two.example.com + audiences: + - two.foo.com + remoteJWKS: + uri: https://two.example.com/jwt/public-key/jwks.json + claimToHeaders: + - header: two-route-example-key + claim: claim2 +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + namespace: default + name: policy-for-route + spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + jwt: + providers: + - name: example3 + issuer: https://three.example.com + audiences: + - three.foo.com + remoteJWKS: + uri: https://three.example.com/jwt/public-key/jwks.json + claimToHeaders: + - header: three-route-example-key + claim: claim3 diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt.out.yaml new file mode 100755 index 00000000000..8acf1b66a51 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt.out.yaml @@ -0,0 +1,337 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-2 + namespace: envoy-gateway + spec: + gatewayClassName: envoy-gateway-class + listeners: + - allowedRoutes: + namespaces: + from: All + name: http + port: 80 + protocol: HTTP + status: + listeners: + - attachedRoutes: 1 + conditions: + - lastTransitionTime: null + message: Sending translated listener configuration to the data plane + reason: Programmed + status: "True" + type: Programmed + - lastTransitionTime: null + message: Listener has been successfully translated + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Listener references have been resolved + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + name: http + supportedKinds: + - group: gateway.networking.k8s.io + kind: HTTPRoute + - group: gateway.networking.k8s.io + kind: GRPCRoute +grpcRoutes: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: GRPCRoute + metadata: + creationTimestamp: null + name: grpcroute-1 + namespace: default + spec: + parentRefs: + - name: gateway-1 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-1 + namespace: envoy-gateway + sectionName: http +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + creationTimestamp: null + name: httproute-1 + namespace: default + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - name: gateway-2 + namespace: envoy-gateway + sectionName: http + rules: + - backendRefs: + - name: service-1 + port: 8080 + matches: + - path: + value: / + status: + parents: + - conditions: + - lastTransitionTime: null + message: Route is accepted + reason: Accepted + status: "True" + type: Accepted + - lastTransitionTime: null + message: Resolved all the Object references for the Route + reason: ResolvedRefs + status: "True" + type: ResolvedRefs + controllerName: gateway.envoyproxy.io/gatewayclass-controller + parentRef: + name: gateway-2 + namespace: envoy-gateway + sectionName: http +infraIR: + envoy-gateway/gateway-1: + proxy: + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-1 + envoy-gateway/gateway-2: + proxy: + listeners: + - address: "" + ports: + - containerPort: 10080 + name: http + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-2 + gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway + name: envoy-gateway/gateway-2 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + jwt: + providers: + - audiences: + - three.foo.com + claimToHeaders: + - claim: claim3 + header: three-route-example-key + issuer: https://three.example.com + name: example3 + remoteJWKS: + uri: https://three.example.com/jwt/public-key/jwks.json + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + namespace: default + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-gateway + namespace: envoy-gateway + spec: + jwt: + providers: + - audiences: + - one.foo.com + claimToHeaders: + - claim: claim1 + header: one-route-example-key + issuer: https://one.example.com + name: example1 + remoteJWKS: + uri: https://one.example.com/jwt/public-key/jwks.json + - audiences: + - two.foo.com + claimToHeaders: + - claim: claim2 + header: two-route-example-key + issuer: https://two.example.com + name: example2 + remoteJWKS: + uri: https://two.example.com/jwt/public-key/jwks.json + targetRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: envoy-gateway + status: + conditions: + - lastTransitionTime: null + message: SecurityPolicy has been accepted. + reason: Accepted + status: "True" + type: Accepted +xdsIR: + envoy-gateway/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: true + name: envoy-gateway/gateway-1/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: grpcroute/default/grpcroute-1/rule/0 + settings: + - endpoints: + - host: 7.7.7.7 + port: 8080 + weight: 1 + hostname: '*' + jwt: + providers: + - audiences: + - one.foo.com + claimToHeaders: + - claim: claim1 + header: one-route-example-key + issuer: https://one.example.com + name: example1 + remoteJWKS: + uri: https://one.example.com/jwt/public-key/jwks.json + - audiences: + - two.foo.com + claimToHeaders: + - claim: claim2 + header: two-route-example-key + issuer: https://two.example.com + name: example2 + remoteJWKS: + uri: https://two.example.com/jwt/public-key/jwks.json + name: grpcroute/default/grpcroute-1/rule/0/match/-1/* + envoy-gateway/gateway-2: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + name: envoy-gateway/gateway-2/http + port: 10080 + routes: + - backendWeights: + invalid: 0 + valid: 0 + destination: + name: httproute/default/httproute-1/rule/0 + settings: + - endpoints: + - host: 7.7.7.7 + port: 8080 + weight: 1 + hostname: gateway.envoyproxy.io + jwt: + providers: + - audiences: + - three.foo.com + claimToHeaders: + - claim: claim3 + header: three-route-example-key + issuer: https://three.example.com + name: example3 + remoteJWKS: + uri: https://three.example.com/jwt/public-key/jwks.json + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / diff --git a/internal/ir/xds.go b/internal/ir/xds.go index e7bf0415621..6a7a0041918 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -273,7 +273,7 @@ type HTTPRoute struct { // RateLimit defines the more specific match conditions as well as limits for ratelimiting // the requests on this route. RateLimit *RateLimit `json:"rateLimit,omitempty" yaml:"rateLimit,omitempty"` - // RequestAuthentication defines the schema for authenticating HTTP requests. + // RequestAuthentication defines the schema for authenticating HTTP requests. //TODO zhaohuabing remove this field RequestAuthentication *RequestAuthentication `json:"requestAuthentication,omitempty" yaml:"requestAuthentication,omitempty"` // Timeout is the time until which entire response is received from the upstream. Timeout *metav1.Duration `json:"timeout,omitempty" yaml:"timeout,omitempty"` @@ -281,6 +281,8 @@ type HTTPRoute struct { LoadBalancer *LoadBalancer `json:"loadBalancer,omitempty" yaml:"loadBalancer,omitempty"` // CORS policy for the route. CORS *CORS `json:"cors,omitempty" yaml:"cors,omitempty"` + // JWT defines the schema for authenticating HTTP requests using JSON Web Tokens (JWT). + JWT *JWT `json:"jwt,omitempty" yaml:"jwt,omitempty"` // ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"` } @@ -300,6 +302,7 @@ type UnstructuredRef struct { // TODO: Add support for additional request authentication providers, i.e. OIDC. // // +k8s:deepcopy-gen=true +// TODO zhaohuabing remove this type type RequestAuthentication struct { // JWT defines the schema for authenticating HTTP requests using JSON Web Tokens (JWT). JWT *JwtRequestAuthentication `json:"jwt,omitempty" yaml:"jwt,omitempty"` @@ -309,6 +312,7 @@ type RequestAuthentication struct { // JSON Web Tokens (JWT). // // +k8s:deepcopy-gen=true +// TODO zhaohuabing remove this type type JwtRequestAuthentication struct { // Providers defines a list of JSON Web Token (JWT) authentication providers. Providers []egv1a1.JwtAuthenticationFilterProvider `json:"providers,omitempty" yaml:"providers,omitempty"` @@ -330,6 +334,15 @@ type CORS struct { MaxAge *metav1.Duration `json:"maxAge,omitempty" yaml:"maxAge,omitempty"` } +// JWT defines the schema for authenticating HTTP requests using +// JSON Web Tokens (JWT). +// +// +k8s:deepcopy-gen=true +type JWT struct { + // Providers defines a list of JSON Web Token (JWT) authentication providers. + Providers []egv1a1.JWTProvider `json:"providers,omitempty" yaml:"providers,omitempty"` +} + // Validate the fields within the HTTPRoute structure func (h HTTPRoute) Validate() error { var errs error @@ -446,6 +459,11 @@ func (h HTTPRoute) Validate() error { errs = multierror.Append(errs, err) } } + if h.JWT != nil { + if err := h.JWT.validate(); err != nil { + errs = multierror.Append(errs, err) + } + } return errs } @@ -460,6 +478,16 @@ func (j *JwtRequestAuthentication) Validate() error { return errs } +func (j *JWT) validate() error { + var errs error + + if err := validation.ValidateJWTProvider(j.Providers); err != nil { + errs = multierror.Append(errs, err) + } + + return errs +} + // RouteDestination holds the destination details associated with the route // +kubebuilder:object:generate=true type RouteDestination struct { diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index 5d5344b8e02..17ca9878751 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -426,14 +426,12 @@ var ( PathMatch: &StringMatch{ Exact: ptrTo("jwtauthen"), }, - RequestAuthentication: &RequestAuthentication{ - JWT: &JwtRequestAuthentication{ - Providers: []egv1a1.JwtAuthenticationFilterProvider{ - { - Name: "test1", - RemoteJWKS: egv1a1.RemoteJWKS{ - URI: "https://test1.local", - }, + JWT: &JWT{ + Providers: []egv1a1.JWTProvider{ + { + Name: "test1", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test1.local", }, }, }, @@ -1072,20 +1070,20 @@ func TestValidateStringMatch(t *testing.T) { func TestValidateJwtRequestAuthentication(t *testing.T) { tests := []struct { name string - input JwtRequestAuthentication + input JWT want error }{ { name: "nil rules", - input: JwtRequestAuthentication{ + input: JWT{ Providers: nil, }, want: nil, }, { name: "provider with remote jwks uri", - input: JwtRequestAuthentication{ - Providers: []egv1a1.JwtAuthenticationFilterProvider{ + input: JWT{ + Providers: []egv1a1.JWTProvider{ { Name: "test", Issuer: "https://test.local", @@ -1103,9 +1101,9 @@ func TestValidateJwtRequestAuthentication(t *testing.T) { test := tests[i] t.Run(test.name, func(t *testing.T) { if test.want == nil { - require.NoError(t, test.input.Validate()) + require.NoError(t, test.input.validate()) } else { - require.EqualError(t, test.input.Validate(), test.want.Error()) + require.EqualError(t, test.input.validate(), test.want.Error()) } }) } diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 02173a1ae7f..8044ec291a5 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -461,6 +461,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) { *out = new(CORS) (*in).DeepCopyInto(*out) } + if in.JWT != nil { + in, out := &in.JWT, &out.JWT + *out = new(JWT) + (*in).DeepCopyInto(*out) + } if in.ExtensionRefs != nil { in, out := &in.ExtensionRefs, &out.ExtensionRefs *out = make([]*UnstructuredRef, len(*in)) @@ -580,6 +585,28 @@ func (in *JSONPatchOperation) DeepCopy() *JSONPatchOperation { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *JWT) DeepCopyInto(out *JWT) { + *out = *in + if in.Providers != nil { + in, out := &in.Providers, &out.Providers + *out = make([]v1alpha1.JWTProvider, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new JWT. +func (in *JWT) DeepCopy() *JWT { + if in == nil { + return nil + } + out := new(JWT) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *JwtRequestAuthentication) DeepCopyInto(out *JwtRequestAuthentication) { *out = *in diff --git a/internal/xds/translator/authentication.go b/internal/xds/translator/authentication.go index 80426d4f320..17fd874edd4 100644 --- a/internal/xds/translator/authentication.go +++ b/internal/xds/translator/authentication.go @@ -17,9 +17,7 @@ import ( routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" "github.com/envoyproxy/go-control-plane/pkg/resource/v3" - "github.com/envoyproxy/go-control-plane/pkg/wellknown" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" @@ -29,11 +27,7 @@ import ( "github.com/envoyproxy/gateway/internal/xds/types" ) -const ( - jwtAuthenFilter = "envoy.filters.http.jwt_authn" - envoyTrustBundle = "/etc/ssl/certs/ca-certificates.crt" -) - +// TODO zhaohuabing remove this file after deprecating authentication filter // patchHCMWithJwtAuthnFilter builds and appends the Jwt Filter to the HTTP // Connection Manager if applicable, and it does not already exist. func patchHCMWithJwtAuthnFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { @@ -51,7 +45,7 @@ func patchHCMWithJwtAuthnFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir // Return early if filter already exists. for _, httpFilter := range mgr.HttpFilters { - if httpFilter.Name == jwtAuthenFilter { + if httpFilter.Name == jwtAuthnFilter { return nil } } @@ -84,7 +78,7 @@ func buildHCMJwtFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) { } return &hcmv3.HttpFilter{ - Name: jwtAuthenFilter, + Name: jwtAuthnFilter, ConfigType: &hcmv3.HttpFilter_TypedConfig{ TypedConfig: jwtAuthnAny, }, @@ -164,36 +158,6 @@ func buildJwtAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, }, nil } -// buildXdsUpstreamTLSSocket returns an xDS TransportSocket that uses envoyTrustBundle -// as the CA to authenticate server certificates. -func buildXdsUpstreamTLSSocket() (*corev3.TransportSocket, error) { - tlsCtxProto := &tlsv3.UpstreamTlsContext{ - CommonTlsContext: &tlsv3.CommonTlsContext{ - ValidationContextType: &tlsv3.CommonTlsContext_ValidationContext{ - ValidationContext: &tlsv3.CertificateValidationContext{ - TrustedCa: &corev3.DataSource{ - Specifier: &corev3.DataSource_Filename{ - Filename: envoyTrustBundle, - }, - }, - }, - }, - }, - } - - tlsCtxAny, err := anypb.New(tlsCtxProto) - if err != nil { - return nil, err - } - - return &corev3.TransportSocket{ - Name: wellknown.TransportSocketTls, - ConfigType: &corev3.TransportSocket_TypedConfig{ - TypedConfig: tlsCtxAny, - }, - }, nil -} - // patchRouteWithJwtConfig patches the provided route with a JWT PerRouteConfig, if the // route doesn't contain it. func patchRouteWithJwtConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error { @@ -205,7 +169,7 @@ func patchRouteWithJwtConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error } filterCfg := route.GetTypedPerFilterConfig() - if _, ok := filterCfg[jwtAuthenFilter]; !ok { + if _, ok := filterCfg[jwtAuthnFilter]; !ok { if !routeContainsJwtAuthn(irRoute) { return nil } @@ -222,19 +186,12 @@ func patchRouteWithJwtConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error route.TypedPerFilterConfig = make(map[string]*anypb.Any) } - route.TypedPerFilterConfig[jwtAuthenFilter] = routeCfgAny + route.TypedPerFilterConfig[jwtAuthnFilter] = routeCfgAny } return nil } -type jwksCluster struct { - name string - hostname string - port uint32 - isStatic bool -} - // createJwksClusters creates JWKS clusters from the provided routes, if needed. func createJwksClusters(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error { if tCtx == nil || diff --git a/internal/xds/translator/httpfilters.go b/internal/xds/translator/httpfilters.go index cef974cec33..4aba259b1e5 100644 --- a/internal/xds/translator/httpfilters.go +++ b/internal/xds/translator/httpfilters.go @@ -40,7 +40,7 @@ func newOrderedHTTPFilter(filter *hcmv3.HttpFilter) *OrderedHTTPFilter { switch filter.Name { case wellknown.CORS: order = 1 - case jwtAuthenFilter: + case jwtAuthnFilter: order = 2 case wellknown.HTTPRateLimit: order = 3 @@ -101,6 +101,7 @@ func (t *Translator) patchHCMWithFilters( t.patchHCMWithRateLimit(mgr, irListener) // Add the jwt authn filter, if needed. + // TODO zhaohuabing remove this after deprecating authentication filter if err := patchHCMWithJwtAuthnFilter(mgr, irListener); err != nil { return err } @@ -110,6 +111,11 @@ func (t *Translator) patchHCMWithFilters( return err } + // Add the jwt authn filter, if needed. + if err := patchHCMWithJWTAuthnFilter(mgr, irListener); err != nil { + return err + } + // Add the router filter mgr.HttpFilters = append(mgr.HttpFilters, xdsfilters.HTTPRouter) @@ -129,7 +135,7 @@ func patchRouteWithFilters( return nil } - // Add the jwt per route config to the route, if needed. + // Add the jwt per route config to the route, if needed. // TODO zhaohuabing remove this after deprecating authentication filter if err := patchRouteWithJwtConfig(route, irRoute); err != nil { return nil } @@ -138,5 +144,11 @@ func patchRouteWithFilters( if err := patchRouteWithCORSConfig(route, irRoute); err != nil { return err } + + // Add the jwt per route config to the route, if needed. + if err := patchRouteWithJWTConfig(route, irRoute); err != nil { + return nil + } + return nil } diff --git a/internal/xds/translator/httpfilters_test.go b/internal/xds/translator/httpfilters_test.go index 928afb183c4..ae4f0452014 100644 --- a/internal/xds/translator/httpfilters_test.go +++ b/internal/xds/translator/httpfilters_test.go @@ -24,12 +24,12 @@ func Test_sortHTTPFilters(t *testing.T) { filters: []*hcmv3.HttpFilter{ httpFilterForTest(wellknown.Router), httpFilterForTest(wellknown.CORS), - httpFilterForTest(jwtAuthenFilter), + httpFilterForTest(jwtAuthnFilter), httpFilterForTest(wellknown.HTTPRateLimit), }, want: []*hcmv3.HttpFilter{ httpFilterForTest(wellknown.CORS), - httpFilterForTest(jwtAuthenFilter), + httpFilterForTest(jwtAuthnFilter), httpFilterForTest(wellknown.HTTPRateLimit), httpFilterForTest(wellknown.Router), }, diff --git a/internal/xds/translator/jwt_authn.go b/internal/xds/translator/jwt_authn.go new file mode 100644 index 00000000000..7003bcac3fb --- /dev/null +++ b/internal/xds/translator/jwt_authn.go @@ -0,0 +1,359 @@ +// Copyright Envoy Gateway Authors +// SPDX-License-Identifier: Apache-2.0 +// The full text of the Apache license is available in the LICENSE file at +// the root of the repo. + +package translator + +import ( + "errors" + "fmt" + "net" + "net/url" + "strconv" + "strings" + + corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + routev3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + jwtauthnv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/http/jwt_authn/v3" + hcmv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + tlsv3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "github.com/envoyproxy/go-control-plane/pkg/resource/v3" + "github.com/envoyproxy/go-control-plane/pkg/wellknown" + "google.golang.org/protobuf/types/known/anypb" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/envoyproxy/gateway/api/v1alpha1" + "github.com/envoyproxy/gateway/internal/ir" + "github.com/envoyproxy/gateway/internal/utils/ptr" + "github.com/envoyproxy/gateway/internal/xds/types" +) + +const ( + jwtAuthnFilter = "envoy.filters.http.jwt_authn" + envoyTrustBundle = "/etc/ssl/certs/ca-certificates.crt" +) + +// patchHCMWithJWTAuthnFilter builds and appends the Jwt Filter to the HTTP +// Connection Manager if applicable, and it does not already exist. +func patchHCMWithJWTAuthnFilter(mgr *hcmv3.HttpConnectionManager, irListener *ir.HTTPListener) error { + if mgr == nil { + return errors.New("hcm is nil") + } + + if irListener == nil { + return errors.New("ir listener is nil") + } + + if !listenerContainsJWTAuthn(irListener) { + return nil + } + + // Return early if filter already exists. + for _, httpFilter := range mgr.HttpFilters { + if httpFilter.Name == jwtAuthnFilter { + return nil + } + } + + jwtFilter, err := buildHCMJWTFilter(irListener) + if err != nil { + return err + } + + // Ensure the authn filter is the first and the terminal filter is the last in the chain. + mgr.HttpFilters = append([]*hcmv3.HttpFilter{jwtFilter}, mgr.HttpFilters...) + + return nil +} + +// buildHCMJWTFilter returns a JWT authn HTTP filter from the provided IR listener. +func buildHCMJWTFilter(irListener *ir.HTTPListener) (*hcmv3.HttpFilter, error) { + jwtAuthnProto, err := buildJWTAuthn(irListener) + if err != nil { + return nil, err + } + + if err := jwtAuthnProto.ValidateAll(); err != nil { + return nil, err + } + + jwtAuthnAny, err := anypb.New(jwtAuthnProto) + if err != nil { + return nil, err + } + + return &hcmv3.HttpFilter{ + Name: jwtAuthnFilter, + ConfigType: &hcmv3.HttpFilter_TypedConfig{ + TypedConfig: jwtAuthnAny, + }, + }, nil +} + +// buildJWTAuthn returns a JwtAuthentication based on the provided IR HTTPListener. +func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, error) { + jwtProviders := make(map[string]*jwtauthnv3.JwtProvider) + reqMap := make(map[string]*jwtauthnv3.JwtRequirement) + + for _, route := range irListener.Routes { + if route != nil && routeContainsJWTAuthn(route) { + var reqs []*jwtauthnv3.JwtRequirement + for i := range route.JWT.Providers { + irProvider := route.JWT.Providers[i] + // Create the cluster for the remote jwks, if it doesn't exist. + jwksCluster, err := newJWKSCluster(&irProvider) + if err != nil { + return nil, err + } + + remote := &jwtauthnv3.JwtProvider_RemoteJwks{ + RemoteJwks: &jwtauthnv3.RemoteJwks{ + HttpUri: &corev3.HttpUri{ + Uri: irProvider.RemoteJWKS.URI, + HttpUpstreamType: &corev3.HttpUri_Cluster{ + Cluster: jwksCluster.name, + }, + Timeout: &durationpb.Duration{Seconds: 5}, + }, + CacheDuration: &durationpb.Duration{Seconds: 5 * 60}, + AsyncFetch: &jwtauthnv3.JwksAsyncFetch{}, + RetryPolicy: &corev3.RetryPolicy{}, + }, + } + + claimToHeaders := []*jwtauthnv3.JwtClaimToHeader{} + for _, claimToHeader := range irProvider.ClaimToHeaders { + claimToHeader := &jwtauthnv3.JwtClaimToHeader{HeaderName: claimToHeader.Header, ClaimName: claimToHeader.Claim} + claimToHeaders = append(claimToHeaders, claimToHeader) + } + jwtProvider := &jwtauthnv3.JwtProvider{ + Issuer: irProvider.Issuer, + Audiences: irProvider.Audiences, + JwksSourceSpecifier: remote, + PayloadInMetadata: irProvider.Issuer, + ClaimToHeaders: claimToHeaders, + } + + providerKey := fmt.Sprintf("%s/%s", route.Name, irProvider.Name) + jwtProviders[providerKey] = jwtProvider + reqs = append(reqs, &jwtauthnv3.JwtRequirement{ + RequiresType: &jwtauthnv3.JwtRequirement_ProviderName{ + ProviderName: providerKey, + }, + }) + } + if len(reqs) == 1 { + reqMap[route.Name] = reqs[0] + } else { + orListReqs := &jwtauthnv3.JwtRequirement{ + RequiresType: &jwtauthnv3.JwtRequirement_RequiresAny{ + RequiresAny: &jwtauthnv3.JwtRequirementOrList{ + Requirements: reqs, + }, + }, + } + reqMap[route.Name] = orListReqs + } + } + } + + return &jwtauthnv3.JwtAuthentication{ + RequirementMap: reqMap, + Providers: jwtProviders, + }, nil +} + +// buildXdsUpstreamTLSSocket returns an xDS TransportSocket that uses envoyTrustBundle +// as the CA to authenticate server certificates. +func buildXdsUpstreamTLSSocket() (*corev3.TransportSocket, error) { + tlsCtxProto := &tlsv3.UpstreamTlsContext{ + CommonTlsContext: &tlsv3.CommonTlsContext{ + ValidationContextType: &tlsv3.CommonTlsContext_ValidationContext{ + ValidationContext: &tlsv3.CertificateValidationContext{ + TrustedCa: &corev3.DataSource{ + Specifier: &corev3.DataSource_Filename{ + Filename: envoyTrustBundle, + }, + }, + }, + }, + }, + } + + tlsCtxAny, err := anypb.New(tlsCtxProto) + if err != nil { + return nil, err + } + + return &corev3.TransportSocket{ + Name: wellknown.TransportSocketTls, + ConfigType: &corev3.TransportSocket_TypedConfig{ + TypedConfig: tlsCtxAny, + }, + }, nil +} + +// patchRouteWithJWTConfig patches the provided route with a JWT PerRouteConfig, if the +// route doesn't contain it. +func patchRouteWithJWTConfig(route *routev3.Route, irRoute *ir.HTTPRoute) error { + if route == nil { + return errors.New("xds route is nil") + } + if irRoute == nil { + return errors.New("ir route is nil") + } + + filterCfg := route.GetTypedPerFilterConfig() + if _, ok := filterCfg[jwtAuthnFilter]; !ok { + if !routeContainsJWTAuthn(irRoute) { + return nil + } + + routeCfgProto := &jwtauthnv3.PerRouteConfig{ + RequirementSpecifier: &jwtauthnv3.PerRouteConfig_RequirementName{RequirementName: irRoute.Name}} + + routeCfgAny, err := anypb.New(routeCfgProto) + if err != nil { + return err + } + + if filterCfg == nil { + route.TypedPerFilterConfig = make(map[string]*anypb.Any) + } + + route.TypedPerFilterConfig[jwtAuthnFilter] = routeCfgAny + } + + return nil +} + +type jwksCluster struct { + name string + hostname string + port uint32 + isStatic bool +} + +// createJWKSClusters creates JWKS clusters from the provided routes, if needed. +func createJWKSClusters(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRoute) error { + if tCtx == nil || + tCtx.XdsResources == nil || + tCtx.XdsResources[resource.ClusterType] == nil || + len(routes) == 0 { + return nil + } + + for _, route := range routes { + if routeContainsJWTAuthn(route) { + for i := range route.JWT.Providers { + provider := route.JWT.Providers[i] + jwks, err := newJWKSCluster(&provider) + epType := DefaultEndpointType + if jwks.isStatic { + epType = Static + } + if err != nil { + return err + } + ds := &ir.DestinationSetting{ + Weight: ptr.To(uint32(1)), + Endpoints: []*ir.DestinationEndpoint{ir.NewDestEndpoint(jwks.hostname, jwks.port)}, + } + tSocket, err := buildXdsUpstreamTLSSocket() + if err != nil { + return err + } + if err := addXdsCluster(tCtx, &xdsClusterArgs{ + name: jwks.name, + settings: []*ir.DestinationSetting{ds}, + tSocket: tSocket, + protocol: DefaultProtocol, + endpointType: epType, + }); err != nil && !errors.Is(err, ErrXdsClusterExists) { + return err + } + } + } + } + + return nil +} + +// newJWKSCluster returns a jwksCluster from the provided provider. +func newJWKSCluster(provider *v1alpha1.JWTProvider) (*jwksCluster, error) { + static := false + if provider == nil { + return nil, errors.New("nil provider") + } + + u, err := url.Parse(provider.RemoteJWKS.URI) + if err != nil { + return nil, err + } + + var strPort string + switch u.Scheme { + case "https": + strPort = "443" + default: + return nil, fmt.Errorf("unsupported JWKS URI scheme %s", u.Scheme) + } + + if u.Port() != "" { + strPort = u.Port() + } + + name := fmt.Sprintf("%s_%s", strings.ReplaceAll(u.Hostname(), ".", "_"), strPort) + + port, err := strconv.Atoi(strPort) + if err != nil { + return nil, err + } + + if ip := net.ParseIP(u.Hostname()); ip != nil { + if v4 := ip.To4(); v4 != nil { + static = true + } + } + + return &jwksCluster{ + name: name, + hostname: u.Hostname(), + port: uint32(port), + isStatic: static, + }, nil +} + +// listenerContainsJWTAuthn returns true if JWT authentication exists for the +// provided listener. +func listenerContainsJWTAuthn(irListener *ir.HTTPListener) bool { + if irListener == nil { + return false + } + + for _, route := range irListener.Routes { + if routeContainsJWTAuthn(route) { + return true + } + } + + return false +} + +// routeContainsJWTAuthn returns true if JWT authentication exists for the +// provided route. +func routeContainsJWTAuthn(irRoute *ir.HTTPRoute) bool { + if irRoute == nil { + return false + } + + if irRoute != nil && + irRoute.JWT != nil && + irRoute.JWT.Providers != nil && + len(irRoute.JWT.Providers) > 0 { + return true + } + + return false +} diff --git a/internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-multi-provider.yaml b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-multi-provider.yaml new file mode 100644 index 00000000000..6abee5f0575 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-multi-provider.yaml @@ -0,0 +1,68 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route-www.test.com" + hostname: "*" + pathMatch: + exact: "foo/bar" + jwt: + providers: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJWKS: + uri: https://localhost/jwt/public-key/jwks.json + claimToHeaders: + - header: one-route-example-key1 + claim: claim.neteased.key + - name: example2 + issuer: https://www.two.example.com + audiences: + - one.foo.com + - two.foo.com + remoteJWKS: + uri: https://192.168.1.250:8080/jwt/public-key/jwks.json + claimToHeaders: + - header: one-route-example2-key1 + claim: claim.neteased.key + - header: one-route-example2-key2 + claim: name + destination: + name: "first-route-www.test.com-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "second-route-www.test.com" + hostname: "*" + pathMatch: + exact: "foo/baz" + jwt: + providers: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJWKS: + uri: https://localhost/jwt/public-key/jwks.json + claimToHeaders: + - header: second-route-example-key1 + claim: claim.neteased.key + - name: example2 + issuer: https://www.two.example.com + audiences: + - one.foo.com + - two.foo.com + remoteJWKS: + uri: https://192.168.1.250:8080/jwt/public-key/jwks.json + destination: + name: "second-route-www.test.com-dest" + settings: + - endpoints: + - host: "5.6.7.8" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-single-provider.yaml b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-single-provider.yaml new file mode 100644 index 00000000000..d9699c052f9 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-multi-route-single-provider.yaml @@ -0,0 +1,49 @@ +accesslog: + text: + - path: "/dev/stdout" +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + pathMatch: + exact: "foo/bar" + jwt: + providers: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJWKS: + uri: https://localhost/jwt/public-key/jwks.json + claimToHeaders: + - header: first-route-key + claim: claim.neteased.key + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "second-route" + hostname: "*" + pathMatch: + exact: "foo/baz" + jwt: + providers: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJWKS: + uri: https://localhost/jwt/public-key/jwks.json + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "5.6.7.8" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/jwt-authn-ratelimit.yaml b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-ratelimit.yaml new file mode 100644 index 00000000000..f4b24fe3525 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-ratelimit.yaml @@ -0,0 +1,69 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + exact: "one" + limit: + requests: 5 + unit: second + pathMatch: + exact: "foo/bar" + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + jwt: + providers: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJWKS: + uri: https://192.168.1.250/jwt/public-key/jwks.json + - name: "second-route" + hostname: "*" + rateLimit: + global: + rules: + - headerMatches: + - name: "x-user-id" + distinct: true + limit: + requests: 5 + unit: second + pathMatch: + exact: "example" + destination: + name: "second-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 + - name: "third-route" + hostname: "*" + rateLimit: + global: + rules: + - limit: + requests: 5 + unit: second + pathMatch: + exact: "test" + destination: + name: "third-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/in/xds-ir/jwt-authn-single-route-single-match.yaml b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-single-route-single-match.yaml new file mode 100644 index 00000000000..cb7b0a20946 --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jwt-authn-single-route-single-match.yaml @@ -0,0 +1,25 @@ +http: +- name: "first-listener" + address: "0.0.0.0" + port: 10080 + hostnames: + - "*" + routes: + - name: "first-route" + hostname: "*" + pathMatch: + exact: "foo/bar" + jwt: + providers: + - name: example + issuer: https://www.example.com + audiences: + - foo.com + remoteJWKS: + uri: https://localhost/jwt/public-key/jwks.json + destination: + name: "first-route-dest" + settings: + - endpoints: + - host: "1.2.3.4" + port: 50000 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.clusters.yaml new file mode 100755 index 00000000000..bf7f010954b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.clusters.yaml @@ -0,0 +1,81 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-www.test.com-dest + lbPolicy: LEAST_REQUEST + name: first-route-www.test.com-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-www.test.com-dest + lbPolicy: LEAST_REQUEST + name: second-route-www.test.com-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: localhost_443 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: localhost + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} + name: localhost_443 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + validationContext: + trustedCa: + filename: /etc/ssl/certs/ca-certificates.crt + type: STRICT_DNS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: "192_168_1_250_8080" + lbPolicy: LEAST_REQUEST + name: "192_168_1_250_8080" + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + validationContext: + trustedCa: + filename: /etc/ssl/certs/ca-certificates.crt + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.endpoints.yaml new file mode 100755 index 00000000000..7d394e5f496 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.endpoints.yaml @@ -0,0 +1,33 @@ +- clusterName: first-route-www.test.com-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} +- clusterName: second-route-www.test.com-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 5.6.7.8 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} +- clusterName: "192_168_1_250_8080" + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 192.168.1.250 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.listeners.yaml new file mode 100755 index 00000000000..1eec8005ef8 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.listeners.yaml @@ -0,0 +1,113 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.jwt_authn + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + first-route-www.test.com/example: + audiences: + - foo.com + claimToHeaders: + - claimName: claim.neteased.key + headerName: one-route-example-key1 + issuer: https://www.example.com + payloadInMetadata: https://www.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: localhost_443 + timeout: 5s + uri: https://localhost/jwt/public-key/jwks.json + retryPolicy: {} + first-route-www.test.com/example2: + audiences: + - one.foo.com + - two.foo.com + claimToHeaders: + - claimName: claim.neteased.key + headerName: one-route-example2-key1 + - claimName: name + headerName: one-route-example2-key2 + issuer: https://www.two.example.com + payloadInMetadata: https://www.two.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: "192_168_1_250_8080" + timeout: 5s + uri: https://192.168.1.250:8080/jwt/public-key/jwks.json + retryPolicy: {} + second-route-www.test.com/example: + audiences: + - foo.com + claimToHeaders: + - claimName: claim.neteased.key + headerName: second-route-example-key1 + issuer: https://www.example.com + payloadInMetadata: https://www.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: localhost_443 + timeout: 5s + uri: https://localhost/jwt/public-key/jwks.json + retryPolicy: {} + second-route-www.test.com/example2: + audiences: + - one.foo.com + - two.foo.com + issuer: https://www.two.example.com + payloadInMetadata: https://www.two.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: "192_168_1_250_8080" + timeout: 5s + uri: https://192.168.1.250:8080/jwt/public-key/jwks.json + retryPolicy: {} + requirementMap: + first-route-www.test.com: + requiresAny: + requirements: + - providerName: first-route-www.test.com/example + - providerName: first-route-www.test.com/example2 + second-route-www.test.com: + requiresAny: + requirements: + - providerName: second-route-www.test.com/example + - providerName: second-route-www.test.com/example2 + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.routes.yaml new file mode 100755 index 00000000000..630157c4b40 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-multi-provider.routes.yaml @@ -0,0 +1,25 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo/bar + name: first-route-www.test.com + route: + cluster: first-route-www.test.com-dest + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: first-route-www.test.com + - match: + path: foo/baz + name: second-route-www.test.com + route: + cluster: second-route-www.test.com-dest + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: second-route-www.test.com diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.clusters.yaml new file mode 100644 index 00000000000..35bc85eaf5b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.clusters.yaml @@ -0,0 +1,59 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + lbPolicy: LEAST_REQUEST + name: second-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: localhost_443 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: localhost + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} + name: localhost_443 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + validationContext: + trustedCa: + filename: /etc/ssl/certs/ca-certificates.crt + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.endpoints.yaml new file mode 100644 index 00000000000..b321ca1f8e7 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.endpoints.yaml @@ -0,0 +1,22 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 5.6.7.8 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.listeners.yaml new file mode 100644 index 00000000000..133530ef79f --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.listeners.yaml @@ -0,0 +1,93 @@ +- accessLog: + - filter: + responseFlagFilter: + flags: + - NR + name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + logFormat: + textFormatSource: + inlineString: | + {"start_time":"%START_TIME%","method":"%REQ(:METHOD)%","x-envoy-origin-path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","response_code_details":"%RESPONSE_CODE_DETAILS%","connection_termination_details":"%CONNECTION_TERMINATION_DETAILS%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","duration":"%DURATION%","x-envoy-upstream-service-time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","x-forwarded-for":"%REQ(X-FORWARDED-FOR)%","user-agent":"%REQ(USER-AGENT)%","x-request-id":"%REQ(X-REQUEST-ID)%",":authority":"%REQ(:AUTHORITY)%","upstream_host":"%UPSTREAM_HOST%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","requested_server_name":"%REQUESTED_SERVER_NAME%","route_name":"%ROUTE_NAME%"} + path: /dev/stdout + address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + accessLog: + - name: envoy.access_loggers.file + typedConfig: + '@type': type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog + logFormat: + textFormatSource: + inlineString: | + {"start_time":"%START_TIME%","method":"%REQ(:METHOD)%","x-envoy-origin-path":"%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%","protocol":"%PROTOCOL%","response_code":"%RESPONSE_CODE%","response_flags":"%RESPONSE_FLAGS%","response_code_details":"%RESPONSE_CODE_DETAILS%","connection_termination_details":"%CONNECTION_TERMINATION_DETAILS%","upstream_transport_failure_reason":"%UPSTREAM_TRANSPORT_FAILURE_REASON%","bytes_received":"%BYTES_RECEIVED%","bytes_sent":"%BYTES_SENT%","duration":"%DURATION%","x-envoy-upstream-service-time":"%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%","x-forwarded-for":"%REQ(X-FORWARDED-FOR)%","user-agent":"%REQ(USER-AGENT)%","x-request-id":"%REQ(X-REQUEST-ID)%",":authority":"%REQ(:AUTHORITY)%","upstream_host":"%UPSTREAM_HOST%","upstream_cluster":"%UPSTREAM_CLUSTER%","upstream_local_address":"%UPSTREAM_LOCAL_ADDRESS%","downstream_local_address":"%DOWNSTREAM_LOCAL_ADDRESS%","downstream_remote_address":"%DOWNSTREAM_REMOTE_ADDRESS%","requested_server_name":"%REQUESTED_SERVER_NAME%","route_name":"%ROUTE_NAME%"} + path: /dev/stdout + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.jwt_authn + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + first-route/example: + audiences: + - foo.com + claimToHeaders: + - claimName: claim.neteased.key + headerName: first-route-key + issuer: https://www.example.com + payloadInMetadata: https://www.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: localhost_443 + timeout: 5s + uri: https://localhost/jwt/public-key/jwks.json + retryPolicy: {} + second-route/example: + audiences: + - foo.com + issuer: https://www.example.com + payloadInMetadata: https://www.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: localhost_443 + timeout: 5s + uri: https://localhost/jwt/public-key/jwks.json + retryPolicy: {} + requirementMap: + first-route: + providerName: first-route/example + second-route: + providerName: second-route/example + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.routes.yaml new file mode 100644 index 00000000000..2078809a694 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-multi-route-single-provider.routes.yaml @@ -0,0 +1,25 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo/bar + name: first-route + route: + cluster: first-route-dest + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: first-route + - match: + path: foo/baz + name: second-route + route: + cluster: second-route-dest + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: second-route diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.clusters.yaml new file mode 100644 index 00000000000..868cdb1b6c2 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.clusters.yaml @@ -0,0 +1,105 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: second-route-dest + lbPolicy: LEAST_REQUEST + name: second-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: third-route-dest + lbPolicy: LEAST_REQUEST + name: third-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: ratelimit_cluster + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: envoy-ratelimit.envoy-gateway-system.svc.cluster.local + portValue: 8081 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} + name: ratelimit_cluster + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + tlsCertificates: + - certificateChain: + filename: /certs/tls.crt + privateKey: + filename: /certs/tls.key + validationContext: + trustedCa: + filename: /certs/ca.crt + type: STRICT_DNS + typedExtensionProtocolOptions: + envoy.extensions.upstreams.http.v3.HttpProtocolOptions: + '@type': type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions + explicitHttpConfig: + http2ProtocolOptions: {} +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: "192_168_1_250_443" + lbPolicy: LEAST_REQUEST + name: "192_168_1_250_443" + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + validationContext: + trustedCa: + filename: /etc/ssl/certs/ca-certificates.crt + type: EDS diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.endpoints.yaml new file mode 100644 index 00000000000..d8d04b17185 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.endpoints.yaml @@ -0,0 +1,44 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} +- clusterName: second-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} +- clusterName: third-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} +- clusterName: "192_168_1_250_443" + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 192.168.1.250 + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.listeners.yaml new file mode 100644 index 00000000000..8ff2832d64b --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.listeners.yaml @@ -0,0 +1,63 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.jwt_authn + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + first-route/example: + audiences: + - foo.com + issuer: https://www.example.com + payloadInMetadata: https://www.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: "192_168_1_250_443" + timeout: 5s + uri: https://192.168.1.250/jwt/public-key/jwks.json + retryPolicy: {} + requirementMap: + first-route: + providerName: first-route/example + - name: envoy.filters.http.ratelimit + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: first-listener + enableXRatelimitHeaders: DRAFT_VERSION_03 + rateLimitService: + grpcService: + envoyGrpc: + clusterName: ratelimit_cluster + transportApiVersion: V3 + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.routes.yaml new file mode 100644 index 00000000000..0223e989d39 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-ratelimit.routes.yaml @@ -0,0 +1,46 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo/bar + name: first-route + route: + cluster: first-route-dest + rateLimits: + - actions: + - headerValueMatch: + descriptorKey: first-route-key-rule-0-match-0 + descriptorValue: first-route-value-rule-0-match-0 + expectMatch: true + headers: + - name: x-user-id + stringMatch: + exact: one + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: first-route + - match: + path: example + name: second-route + route: + cluster: second-route-dest + rateLimits: + - actions: + - requestHeaders: + descriptorKey: second-route-key-rule-0-match-0 + headerName: x-user-id + - match: + path: test + name: third-route + route: + cluster: third-route-dest + rateLimits: + - actions: + - genericKey: + descriptorKey: third-route-key-rule-0-match--1 + descriptorValue: third-route-value-rule-0-match--1 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.clusters.yaml new file mode 100644 index 00000000000..7a0c933174e --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.clusters.yaml @@ -0,0 +1,45 @@ +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: first-route-dest + lbPolicy: LEAST_REQUEST + name: first-route-dest + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + type: EDS +- commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_ONLY + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: localhost_443 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: localhost + portValue: 443 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} + name: localhost_443 + outlierDetection: {} + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + validationContext: + trustedCa: + filename: /etc/ssl/certs/ca-certificates.crt + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.endpoints.yaml new file mode 100644 index 00000000000..0d68b430c20 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.endpoints.yaml @@ -0,0 +1,11 @@ +- clusterName: first-route-dest + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 1.2.3.4 + portValue: 50000 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: {} diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.listeners.yaml new file mode 100644 index 00000000000..9a095ee2e29 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.listeners.yaml @@ -0,0 +1,53 @@ +- address: + socketAddress: + address: 0.0.0.0 + portValue: 10080 + defaultFilterChain: + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + commonHttpProtocolOptions: + headersWithUnderscoresAction: REJECT_REQUEST + http2ProtocolOptions: + initialConnectionWindowSize: 1048576 + initialStreamWindowSize: 65536 + maxConcurrentStreams: 100 + httpFilters: + - name: envoy.filters.http.jwt_authn + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication + providers: + first-route/example: + audiences: + - foo.com + issuer: https://www.example.com + payloadInMetadata: https://www.example.com + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: localhost_443 + timeout: 5s + uri: https://localhost/jwt/public-key/jwks.json + retryPolicy: {} + requirementMap: + first-route: + providerName: first-route/example + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: first-listener + statPrefix: http + upgradeConfigs: + - upgradeType: websocket + useRemoteAddress: true + name: first-listener + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.routes.yaml new file mode 100644 index 00000000000..c73bec09093 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-authn-single-route-single-match.routes.yaml @@ -0,0 +1,16 @@ +- ignorePortInHostMatching: true + name: first-listener + virtualHosts: + - domains: + - '*' + name: first-listener/* + routes: + - match: + path: foo/bar + name: first-route + route: + cluster: first-route-dest + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: first-route diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go index a72b493e986..e13f8f4cc93 100644 --- a/internal/xds/translator/translator.go +++ b/internal/xds/translator/translator.go @@ -254,10 +254,16 @@ func (t *Translator) processHTTPListenerXdsTranslation(tCtx *types.ResourceVersi return err } - // Create authn jwks clusters, if needed. + // Create authn jwks clusters, if needed. // TODO zhaohuabing remove this after deprecating authentication filter if err := createJwksClusters(tCtx, httpListener.Routes); err != nil { return err } + + // Create authn jwks clusters, if needed. + if err := createJWKSClusters(tCtx, httpListener.Routes); err != nil { + return err + } + // Check if an extension want to modify the listener that was just configured/created // If no extension exists (or it doesn't subscribe to this hook) then this is a quick no-op if err := processExtensionPostListenerHook(tCtx, xdsListener, t.ExtensionManager); err != nil { diff --git a/internal/xds/translator/translator_test.go b/internal/xds/translator/translator_test.go index 2ca793722f1..6c6fd275397 100644 --- a/internal/xds/translator/translator_test.go +++ b/internal/xds/translator/translator_test.go @@ -193,6 +193,18 @@ func TestTranslateXds(t *testing.T) { { name: "cors", }, + { + name: "jwt-authn-multi-route-multi-provider", + }, + { + name: "jwt-authn-multi-route-single-provider", + }, + { + name: "jwt-authn-ratelimit", + }, + { + name: "jwt-authn-single-route-single-match", + }, } for _, tc := range testCases { diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 5e680fc52ba..b79f1dc2b94 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -158,6 +158,7 @@ _Appears in:_ ClaimToHeader defines a configuration to convert JWT claims into HTTP headers _Appears in:_ +- [JWTProvider](#jwtprovider) - [JwtAuthenticationFilterProvider](#jwtauthenticationfilterprovider) | Field | Description | @@ -906,6 +907,39 @@ _Appears in:_ +#### JWT + + + +JWT defines the configuration for JSON Web Token (JWT) authentication. + +_Appears in:_ +- [SecurityPolicySpec](#securitypolicyspec) + +| Field | Description | +| --- | --- | +| `providers` _[JWTProvider](#jwtprovider) array_ | Providers defines the JSON Web Token (JWT) authentication provider type. + When multiple JWT providers are specified, the JWT is considered valid if any of the providers successfully validate the JWT. For additional details, see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter.html. | + + +#### JWTProvider + + + +JWTProvider defines how a JSON Web Token (JWT) can be verified. + +_Appears in:_ +- [JWT](#jwt) + +| Field | Description | +| --- | --- | +| `name` _string_ | 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. | +| `issuer` _string_ | 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_ | 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)_ | RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. | +| `claimToHeaders` _[ClaimToHeader](#claimtoheader) array_ | 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 | + + #### JwtAuthenticationFilterProvider @@ -1560,6 +1594,7 @@ _Appears in:_ RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. _Appears in:_ +- [JWTProvider](#jwtprovider) - [JwtAuthenticationFilterProvider](#jwtauthenticationfilterprovider) | Field | Description | @@ -1639,6 +1674,7 @@ _Appears in:_ | --- | --- | | `targetRef` _[PolicyTargetReferenceWithSectionName](#policytargetreferencewithsectionname)_ | TargetRef is the name of the Gateway resource this policy is being attached to. This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway. TargetRef | | `cors` _[CORS](#cors)_ | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). | +| `jwt` _[JWT](#jwt)_ | JWT defines the configuration for JSON Web Token (JWT) authentication. |