diff --git a/api/v1alpha1/jwt_types.go b/api/v1alpha1/jwt_types.go index 4bc8a994f5d..19825afdeee 100644 --- a/api/v1alpha1/jwt_types.go +++ b/api/v1alpha1/jwt_types.go @@ -75,17 +75,28 @@ type JWTProvider struct { ExtractFrom *JWTExtractor `json:"extractFrom,omitempty"` } -// RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote -// HTTP/HTTPS endpoint. +// RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. +// +kubebuilder:validation:XValidation:rule="!has(self.backendRef)",message="BackendRefs must be used, backendRef is not supported." +// +kubebuilder:validation:XValidation:rule="has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? !has(self.backendSettings.retry.perRetry.timeout):true):true):true",message="Retry timeout is not supported." +// +kubebuilder:validation:XValidation:rule="has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true",message="HTTPStatusCodes is not supported." type RemoteJWKS struct { - // URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to - // validate the server certificate. + // BackendRefs is used to specify the address of the Remote JWKS. The BackendRefs are optional, if not specified, + // the backend service is extracted from the host and port of the URI field. + // + // TLS configuration can be specified in a BackendTLSConfig resource and target the BackendRefs. + // + // Other settings for the connection to remote JWKS can be specified in the BackendSettings resource. + // Currently, only the retry policy is supported. + // + // +optional + BackendCluster `json:",inline"` + + // URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to validate the server certificate. + // If a custom trust bundle is needed, it can be specified in a BackendTLSConfig resource and target the BackendRefs. // // +kubebuilder:validation:MinLength=1 // +kubebuilder:validation:MaxLength=253 URI string `json:"uri"` - - // TODO: Add TBD remote JWKS fields based on defined use cases. } // ClaimToHeader defines a configuration to convert JWT claims into HTTP headers diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go index 8591cc20f0d..de05eb16782 100644 --- a/api/v1alpha1/oidc_types.go +++ b/api/v1alpha1/oidc_types.go @@ -116,6 +116,7 @@ type OIDCProvider struct { // TLS configuration can be specified in a BackendTLSConfig resource and target the BackendRefs. // // Other settings for the connection to the OIDC Provider can be specified in the BackendSettings resource. + // Currently, only the retry policy is supported. // // +optional BackendCluster `json:",inline"` diff --git a/api/v1alpha1/validation/securitypolicy_validate.go b/api/v1alpha1/validation/securitypolicy_validate.go deleted file mode 100644 index d06e2303b2a..00000000000 --- a/api/v1alpha1/validation/securitypolicy_validate.go +++ /dev/null @@ -1,154 +0,0 @@ -// 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" - "strings" - - 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, err) - } - - 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 err := ValidateJWTProvider(spec.JWT.Providers); err != nil { - errs = append(errs, err) - } - case spec.Authorization != nil: - sum++ - case spec.APIKeyAuth != nil: - sum++ - case spec.BasicAuth != nil: - sum++ - case spec.ExtAuth != nil: - sum++ - case spec.OIDC != 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 := ValidateAPIKeyAuth(spec.APIKeyAuth); err != nil { - errs = append(errs, err) - } - return utilerrors.NewAggregate(errs) -} - -func ValidateAPIKeyAuth(p *egv1a1.APIKeyAuth) error { - if p == nil { - return nil - } - - for _, keySource := range p.ExtractFrom { - // only one of headers, params or cookies is supposed to be specified. - if len(keySource.Headers) > 0 && len(keySource.Params) > 0 || - len(keySource.Headers) > 0 && len(keySource.Cookies) > 0 || - len(keySource.Params) > 0 && len(keySource.Cookies) > 0 { - return errors.New("only one of headers, params or cookies must be specified") - } - } - return nil -} - -// 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: - switch { - // Issuer follows StringOrURI format based on https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1. - // Hence, when it contains ':', it MUST be a valid URI. - case strings.Contains(provider.Issuer, ":"): - if _, err := url.ParseRequestURI(provider.Issuer); err != nil { - errs = append(errs, fmt.Errorf("invalid issuer; when issuer contains ':' character, it MUST be a valid URI")) - } - // Adding reserved character for '@', to represent an email address. - // Hence, when it contains '@', it MUST be a valid Email Address. - case strings.Contains(provider.Issuer, "@"): - if _, err := mail.ParseAddress(provider.Issuer); err != nil { - errs = append(errs, fmt.Errorf("invalid issuer; when issuer contains '@' character, it MUST be a valid Email Address format: %w", 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: %w", 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 deleted file mode 100644 index 02cebd09bde..00000000000 --- a/api/v1alpha1/validation/securitypolicy_validate_test.go +++ /dev/null @@ -1,530 +0,0 @@ -// 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 URI 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", - 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 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", - 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 non URI/Email 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", - Issuer: "foo.bar.local", - Audiences: []string{"foo.bar.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, - }, - { - name: "only one of header, query or cookie is supposed to be specified", - policy: &egv1a1.SecurityPolicy{ - TypeMeta: metav1.TypeMeta{ - Kind: egv1a1.KindSecurityPolicy, - APIVersion: egv1a1.GroupVersion.String(), - }, - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "test", - }, - Spec: egv1a1.SecurityPolicySpec{ - APIKeyAuth: &egv1a1.APIKeyAuth{ - ExtractFrom: []*egv1a1.ExtractFrom{ - { - Headers: []string{"header"}, - Params: []string{"param"}, - }, - }, - }, - }, - }, - }, - } - - 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 a0b728391cc..575e2084f35 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3441,7 +3441,7 @@ func (in *JWTProvider) DeepCopyInto(out *JWTProvider) { *out = make([]string, len(*in)) copy(*out, *in) } - out.RemoteJWKS = in.RemoteJWKS + in.RemoteJWKS.DeepCopyInto(&out.RemoteJWKS) if in.ClaimToHeaders != nil { in, out := &in.ClaimToHeaders, &out.ClaimToHeaders *out = make([]ClaimToHeader, len(*in)) @@ -5038,6 +5038,7 @@ func (in *RedisTLSSettings) DeepCopy() *RedisTLSSettings { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RemoteJWKS) DeepCopyInto(out *RemoteJWKS) { *out = *in + in.BackendCluster.DeepCopyInto(&out.BackendCluster) } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteJWKS. 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 8ee490cf032..103d1fb7caa 100644 --- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml +++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml @@ -2356,16 +2356,890 @@ spec: RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. properties: + backendRef: + description: |- + BackendRef references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + + Deprecated: Use BackendRefs instead. + properties: + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + backendRefs: + description: |- + BackendRefs references a Kubernetes object that represents the + backend server to which the authorization request will be sent. + items: + description: BackendRef defines how an ObjectReference + that is specific to BackendRef. + properties: + fallback: + description: |- + Fallback indicates whether the backend is designated as a fallback. + Multiple fallback backends can be configured. + It is highly recommended to configure active or passive health checks to ensure that failover can be detected + when the active backends become unhealthy and to automatically readjust once the primary backends are healthy again. + The overprovisioning factor is set to 1.4, meaning the fallback backends will only start receiving traffic when + the health of the active backends falls below 72%. + type: boolean + group: + default: "" + description: |- + Group is the group of the referent. For example, "gateway.networking.k8s.io". + When unspecified or empty string, core API group is inferred. + maxLength: 253 + pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$ + type: string + kind: + default: Service + description: |- + Kind is the Kubernetes resource kind of the referent. For example + "Service". + + Defaults to "Service" when not specified. + + ExternalName services can refer to CNAME DNS records that may live + outside of the cluster and as such are difficult to reason about in + terms of conformance. They also may not be safe to forward to (see + CVE-2021-25740 for more information). Implementations SHOULD NOT + support ExternalName Services. + + Support: Core (Services with a type other than ExternalName) + + Support: Implementation-specific (Services with type ExternalName) + maxLength: 63 + minLength: 1 + pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$ + type: string + name: + description: Name is the name of the referent. + maxLength: 253 + minLength: 1 + type: string + namespace: + description: |- + Namespace is the namespace of the backend. When unspecified, the local + namespace is inferred. + + Note that when a namespace different than the local namespace is specified, + a ReferenceGrant object is required in the referent namespace to allow that + namespace's owner to accept the reference. See the ReferenceGrant + documentation for details. + + Support: Core + maxLength: 63 + minLength: 1 + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + port: + description: |- + Port specifies the destination port number to use for this resource. + Port is required when the referent is a Kubernetes Service. In this + case, the port number is the service port number, not the target port. + For other resources, destination port might be derived from the referent + resource or this field. + format: int32 + maximum: 65535 + minimum: 1 + type: integer + required: + - name + type: object + x-kubernetes-validations: + - message: Must have port for Service reference + rule: '(size(self.group) == 0 && self.kind == ''Service'') + ? has(self.port) : true' + maxItems: 16 + type: array + backendSettings: + description: |- + BackendSettings holds configuration for managing the connection + to the backend. + properties: + circuitBreaker: + description: |- + Circuit Breaker settings for the upstream connections and requests. + If not set, circuit breakers will be enabled with the default thresholds + properties: + maxConnections: + default: 1024 + description: The maximum number of connections + that Envoy will establish to the referenced + backend defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRequests: + default: 1024 + description: The maximum number of parallel + requests that Envoy will make to the referenced + backend defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxParallelRetries: + default: 1024 + description: The maximum number of parallel + retries that Envoy will make to the referenced + backend defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxPendingRequests: + default: 1024 + description: The maximum number of pending requests + that Envoy will queue to the referenced backend + defined within a xRoute rule. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + maxRequestsPerConnection: + description: |- + The maximum number of requests that Envoy will make over a single connection to the referenced backend defined within a xRoute rule. + Default: unlimited. + format: int64 + maximum: 4294967295 + minimum: 0 + type: integer + type: object + connection: + description: Connection includes backend connection + settings. + properties: + bufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + BufferLimit Soft limit on size of the cluster’s connections read and write buffers. + BufferLimit applies to connection streaming (maybe non-streaming) channel between processes, it's in user space. + If unspecified, an implementation defined default is applied (32768 bytes). + For example, 20Mi, 1Gi, 256Ki etc. + Note: that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + socketBufferLimit: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + SocketBufferLimit provides configuration for the maximum buffer size in bytes for each socket + to backend. + SocketBufferLimit applies to socket streaming channel between TCP/IP stacks, it's in kernel space. + For example, 20Mi, 1Gi, 256Ki etc. + Note that when the suffix is not provided, the value is interpreted as bytes. + x-kubernetes-int-or-string: true + type: object + dns: + description: DNS includes dns resolution settings. + properties: + dnsRefreshRate: + description: |- + DNSRefreshRate specifies the rate at which DNS records should be refreshed. + Defaults to 30 seconds. + type: string + respectDnsTtl: + description: |- + RespectDNSTTL indicates whether the DNS Time-To-Live (TTL) should be respected. + If the value is set to true, the DNS refresh rate will be set to the resource record’s TTL. + Defaults to true. + type: boolean + type: object + healthCheck: + description: HealthCheck allows gateway to perform + active health checking on backends. + properties: + active: + description: Active health check configuration + properties: + grpc: + description: |- + GRPC defines the configuration of the GRPC health checker. + It's optional, and can only be used if the specified type is GRPC. + properties: + service: + description: |- + Service to send in the health check request. + If this is not specified, then the health check request applies to the entire + server and not to a specific service. + type: string + type: object + healthyThreshold: + default: 1 + description: HealthyThreshold defines the + number of healthy health checks required + before a backend host is marked healthy. + format: int32 + minimum: 1 + type: integer + http: + description: |- + HTTP defines the configuration of http health checker. + It's required while the health checker type is HTTP. + properties: + expectedResponse: + description: ExpectedResponse defines + a list of HTTP expected responses + to match. + properties: + binary: + description: Binary payload base64 + encoded. + format: byte + type: string + text: + description: Text payload in plain + text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines the type + of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is Text, + text field needs to be set. + rule: 'self.type == ''Text'' ? has(self.text) + : !has(self.text)' + - message: If payload type is Binary, + binary field needs to be set. + rule: 'self.type == ''Binary'' ? has(self.binary) + : !has(self.binary)' + expectedStatuses: + description: |- + ExpectedStatuses defines a list of HTTP response statuses considered healthy. + Defaults to 200 only + items: + description: HTTPStatus defines the + http status code. + exclusiveMaximum: true + maximum: 600 + minimum: 100 + type: integer + type: array + method: + description: |- + Method defines the HTTP method used for health checking. + Defaults to GET + type: string + path: + description: Path defines the HTTP path + that will be requested during health + checking. + maxLength: 1024 + minLength: 1 + type: string + required: + - path + type: object + interval: + default: 3s + description: Interval defines the time between + active health checks. + format: duration + type: string + tcp: + description: |- + TCP defines the configuration of tcp health checker. + It's required while the health checker type is TCP. + properties: + receive: + description: Receive defines the expected + response payload. + properties: + binary: + description: Binary payload base64 + encoded. + format: byte + type: string + text: + description: Text payload in plain + text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines the type + of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is Text, + text field needs to be set. + rule: 'self.type == ''Text'' ? has(self.text) + : !has(self.text)' + - message: If payload type is Binary, + binary field needs to be set. + rule: 'self.type == ''Binary'' ? has(self.binary) + : !has(self.binary)' + send: + description: Send defines the request + payload. + properties: + binary: + description: Binary payload base64 + encoded. + format: byte + type: string + text: + description: Text payload in plain + text. + type: string + type: + allOf: + - enum: + - Text + - Binary + - enum: + - Text + - Binary + description: Type defines the type + of the payload. + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If payload type is Text, + text field needs to be set. + rule: 'self.type == ''Text'' ? has(self.text) + : !has(self.text)' + - message: If payload type is Binary, + binary field needs to be set. + rule: 'self.type == ''Binary'' ? has(self.binary) + : !has(self.binary)' + type: object + timeout: + default: 1s + description: Timeout defines the time to + wait for a health check response. + format: duration + type: string + type: + allOf: + - enum: + - HTTP + - TCP + - GRPC + - enum: + - HTTP + - TCP + - GRPC + description: Type defines the type of health + checker. + type: string + unhealthyThreshold: + default: 3 + description: UnhealthyThreshold defines + the number of unhealthy health checks + required before a backend host is marked + unhealthy. + format: int32 + minimum: 1 + type: integer + required: + - type + type: object + x-kubernetes-validations: + - message: If Health Checker type is HTTP, http + field needs to be set. + rule: 'self.type == ''HTTP'' ? has(self.http) + : !has(self.http)' + - message: If Health Checker type is TCP, tcp + field needs to be set. + rule: 'self.type == ''TCP'' ? has(self.tcp) + : !has(self.tcp)' + - message: The grpc field can only be set if + the Health Checker type is GRPC. + rule: 'has(self.grpc) ? self.type == ''GRPC'' + : true' + passive: + description: Passive passive check configuration + properties: + baseEjectionTime: + default: 30s + description: BaseEjectionTime defines the + base duration for which a host will be + ejected on consecutive failures. + format: duration + type: string + consecutive5XxErrors: + default: 5 + description: Consecutive5xxErrors sets the + number of consecutive 5xx errors triggering + ejection. + format: int32 + type: integer + consecutiveGatewayErrors: + default: 0 + description: ConsecutiveGatewayErrors sets + the number of consecutive gateway errors + triggering ejection. + format: int32 + type: integer + consecutiveLocalOriginFailures: + default: 5 + description: |- + ConsecutiveLocalOriginFailures sets the number of consecutive local origin failures triggering ejection. + Parameter takes effect only when split_external_local_origin_errors is set to true. + format: int32 + type: integer + interval: + default: 3s + description: Interval defines the time between + passive health checks. + format: duration + type: string + maxEjectionPercent: + default: 10 + description: MaxEjectionPercent sets the + maximum percentage of hosts in a cluster + that can be ejected. + format: int32 + type: integer + splitExternalLocalOriginErrors: + default: false + description: SplitExternalLocalOriginErrors + enables splitting of errors between external + and local origin. + type: boolean + type: object + type: object + http2: + description: HTTP2 provides HTTP/2 configuration + for backend connections. + properties: + initialConnectionWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialConnectionWindowSize sets the initial window size for HTTP/2 connections. + If not set, the default value is 1 MiB. + x-kubernetes-int-or-string: true + initialStreamWindowSize: + allOf: + - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + - pattern: ^[1-9]+[0-9]*([EPTGMK]i|[EPTGMk])?$ + anyOf: + - type: integer + - type: string + description: |- + InitialStreamWindowSize sets the initial window size for HTTP/2 streams. + If not set, the default value is 64 KiB(64*1024). + x-kubernetes-int-or-string: true + maxConcurrentStreams: + description: |- + MaxConcurrentStreams sets the maximum number of concurrent streams allowed per connection. + If not set, the default value is 100. + format: int32 + maximum: 2147483647 + minimum: 1 + type: integer + onInvalidMessage: + description: |- + OnInvalidMessage determines if Envoy will terminate the connection or just the offending stream in the event of HTTP messaging error + It's recommended for L2 Envoy deployments to set this value to TerminateStream. + https://www.envoyproxy.io/docs/envoy/latest/configuration/best_practices/level_two + Default: TerminateConnection + type: string + type: object + loadBalancer: + description: |- + LoadBalancer policy to apply when routing traffic from the gateway to + the backend endpoints. Defaults to `LeastRequest`. + properties: + consistentHash: + description: |- + ConsistentHash defines the configuration when the load balancer type is + set to ConsistentHash + properties: + cookie: + description: Cookie configures the cookie + hash policy when the consistent hash type + is set to Cookie. + properties: + attributes: + additionalProperties: + type: string + description: Additional Attributes to + set for the generated cookie. + type: object + name: + description: |- + Name of the cookie to hash. + If this cookie does not exist in the request, Envoy will generate a cookie and set + the TTL on the response back to the client based on Layer 4 + attributes of the backend endpoint, to ensure that these future requests + go to the same backend endpoint. Make sure to set the TTL field for this case. + type: string + ttl: + description: |- + TTL of the generated cookie if the cookie is not present. This value sets the + Max-Age attribute value. + type: string + required: + - name + type: object + header: + description: Header configures the header + hash policy when the consistent hash type + is set to Header. + properties: + name: + description: Name of the header to hash. + type: string + required: + - name + type: object + tableSize: + default: 65537 + description: The table size for consistent + hashing, must be prime number limited + to 5000011. + format: int64 + maximum: 5000011 + minimum: 2 + type: integer + type: + description: |- + ConsistentHashType defines the type of input to hash on. Valid Type values are + "SourceIP", + "Header", + "Cookie". + enum: + - SourceIP + - Header + - Cookie + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If consistent hash type is header, + the header field must be set. + rule: 'self.type == ''Header'' ? has(self.header) + : !has(self.header)' + - message: If consistent hash type is cookie, + the cookie field must be set. + rule: 'self.type == ''Cookie'' ? has(self.cookie) + : !has(self.cookie)' + slowStart: + description: |- + SlowStart defines the configuration related to the slow start load balancer policy. + If set, during slow start window, traffic sent to the newly added hosts will gradually increase. + Currently this is only supported for RoundRobin and LeastRequest load balancers + properties: + window: + description: |- + Window defines the duration of the warm up period for newly added host. + During slow start window, traffic sent to the newly added hosts will gradually increase. + Currently only supports linear growth of traffic. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/cluster/v3/cluster.proto#config-cluster-v3-cluster-slowstartconfig + type: string + required: + - window + type: object + type: + description: |- + Type decides the type of Load Balancer policy. + Valid LoadBalancerType values are + "ConsistentHash", + "LeastRequest", + "Random", + "RoundRobin". + enum: + - ConsistentHash + - LeastRequest + - Random + - RoundRobin + type: string + required: + - type + type: object + x-kubernetes-validations: + - message: If LoadBalancer type is consistentHash, + consistentHash field needs to be set. + rule: 'self.type == ''ConsistentHash'' ? has(self.consistentHash) + : !has(self.consistentHash)' + - message: Currently SlowStart is only supported + for RoundRobin and LeastRequest load balancers. + rule: 'self.type in [''Random'', ''ConsistentHash''] + ? !has(self.slowStart) : true ' + proxyProtocol: + description: ProxyProtocol enables the Proxy Protocol + when communicating with the backend. + properties: + version: + description: |- + Version of ProxyProtol + Valid ProxyProtocolVersion values are + "V1" + "V2" + enum: + - V1 + - V2 + type: string + required: + - version + type: object + retry: + description: |- + Retry provides more advanced usage, allowing users to customize the number of retries, retry fallback strategy, and retry triggering conditions. + If not set, retry will be disabled. + properties: + numRetries: + default: 2 + description: NumRetries is the number of retries + to be attempted. Defaults to 2. + format: int32 + minimum: 0 + type: integer + perRetry: + description: PerRetry is the retry policy to + be applied per retry attempt. + properties: + backOff: + description: |- + Backoff is the backoff policy to be applied per retry attempt. gateway uses a fully jittered exponential + back-off algorithm for retries. For additional details, + see https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/router_filter#config-http-filters-router-x-envoy-max-retries + properties: + baseInterval: + description: BaseInterval is the base + interval between retries. + format: duration + type: string + maxInterval: + description: |- + MaxInterval is the maximum interval between retries. This parameter is optional, but must be greater than or equal to the base_interval if set. + The default is 10 times the base_interval + format: duration + type: string + type: object + timeout: + description: Timeout is the timeout per + retry attempt. + format: duration + type: string + type: object + retryOn: + description: |- + RetryOn specifies the retry trigger condition. + + If not specified, the default is to retry on connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes(503). + properties: + httpStatusCodes: + description: |- + HttpStatusCodes specifies the http status codes to be retried. + The retriable-status-codes trigger must also be configured for these status codes to trigger a retry. + items: + description: HTTPStatus defines the http + status code. + exclusiveMaximum: true + maximum: 600 + minimum: 100 + type: integer + type: array + triggers: + description: Triggers specifies the retry + trigger condition(Http/Grpc). + items: + description: TriggerEnum specifies the + conditions that trigger retries. + enum: + - 5xx + - gateway-error + - reset + - connect-failure + - retriable-4xx + - refused-stream + - retriable-status-codes + - cancelled + - deadline-exceeded + - internal + - resource-exhausted + - unavailable + type: string + type: array + type: object + type: object + tcpKeepalive: + description: |- + TcpKeepalive settings associated with the upstream client connection. + Disabled by default. + properties: + idleTime: + description: |- + The duration a connection needs to be idle before keep-alive + probes start being sent. + The duration format is + Defaults to `7200s`. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + interval: + description: |- + The duration between keep-alive probes. + Defaults to `75s`. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + probes: + description: |- + The total number of unacknowledged probes to send before deciding + the connection is dead. + Defaults to 9. + format: int32 + type: integer + type: object + timeout: + description: Timeout settings for the backend connections. + properties: + http: + description: Timeout settings for HTTP. + properties: + connectionIdleTimeout: + description: |- + The idle timeout for an HTTP connection. Idle time is defined as a period in which there are no active requests in the connection. + Default: 1 hour. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + maxConnectionDuration: + description: |- + The maximum duration of an HTTP connection. + Default: unlimited. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + requestTimeout: + description: RequestTimeout is the time + until which entire response is received + from the upstream. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + tcp: + description: Timeout settings for TCP. + properties: + connectTimeout: + description: |- + The timeout for network connection establishment, including TCP and TLS handshakes. + Default: 10 seconds. + pattern: ^([0-9]{1,5}(h|m|s|ms)){1,4}$ + type: string + type: object + type: object + type: object uri: description: |- - URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to - validate the server certificate. + URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to validate the server certificate. + If a custom trust bundle is needed, it can be specified in a BackendTLSConfig resource and target the BackendRefs. maxLength: 253 minLength: 1 type: string required: - uri type: object + x-kubernetes-validations: + - message: BackendRefs must be used, backendRef is not supported. + rule: '!has(self.backendRef)' + - message: Retry timeout is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.perRetry)? + !has(self.backendSettings.retry.perRetry.timeout):true):true):true + - message: HTTPStatusCodes is not supported. + rule: has(self.backendSettings)? (has(self.backendSettings.retry)?(has(self.backendSettings.retry.retryOn)? + !has(self.backendSettings.retry.retryOn.httpStatusCodes):true):true):true required: - name - remoteJWKS diff --git a/internal/cmd/egctl/testdata/translate/in/invalid-securitypolicy.yaml b/internal/cmd/egctl/testdata/translate/in/invalid-securitypolicy.yaml deleted file mode 100644 index bf754393b75..00000000000 --- a/internal/cmd/egctl/testdata/translate/in/invalid-securitypolicy.yaml +++ /dev/null @@ -1,103 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: GatewayClass -metadata: - name: eg -spec: - controllerName: gateway.envoyproxy.io/gatewayclass-controller ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: eg -spec: - gatewayClassName: eg - listeners: - - name: http - protocol: HTTP - port: 80 ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - name: backend ---- -apiVersion: v1 -kind: Service -metadata: - name: backend - labels: - app: backend - service: backend -spec: - clusterIP: 7.7.7.7 - ports: - - name: http - port: 3000 - targetPort: 3000 - selector: - app: backend ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: backend -spec: - replicas: 1 - selector: - matchLabels: - app: backend - version: v1 - template: - metadata: - labels: - app: backend - version: v1 - spec: - serviceAccountName: backend - containers: - - image: gcr.io/k8s-staging-gateway-api/echo-basic:v20231214-v1.0.0-140-gf544a46e - imagePullPolicy: IfNotPresent - name: backend - ports: - - containerPort: 3000 - env: - - name: POD_NAME - valueFrom: - fieldRef: - fieldPath: metadata.name - - name: NAMESPACE - valueFrom: - fieldRef: - fieldPath: metadata.namespace ---- -apiVersion: gateway.envoyproxy.io/v1alpha1 -kind: SecurityPolicy -metadata: - name: jwt-example -spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: backend -# No policy inside, which is invalid ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: backend -spec: - parentRefs: - - name: eg - hostnames: - - "www.example.com" - rules: - - backendRefs: - - group: "" - kind: Service - name: backend - port: 3000 - weight: 1 - matches: - - path: - type: PathPrefix - value: /foo diff --git a/internal/cmd/egctl/testdata/translate/out/invalid-securitypolicy.all.yaml b/internal/cmd/egctl/testdata/translate/out/invalid-securitypolicy.all.yaml deleted file mode 100644 index 9dc0fdfd2f7..00000000000 --- a/internal/cmd/egctl/testdata/translate/out/invalid-securitypolicy.all.yaml +++ /dev/null @@ -1,115 +0,0 @@ -gatewayClass: - kind: GatewayClass - metadata: - creationTimestamp: null - name: eg - namespace: envoy-gateway-system - spec: - controllerName: gateway.envoyproxy.io/gatewayclass-controller - status: - conditions: - - lastTransitionTime: null - message: Valid GatewayClass - reason: Accepted - status: "True" - type: Accepted -gateways: -- kind: Gateway - metadata: - creationTimestamp: null - name: eg - namespace: envoy-gateway-system - spec: - gatewayClassName: eg - listeners: - - 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 -httpRoutes: -- kind: HTTPRoute - metadata: - creationTimestamp: null - name: backend - namespace: envoy-gateway-system - spec: - hostnames: - - www.example.com - parentRefs: - - name: eg - rules: - - backendRefs: - - group: "" - kind: Service - name: backend - port: 3000 - weight: 1 - matches: - - path: - type: PathPrefix - value: /foo - 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: eg -securityPolicies: -- kind: SecurityPolicy - metadata: - creationTimestamp: null - name: jwt-example - namespace: envoy-gateway-system - spec: - targetRef: - group: gateway.networking.k8s.io - kind: HTTPRoute - name: backend - status: - ancestors: - - ancestorRef: - group: gateway.networking.k8s.io - kind: Gateway - name: eg - namespace: envoy-gateway-system - conditions: - - lastTransitionTime: null - message: 'Invalid SecurityPolicy: no security policy is specified.' - reason: Invalid - status: "False" - type: Accepted - controllerName: gateway.envoyproxy.io/gatewayclass-controller diff --git a/internal/cmd/egctl/translate_test.go b/internal/cmd/egctl/translate_test.go index 0dafcc49dae..e87167ce305 100644 --- a/internal/cmd/egctl/translate_test.go +++ b/internal/cmd/egctl/translate_test.go @@ -269,13 +269,6 @@ func TestTranslate(t *testing.T) { output: yamlOutput, expect: true, }, - { - name: "invalid-securitypolicy", - from: "gateway-api", - to: "gateway-api", - output: yamlOutput, - expect: true, - }, { name: "no-gateway-class-resources", from: "gateway-api", diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go index 405a0af23be..9d350531814 100644 --- a/internal/gatewayapi/securitypolicy.go +++ b/internal/gatewayapi/securitypolicy.go @@ -12,6 +12,7 @@ import ( "fmt" "net" "net/http" + "net/mail" "net/netip" "net/url" "sort" @@ -25,12 +26,12 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/utils/ptr" gwapiv1 "sigs.k8s.io/gateway-api/apis/v1" gwapiv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" - "github.com/envoyproxy/gateway/api/v1alpha1/validation" "github.com/envoyproxy/gateway/internal/gatewayapi/resource" "github.com/envoyproxy/gateway/internal/gatewayapi/status" "github.com/envoyproxy/gateway/internal/ir" @@ -151,7 +152,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security continue } - if err := validation.ValidateSecurityPolicy(policy); err != nil { + if err := validateSecurityPolicy(policy); err != nil { status.SetTranslationErrorForPolicyAncestors(&policy.Status, parentGateways, t.GatewayControllerName, @@ -260,6 +261,30 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security return res } +// validateSecurityPolicy validates the SecurityPolicy. +// It checks some constraints that are not covered by the CRD schema validation. +func validateSecurityPolicy(p *egv1a1.SecurityPolicy) error { + apiKeyAuth := p.Spec.APIKeyAuth + if apiKeyAuth != nil { + if err := validateAPIKeyAuth(apiKeyAuth); err != nil { + return err + } + } + return nil +} + +func validateAPIKeyAuth(apiKeyAuth *egv1a1.APIKeyAuth) error { + for _, keySource := range apiKeyAuth.ExtractFrom { + // only one of headers, params or cookies is supposed to be specified. + if len(keySource.Headers) > 0 && len(keySource.Params) > 0 || + len(keySource.Headers) > 0 && len(keySource.Cookies) > 0 || + len(keySource.Params) > 0 && len(keySource.Cookies) > 0 { + return errors.New("only one of headers, params or cookies must be specified") + } + } + return nil +} + func resolveSecurityPolicyGatewayTargetRef( policy *egv1a1.SecurityPolicy, target gwapiv1a2.LocalPolicyTargetReferenceWithSectionName, @@ -344,7 +369,6 @@ func (t *Translator) translateSecurityPolicyForRoute( // Build IR var ( cors *ir.CORS - jwt *ir.JWT apiKeyAuth *ir.APIKeyAuth basicAuth *ir.BasicAuth authorization *ir.Authorization @@ -355,10 +379,6 @@ func (t *Translator) translateSecurityPolicyForRoute( cors = t.buildCORS(policy.Spec.CORS) } - if policy.Spec.JWT != nil { - jwt = t.buildJWT(policy.Spec.JWT) - } - if policy.Spec.BasicAuth != nil { if basicAuth, err = t.buildBasicAuth( policy, @@ -409,12 +429,23 @@ func (t *Translator) translateSecurityPolicyForRoute( if oidc, err = t.buildOIDC( policy, resources, - gtwCtx.envoyProxy); err != nil { // TODO zhaohuabing: Only the last EnvoyProxy is used + gtwCtx.envoyProxy); err != nil { err = perr.WithMessage(err, "OIDC") errs = errors.Join(errs, err) } } + var jwt *ir.JWT + if policy.Spec.JWT != nil { + if jwt, err = t.buildJWT( + policy, + resources, + gtwCtx.envoyProxy); err != nil { + err = perr.WithMessage(err, "JWT") + errs = errors.Join(errs, err) + } + } + irKey := t.getIRKey(gtwCtx.Gateway) for _, listener := range parentRefCtx.listeners { irListener := xdsIR[irKey].GetHTTPListener(irListenerName(listener)) @@ -468,7 +499,13 @@ func (t *Translator) translateSecurityPolicyForGateway( } if policy.Spec.JWT != nil { - jwt = t.buildJWT(policy.Spec.JWT) + if jwt, err = t.buildJWT( + policy, + resources, + gateway.envoyProxy); err != nil { + err = perr.WithMessage(err, "JWT") + errs = errors.Join(errs, err) + } } if policy.Spec.OIDC != nil { @@ -595,11 +632,147 @@ func wildcard2regex(wildcard string) string { return regexStr } -func (t *Translator) buildJWT(jwt *egv1a1.JWT) *ir.JWT { +func (t *Translator) buildJWT( + policy *egv1a1.SecurityPolicy, + resources *resource.Resources, + envoyProxy *egv1a1.EnvoyProxy, +) (*ir.JWT, error) { + if err := validateJWTProvider(policy.Spec.JWT.Providers); err != nil { + return nil, err + } + + var providers []ir.JWTProvider + for i, p := range policy.Spec.JWT.Providers { + provider := ir.JWTProvider{ + Name: p.Name, + Issuer: p.Issuer, + Audiences: p.Audiences, + ClaimToHeaders: p.ClaimToHeaders, + RecomputeRoute: p.RecomputeRoute, + ExtractFrom: p.ExtractFrom, + } + + remoteJWKS, err := t.buildRemoteJWKS(policy, &p.RemoteJWKS, i, resources, envoyProxy) + if err != nil { + return nil, err + } + provider.RemoteJWKS = *remoteJWKS + providers = append(providers, provider) + } + return &ir.JWT{ - AllowMissing: ptr.Deref(jwt.Optional, false), - Providers: jwt.Providers, + AllowMissing: ptr.Deref(policy.Spec.JWT.Optional, false), + Providers: providers, + }, nil +} + +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: + switch { + // Issuer follows StringOrURI format based on https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1. + // Hence, when it contains ':', it MUST be a valid URI. + case strings.Contains(provider.Issuer, ":"): + if _, err := url.ParseRequestURI(provider.Issuer); err != nil { + errs = append(errs, fmt.Errorf("invalid issuer; when issuer contains ':' character, it MUST be a valid URI")) + } + // Adding reserved character for '@', to represent an email address. + // Hence, when it contains '@', it MUST be a valid Email Address. + case strings.Contains(provider.Issuer, "@"): + if _, err := mail.ParseAddress(provider.Issuer); err != nil { + errs = append(errs, fmt.Errorf("invalid issuer; when issuer contains '@' character, it MUST be a valid Email Address format: %w", 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: %w", 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 errors.Join(errs...) +} + +func (t *Translator) buildRemoteJWKS( + policy *egv1a1.SecurityPolicy, + remoteJWKS *egv1a1.RemoteJWKS, + index int, + resources *resource.Resources, + envoyProxy *egv1a1.EnvoyProxy, +) (*ir.RemoteJWKS, error) { + var ( + protocol ir.AppProtocol + rd *ir.RouteDestination + traffic *ir.TrafficFeatures + err error + ) + + u, err := url.Parse(remoteJWKS.URI) + if err != nil { + return nil, err + } + + if u.Scheme == "https" { + protocol = ir.HTTPS + } else { + protocol = ir.HTTP + } + + if len(remoteJWKS.BackendRefs) > 0 { + if rd, err = t.translateExtServiceBackendRefs( + policy, remoteJWKS.BackendRefs, protocol, resources, envoyProxy, "jwt", index); err != nil { + return nil, err + } + } + + if remoteJWKS.BackendSettings != nil { + if traffic, err = translateTrafficFeatures(remoteJWKS.BackendSettings); err != nil { + return nil, err + } + } + + return &ir.RemoteJWKS{ + Destination: rd, + Traffic: traffic, + URI: remoteJWKS.URI, + }, nil } func (t *Translator) buildOIDC( diff --git a/internal/gatewayapi/securitypolicy_test.go b/internal/gatewayapi/securitypolicy_test.go index 829144b81b0..b59bc528059 100644 --- a/internal/gatewayapi/securitypolicy_test.go +++ b/internal/gatewayapi/securitypolicy_test.go @@ -11,6 +11,8 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" ) func Test_wildcard2regex(t *testing.T) { @@ -150,3 +152,341 @@ func Test_extractRedirectPath(t *testing.T) { }) } } + +func Test_JWTProvider(t *testing.T) { + tests := []struct { + name string + Providers []egv1a1.JWTProvider + wantError bool + }{ + { + name: "valid security policy with URI issuer", + 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", + }, + }, + }, + }, + { + name: "valid security policy with Email issuer", + 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", + }, + }, + }, + }, + { + name: "valid security policy with non URI/Email Issuer", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "foo.bar.local", + Audiences: []string{"foo.bar.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + }, + { + name: "valid security policy with jwtClaimToHeader", + 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", + }, + }, + }, + }, + }, + + { + name: "unqualified authentication provider name", + 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", + }, + }, + }, + wantError: true, + }, + { + name: "unspecified provider name", + 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", + }, + }, + }, + wantError: true, + }, + + { + name: "non unique provider names", + 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", + }, + }, + }, + wantError: true, + }, + + { + name: "invalid issuer uri", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "http://invalid url.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "http://www.test.local", + }, + }, + }, + wantError: true, + }, + { + name: "inivalid issuer email", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "test@!123...", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + wantError: true, + }, + { + name: "invalid remote jwks uri", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "http://www.test.local", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "invalid/local", + }, + }, + }, + wantError: true, + }, + { + name: "unspecified remote jwks uri", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "", + }, + }, + }, + wantError: true, + }, + { + name: "unspecified jwtClaimToHeader headerName", + 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", + }, + }, + }, + }, + wantError: true, + }, + { + name: "unspecified jwtClaimToHeader claimName", + 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: "", + }, + }, + }, + }, + wantError: true, + }, + { + name: "unspecified issuer", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Audiences: []string{"test.local"}, + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + wantError: false, + }, + { + name: "unspecified audiences", + Providers: []egv1a1.JWTProvider{ + { + Name: "test", + Issuer: "https://www.test.local", + RemoteJWKS: egv1a1.RemoteJWKS{ + URI: "https://test.local/jwt/public-key/jwks.json", + }, + }, + }, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateJWTProvider(tt.Providers) + if (err != nil) != tt.wantError { + t.Errorf("validateJWTProvider() error = %v, wantErr %v", err, tt.wantError) + return + } + }) + } +} + +func Test_APIKeyAuth(t *testing.T) { + tests := []struct { + name string + APIKeyAuth egv1a1.APIKeyAuth + wantError bool + }{ + { + name: "only one of header, query or cookie is supposed to be specified", + APIKeyAuth: egv1a1.APIKeyAuth{ + ExtractFrom: []*egv1a1.ExtractFrom{ + { + Headers: []string{"header"}, + Params: []string{"param"}, + }, + }, + }, + wantError: true, + }, + { + name: "only one of header, query or cookie is supposed to be specified", + APIKeyAuth: egv1a1.APIKeyAuth{ + ExtractFrom: []*egv1a1.ExtractFrom{ + { + Headers: []string{"header"}, + Cookies: []string{"cookie"}, + }, + }, + }, + wantError: true, + }, + { + name: "only one of header, query or cookie is supposed to be specified", + APIKeyAuth: egv1a1.APIKeyAuth{ + ExtractFrom: []*egv1a1.ExtractFrom{ + { + Params: []string{"param"}, + Cookies: []string{"cookie"}, + }, + }, + }, + wantError: true, + }, + { + name: "only one of header, query or cookie is supposed to be specified", + APIKeyAuth: egv1a1.APIKeyAuth{ + ExtractFrom: []*egv1a1.ExtractFrom{ + { + Headers: []string{"header"}, + Params: []string{"param"}, + Cookies: []string{"cookie"}, + }, + }, + }, + wantError: true, + }, + { + name: "valid APIKeyAuth", + APIKeyAuth: egv1a1.APIKeyAuth{ + ExtractFrom: []*egv1a1.ExtractFrom{ + { + Headers: []string{"header"}, + }, + }, + }, + wantError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateAPIKeyAuth(&tt.APIKeyAuth) + if (err != nil) != tt.wantError { + t.Errorf("validateAPIKeyAuth() error = %v, wantErr %v", err, tt.wantError) + return + } + }) + } +} diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt-backendcluster.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt-backendcluster.in.yaml new file mode 100644 index 00000000000..57cdd9c3840 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt-backendcluster.in.yaml @@ -0,0 +1,126 @@ +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + namespace: default + name: gateway-1 + spec: + gatewayClassName: envoy-gateway-class + listeners: + - name: http + protocol: HTTP + port: 80 + allowedRoutes: + namespaces: + from: All +httpRoutes: +- apiVersion: gateway.networking.k8s.io/v1 + kind: HTTPRoute + metadata: + namespace: default + name: httproute-1 + spec: + hostnames: + - gateway.envoyproxy.io + parentRefs: + - namespace: default + name: gateway-1 + sectionName: http + rules: + - matches: + - path: + value: "/" + backendRefs: + - name: service-1 + port: 8080 +backends: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + name: backend-fqdn + namespace: default + spec: + endpoints: + - fqdn: + hostname: 'foo.bar.com' + port: 443 +configMaps: +- apiVersion: v1 + kind: ConfigMap + metadata: + name: ca-cmap + namespace: default + data: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIDJzCCAg+gAwIBAgIUAl6UKIuKmzte81cllz5PfdN2IlIwDQYJKoZIhvcNAQEL + BQAwIzEQMA4GA1UEAwwHbXljaWVudDEPMA0GA1UECgwGa3ViZWRiMB4XDTIzMTAw + MjA1NDE1N1oXDTI0MTAwMTA1NDE1N1owIzEQMA4GA1UEAwwHbXljaWVudDEPMA0G + A1UECgwGa3ViZWRiMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSTc + 1yj8HW62nynkFbXo4VXKv2jC0PM7dPVky87FweZcTKLoWQVPQE2p2kLDK6OEszmM + yyr+xxWtyiveremrWqnKkNTYhLfYPhgQkczib7eUalmFjUbhWdLvHakbEgCodn3b + kz57mInX2VpiDOKg4kyHfiuXWpiBqrCx0KNLpxo3DEQcFcsQTeTHzh4752GV04RU + Ti/GEWyzIsl4Rg7tGtAwmcIPgUNUfY2Q390FGqdH4ahn+mw/6aFbW31W63d9YJVq + ioyOVcaMIpM5B/c7Qc8SuhCI1YGhUyg4cRHLEw5VtikioyE3X04kna3jQAj54YbR + bpEhc35apKLB21HOUQIDAQABo1MwUTAdBgNVHQ4EFgQUyvl0VI5vJVSuYFXu7B48 + 6PbMEAowHwYDVR0jBBgwFoAUyvl0VI5vJVSuYFXu7B486PbMEAowDwYDVR0TAQH/ + BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAMLxrgFVMuNRq2wAwcBt7SnNR5Cfz + 2MvXq5EUmuawIUi9kaYjwdViDREGSjk7JW17vl576HjDkdfRwi4E28SydRInZf6J + i8HZcZ7caH6DxR335fgHVzLi5NiTce/OjNBQzQ2MJXVDd8DBmG5fyatJiOJQ4bWE + A7FlP0RdP3CO3GWE0M5iXOB2m1qWkE2eyO4UHvwTqNQLdrdAXgDQlbam9e4BG3Gg + d/6thAkWDbt/QNT+EJHDCvhDRKh1RuGHyg+Y+/nebTWWrFWsktRrbOoHCZiCpXI1 + 3eXE6nt0YkgtDxG22KqnhpAg9gUSs2hlhoxyvkzyF0mu6NhPlwAgnq7+/Q== + -----END CERTIFICATE----- +backendTLSPolicies: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + name: policy-btls + namespace: default + spec: + targetRefs: + - group: "gateway.envoyproxy.io" + kind: Backend + name: backend-fqdn + validation: + caCertificateRefs: + - name: ca-cmap + group: "" + kind: ConfigMap + hostname: foo.bar.com +securityPolicies: +- 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 + jwt: + providers: + - name: foobar + issuer: https://foo.bar.com + audiences: + - foo.bar.com + remoteJWKS: + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-fqdn + port: 443 + backendSettings: + retry: + numRetries: 3 + perRetry: + backOff: + baseInterval: 1s + maxInterval: 5s + retryOn: + triggers: ["5xx", "gateway-error", "reset"] + uri: https://foo.bar.com/jwt/public-key/jwks.json + claimToHeaders: + - header: claim-header + claim: claim diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt-backendcluster.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt-backendcluster.out.yaml new file mode 100644 index 00000000000..bf6d4380286 --- /dev/null +++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt-backendcluster.out.yaml @@ -0,0 +1,282 @@ +backendTLSPolicies: +- apiVersion: gateway.networking.k8s.io/v1alpha2 + kind: BackendTLSPolicy + metadata: + creationTimestamp: null + name: policy-btls + namespace: default + spec: + targetRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-fqdn + validation: + caCertificateRefs: + - group: "" + kind: ConfigMap + name: ca-cmap + hostname: foo.bar.com + status: + ancestors: + - ancestorRef: + group: gateway.envoyproxy.io + kind: SecurityPolicy + name: policy-for-route + namespace: default + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +backends: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: Backend + metadata: + creationTimestamp: null + name: backend-fqdn + namespace: default + spec: + endpoints: + - fqdn: + hostname: foo.bar.com + port: 443 + status: + conditions: + - lastTransitionTime: null + message: The Backend was accepted + reason: Accepted + status: "True" + type: Accepted +gateways: +- apiVersion: gateway.networking.k8s.io/v1 + kind: Gateway + metadata: + creationTimestamp: null + name: gateway-1 + namespace: default + 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 +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-1 + namespace: default + 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-1 + namespace: default + sectionName: http +infraIR: + default/gateway-1: + proxy: + listeners: + - address: null + name: default/gateway-1/http + ports: + - containerPort: 10080 + name: http-80 + protocol: HTTP + servicePort: 80 + metadata: + labels: + gateway.envoyproxy.io/owning-gateway-name: gateway-1 + gateway.envoyproxy.io/owning-gateway-namespace: default + name: default/gateway-1 +securityPolicies: +- apiVersion: gateway.envoyproxy.io/v1alpha1 + kind: SecurityPolicy + metadata: + creationTimestamp: null + name: policy-for-route + namespace: default + spec: + jwt: + providers: + - audiences: + - foo.bar.com + claimToHeaders: + - claim: claim + header: claim-header + issuer: https://foo.bar.com + name: foobar + remoteJWKS: + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: backend-fqdn + port: 443 + backendSettings: + retry: + numRetries: 3 + perRetry: + backOff: + baseInterval: 1s + maxInterval: 5s + retryOn: + triggers: + - 5xx + - gateway-error + - reset + uri: https://foo.bar.com/jwt/public-key/jwks.json + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: httproute-1 + status: + ancestors: + - ancestorRef: + group: gateway.networking.k8s.io + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + conditions: + - lastTransitionTime: null + message: Policy has been accepted. + reason: Accepted + status: "True" + type: Accepted + controllerName: gateway.envoyproxy.io/gatewayclass-controller +xdsIR: + default/gateway-1: + accessLog: + text: + - path: /dev/stdout + http: + - address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + security: + jwt: + providers: + - audiences: + - foo.bar.com + claimToHeaders: + - claim: claim + header: claim-header + issuer: https://foo.bar.com + name: foobar + remoteJWKS: + destination: + name: securitypolicy/default/policy-for-route/jwt/0 + settings: + - addressType: FQDN + endpoints: + - host: foo.bar.com + port: 443 + protocol: HTTPS + tls: + alpnProtocols: null + caCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls/default-ca + sni: foo.bar.com + weight: 1 + traffic: + retry: + numRetries: 3 + perRetry: + backOff: + baseInterval: 1s + maxInterval: 5s + retryOn: + triggers: + - 5xx + - gateway-error + - reset + uri: https://foo.bar.com/jwt/public-key/jwks.json diff --git a/internal/ir/xds.go b/internal/ir/xds.go index 821e6b608b3..e34facffe5f 100644 --- a/internal/ir/xds.go +++ b/internal/ir/xds.go @@ -27,7 +27,6 @@ import ( "sigs.k8s.io/yaml" egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1" - egv1a1validation "github.com/envoyproxy/gateway/api/v1alpha1/validation" ) const ( @@ -856,18 +855,6 @@ type SecurityFeatures struct { Authorization *Authorization `json:"authorization,omitempty" yaml:"authorization,omitempty"` } -func (s *SecurityFeatures) Validate() error { - var errs error - - if s.JWT != nil { - if err := s.JWT.Validate(); err != nil { - errs = errors.Join(errs, err) - } - } - - return errs -} - // EnvoyExtensionFeatures holds the information associated with the Envoy Extension Policy. // +k8s:deepcopy-gen=true type EnvoyExtensionFeatures struct { @@ -922,11 +909,62 @@ type CORS struct { // +k8s:deepcopy-gen=true type JWT struct { // AllowMissing determines whether a missing JWT is acceptable. - // AllowMissing bool `json:"allowMissing,omitempty" yaml:"allowMissing,omitempty"` // Providers defines a list of JSON Web Token (JWT) authentication providers. - Providers []egv1a1.JWTProvider `json:"providers,omitempty" yaml:"providers,omitempty"` + Providers []JWTProvider `json:"providers,omitempty" yaml:"providers,omitempty"` +} + +// JWTProvider defines the schema for the JWT Provider. +// +// +k8s:deepcopy-gen=true +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. + Name string `json:"name"` + + // Issuer is the principal that issued the JWT and takes the form of a URL or email address. + 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. + 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 []egv1a1.ClaimToHeader `json:"claimToHeaders,omitempty"` + + // RecomputeRoute clears the route cache and recalculates the routing decision. + // This field must be enabled if the headers generated from the claim are used for + // route matching decisions. If the recomputation selects a new route, features targeting + // the new matched route will be applied. + RecomputeRoute *bool `json:"recomputeRoute,omitempty"` + + // ExtractFrom defines different ways to extract the JWT token from HTTP request. + // If empty, it defaults to extract JWT token from the Authorization HTTP request header using Bearer schema + // or access_token from query parameters. + ExtractFrom *egv1a1.JWTExtractor `json:"extractFrom,omitempty"` +} + +// RemoteJWKSBackend holds the configuration for a remote JWKS backend. +// +// +k8s:deepcopy-gen=true +type RemoteJWKS struct { + // Destination defines the destination for the OIDC Provider. + Destination *RouteDestination `json:"destination,omitempty"` + + // Traffic contains configuration for traffic features for the OIDC Provider + Traffic *TrafficFeatures `json:"traffic,omitempty"` + + // URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to validate the server certificate. + // If a custom trust bundle is needed, it can be specified in a BackendTLSConfig resource and target the BackendRefs. + URI string `json:"uri"` } // OIDC defines the schema for authenticating HTTP requests using @@ -1332,21 +1370,6 @@ func (h *HTTPRoute) Validate() error { errs = errors.Join(errs, err) } } - if h.Security != nil { - if err := h.Security.Validate(); err != nil { - errs = errors.Join(errs, err) - } - } - - return errs -} - -func (j *JWT) Validate() error { - var errs error - - if err := egv1a1validation.ValidateJWTProvider(j.Providers); err != nil { - errs = errors.Join(errs, err) - } return errs } diff --git a/internal/ir/xds_test.go b/internal/ir/xds_test.go index 87a5c0969e2..40e3e5ba7d8 100644 --- a/internal/ir/xds_test.go +++ b/internal/ir/xds_test.go @@ -501,10 +501,10 @@ var ( }, Security: &SecurityFeatures{ JWT: &JWT{ - Providers: []egv1a1.JWTProvider{ + Providers: []JWTProvider{ { Name: "test1", - RemoteJWKS: egv1a1.RemoteJWKS{ + RemoteJWKS: RemoteJWKS{ URI: "https://test1.local", }, }, @@ -1236,48 +1236,6 @@ func TestValidateStringMatch(t *testing.T) { } } -func TestValidateJWT(t *testing.T) { - tests := []struct { - name string - input JWT - want error - }{ - { - name: "nil rules", - input: JWT{ - Providers: nil, - }, - want: nil, - }, - { - name: "provider with remote jwks uri", - input: JWT{ - Providers: []egv1a1.JWTProvider{ - { - Name: "test", - Issuer: "https://test.local", - Audiences: []string{"test1", "test2"}, - RemoteJWKS: egv1a1.RemoteJWKS{ - URI: "https://test.local", - }, - }, - }, - }, - want: nil, - }, - } - for i := range tests { - test := tests[i] - t.Run(test.name, func(t *testing.T) { - if test.want == nil { - require.NoError(t, test.input.Validate()) - } else { - require.EqualError(t, test.input.Validate(), test.want.Error()) - } - }) - } -} - func TestValidateLoadBalancer(t *testing.T) { tests := []struct { name string diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go index 9f2aa016c73..3c42375daa5 100644 --- a/internal/ir/zz_generated.deepcopy.go +++ b/internal/ir/zz_generated.deepcopy.go @@ -1982,7 +1982,7 @@ func (in *JWT) DeepCopyInto(out *JWT) { *out = *in if in.Providers != nil { in, out := &in.Providers, &out.Providers - *out = make([]v1alpha1.JWTProvider, len(*in)) + *out = make([]JWTProvider, len(*in)) for i := range *in { (*in)[i].DeepCopyInto(&(*out)[i]) } @@ -1999,6 +1999,42 @@ func (in *JWT) DeepCopy() *JWT { 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) + } + in.RemoteJWKS.DeepCopyInto(&out.RemoteJWKS) + if in.ClaimToHeaders != nil { + in, out := &in.ClaimToHeaders, &out.ClaimToHeaders + *out = make([]v1alpha1.ClaimToHeader, len(*in)) + copy(*out, *in) + } + if in.RecomputeRoute != nil { + in, out := &in.RecomputeRoute, &out.RecomputeRoute + *out = new(bool) + **out = **in + } + if in.ExtractFrom != nil { + in, out := &in.ExtractFrom, &out.ExtractFrom + *out = new(v1alpha1.JWTExtractor) + (*in).DeepCopyInto(*out) + } +} + +// 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 *LeastRequest) DeepCopyInto(out *LeastRequest) { *out = *in @@ -2626,6 +2662,31 @@ func (in *RegexMatchReplace) DeepCopy() *RegexMatchReplace { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RemoteJWKS) DeepCopyInto(out *RemoteJWKS) { + *out = *in + if in.Destination != nil { + in, out := &in.Destination, &out.Destination + *out = new(RouteDestination) + (*in).DeepCopyInto(*out) + } + if in.Traffic != nil { + in, out := &in.Traffic, &out.Traffic + *out = new(TrafficFeatures) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RemoteJWKS. +func (in *RemoteJWKS) DeepCopy() *RemoteJWKS { + if in == nil { + return nil + } + out := new(RemoteJWKS) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ResourceMetadata) DeepCopyInto(out *ResourceMetadata) { *out = *in diff --git a/internal/xds/translator/jwt.go b/internal/xds/translator/jwt.go index bc3e8d1b16e..2f93854b07c 100644 --- a/internal/xds/translator/jwt.go +++ b/internal/xds/translator/jwt.go @@ -102,11 +102,21 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, var reqs []*jwtauthnv3.JwtRequirement for i := range route.Security.JWT.Providers { - irProvider := route.Security.JWT.Providers[i] - // Create the cluster for the remote jwks, if it doesn't exist. - jwksCluster, err := url2Cluster(irProvider.RemoteJWKS.URI) - if err != nil { - return nil, err + var ( + irProvider = route.Security.JWT.Providers[i] + jwks = irProvider.RemoteJWKS + jwksCluster string + err error + ) + + if jwks.Destination != nil && len(jwks.Destination.Settings) > 0 { + jwksCluster = jwks.Destination.Name + } else { + var cluster *urlCluster + if cluster, err = url2Cluster(jwks.URI); err != nil { + return nil, err + } + jwksCluster = cluster.name } remote := &jwtauthnv3.JwtProvider_RemoteJwks{ @@ -114,7 +124,7 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, HttpUri: &corev3.HttpUri{ Uri: irProvider.RemoteJWKS.URI, HttpUpstreamType: &corev3.HttpUri_Cluster{ - Cluster: jwksCluster.name, + Cluster: jwksCluster, }, Timeout: &durationpb.Duration{Seconds: defaultExtServiceRequestTimeout}, }, @@ -123,6 +133,15 @@ func buildJWTAuthn(irListener *ir.HTTPListener) (*jwtauthnv3.JwtAuthentication, }, } + // Set the retry policy if it exists. + if jwks.Traffic != nil && jwks.Traffic.Retry != nil { + var rp *corev3.RetryPolicy + if rp, err = buildNonRouteRetryPolicy(jwks.Traffic.Retry); err != nil { + return nil, err + } + remote.RemoteJwks.RetryPolicy = rp + } + claimToHeaders := []*jwtauthnv3.JwtClaimToHeader{} for _, claimToHeader := range irProvider.ClaimToHeaders { claimToHeader := &jwtauthnv3.JwtClaimToHeader{ @@ -264,17 +283,26 @@ func (*jwt) patchResources(tCtx *types.ResourceVersionTable, routes []*ir.HTTPRo return errors.New("xds resource table is nil") } - var err, errs error + var errs error for _, route := range routes { if !routeContainsJWTAuthn(route) { continue } for i := range route.Security.JWT.Providers { - provider := route.Security.JWT.Providers[i] + jwks := route.Security.JWT.Providers[i].RemoteJWKS - if err = addClusterFromURL(provider.RemoteJWKS.URI, tCtx); err != nil { - errs = errors.Join(errs, err) + // If the rmote JWKS has a destination, use it. + if jwks.Destination != nil && len(jwks.Destination.Settings) > 0 { + if err := createExtServiceXDSCluster( + jwks.Destination, jwks.Traffic, tCtx); err != nil { + errs = errors.Join(errs, err) + } + } else { + // Create a cluster with the token endpoint url. + if err := addClusterFromURL(jwks.URI, tCtx); err != nil { + errs = errors.Join(errs, err) + } } } } diff --git a/internal/xds/translator/testdata/in/xds-ir/jwt-with-backend-tls-retry.yaml b/internal/xds/translator/testdata/in/xds-ir/jwt-with-backend-tls-retry.yaml new file mode 100644 index 00000000000..37a8c81468f --- /dev/null +++ b/internal/xds/translator/testdata/in/xds-ir/jwt-with-backend-tls-retry.yaml @@ -0,0 +1,75 @@ +http: +- address: 0.0.0.0 + hostnames: + - '*' + isHTTP2: false + metadata: + kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http + path: + escapedSlashesAction: UnescapeAndRedirect + mergeSlashes: true + port: 10080 + routes: + - destination: + name: httproute/default/httproute-1/rule/0 + settings: + - addressType: IP + endpoints: + - host: 7.7.7.7 + port: 8080 + protocol: HTTP + weight: 1 + hostname: gateway.envoyproxy.io + isHTTP2: false + metadata: + kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + pathMatch: + distinct: false + name: "" + prefix: / + security: + jwt: + providers: + - audiences: + - foo.bar.com + claimToHeaders: + - claim: claim + header: claim-header + issuer: https://foo.bar.com + name: foobar + remoteJWKS: + destination: + name: securitypolicy/default/policy-for-route/jwt/0 + settings: + - addressType: FQDN + endpoints: + - host: foo.bar.com + port: 443 + protocol: HTTPS + tls: + alpnProtocols: null + caCertificate: + certificate: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + name: policy-btls/default-ca + sni: foo.bar.com + weight: 1 + traffic: + retry: + numRetries: 3 + perRetry: + backOff: + baseInterval: 1s + maxInterval: 5s + retryOn: + triggers: + - 5xx + - gateway-error + - reset + uri: https://foo.bar.com/jwt/public-key/jwks.json diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml new file mode 100644 index 00000000000..edc7120e86c --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.clusters.yaml @@ -0,0 +1,68 @@ +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + edsClusterConfig: + edsConfig: + ads: {} + resourceApiVersion: V3 + serviceName: httproute/default/httproute-1/rule/0 + ignoreHealthOnHostRemoval: true + lbPolicy: LEAST_REQUEST + name: httproute/default/httproute-1/rule/0 + perConnectionBufferLimitBytes: 32768 + type: EDS +- circuitBreakers: + thresholds: + - maxRetries: 1024 + commonLbConfig: + localityWeightedLbConfig: {} + connectTimeout: 10s + dnsLookupFamily: V4_PREFERRED + dnsRefreshRate: 30s + lbPolicy: LEAST_REQUEST + loadAssignment: + clusterName: securitypolicy/default/policy-for-route/jwt/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: foo.bar.com + portValue: 443 + loadBalancingWeight: 1 + metadata: + filterMetadata: + envoy.transport_socket_match: + name: securitypolicy/default/policy-for-route/jwt/0/tls/0 + loadBalancingWeight: 1 + locality: + region: securitypolicy/default/policy-for-route/jwt/0/backend/0 + name: securitypolicy/default/policy-for-route/jwt/0 + perConnectionBufferLimitBytes: 32768 + respectDnsTtl: true + transportSocketMatches: + - match: + name: securitypolicy/default/policy-for-route/jwt/0/tls/0 + name: securitypolicy/default/policy-for-route/jwt/0/tls/0 + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext + commonTlsContext: + combinedValidationContext: + defaultValidationContext: + matchTypedSubjectAltNames: + - matcher: + exact: foo.bar.com + sanType: DNS + validationContextSdsSecretConfig: + name: policy-btls/default-ca + sdsConfig: + ads: {} + resourceApiVersion: V3 + sni: foo.bar.com + type: STRICT_DNS diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.endpoints.yaml new file mode 100644 index 00000000000..29bb6b4e444 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.endpoints.yaml @@ -0,0 +1,12 @@ +- clusterName: httproute/default/httproute-1/rule/0 + endpoints: + - lbEndpoints: + - endpoint: + address: + socketAddress: + address: 7.7.7.7 + portValue: 8080 + loadBalancingWeight: 1 + loadBalancingWeight: 1 + locality: + region: httproute/default/httproute-1/rule/0/backend/0 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.listeners.yaml new file mode 100644 index 00000000000..4c1b6b0fc23 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.listeners.yaml @@ -0,0 +1,66 @@ +- 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: + httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io/foobar: + audiences: + - foo.bar.com + claimToHeaders: + - claimName: claim + headerName: claim-header + forward: true + issuer: https://foo.bar.com + normalizePayloadInMetadata: + spaceDelimitedClaims: + - scope + payloadInMetadata: foobar + remoteJwks: + asyncFetch: {} + cacheDuration: 300s + httpUri: + cluster: securitypolicy/default/policy-for-route/jwt/0 + timeout: 10s + uri: https://foo.bar.com/jwt/public-key/jwks.json + retryPolicy: + numRetries: 3 + retryBackOff: + baseInterval: 1s + maxInterval: 5s + retryOn: 5xx,gateway-error,reset + requirementMap: + httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io: + providerName: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io/foobar + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + suppressEnvoyHeaders: true + mergeSlashes: true + normalizePath: true + pathWithEscapedSlashesAction: UNESCAPE_AND_REDIRECT + rds: + configSource: + ads: {} + resourceApiVersion: V3 + routeConfigName: default/gateway-1/http + serverHeaderTransformation: PASS_THROUGH + statPrefix: http-10080 + useRemoteAddress: true + name: default/gateway-1/http + name: default/gateway-1/http + perConnectionBufferLimitBytes: 32768 diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.routes.yaml new file mode 100644 index 00000000000..0eae8cd072d --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.routes.yaml @@ -0,0 +1,33 @@ +- ignorePortInHostMatching: true + name: default/gateway-1/http + virtualHosts: + - domains: + - gateway.envoyproxy.io + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: Gateway + name: gateway-1 + namespace: default + sectionName: http + name: default/gateway-1/http/gateway_envoyproxy_io + routes: + - match: + prefix: / + metadata: + filterMetadata: + envoy-gateway: + resources: + - kind: HTTPRoute + name: httproute-1 + namespace: default + name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io + route: + cluster: httproute/default/httproute-1/rule/0 + upgradeConfigs: + - upgradeType: websocket + typedPerFilterConfig: + envoy.filters.http.jwt_authn: + '@type': type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.PerRouteConfig + requirementName: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.secrets.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.secrets.yaml new file mode 100644 index 00000000000..da8f89db5d7 --- /dev/null +++ b/internal/xds/translator/testdata/out/xds-ir/jwt-with-backend-tls-retry.secrets.yaml @@ -0,0 +1,4 @@ +- name: policy-btls/default-ca + validationContext: + trustedCa: + inlineBytes: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURKekNDQWcrZ0F3SUJBZ0lVQWw2VUtJdUttenRlODFjbGx6NVBmZE4ySWxJd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHQTFVRUNnd0dhM1ZpWldSaU1CNFhEVEl6TVRBdwpNakExTkRFMU4xb1hEVEkwTVRBd01UQTFOREUxTjFvd0l6RVFNQTRHQTFVRUF3d0hiWGxqYVdWdWRERVBNQTBHCkExVUVDZ3dHYTNWaVpXUmlNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQXdTVGMKMXlqOEhXNjJueW5rRmJYbzRWWEt2MmpDMFBNN2RQVmt5ODdGd2VaY1RLTG9XUVZQUUUycDJrTERLNk9Fc3ptTQp5eXIreHhXdHlpdmVyZW1yV3FuS2tOVFloTGZZUGhnUWtjemliN2VVYWxtRmpVYmhXZEx2SGFrYkVnQ29kbjNiCmt6NTdtSW5YMlZwaURPS2c0a3lIZml1WFdwaUJxckN4MEtOTHB4bzNERVFjRmNzUVRlVEh6aDQ3NTJHVjA0UlUKVGkvR0VXeXpJc2w0Umc3dEd0QXdtY0lQZ1VOVWZZMlEzOTBGR3FkSDRhaG4rbXcvNmFGYlczMVc2M2Q5WUpWcQppb3lPVmNhTUlwTTVCL2M3UWM4U3VoQ0kxWUdoVXlnNGNSSExFdzVWdGlraW95RTNYMDRrbmEzalFBajU0WWJSCmJwRWhjMzVhcEtMQjIxSE9VUUlEQVFBQm8xTXdVVEFkQmdOVkhRNEVGZ1FVeXZsMFZJNXZKVlN1WUZYdTdCNDgKNlBiTUVBb3dId1lEVlIwakJCZ3dGb0FVeXZsMFZJNXZKVlN1WUZYdTdCNDg2UGJNRUFvd0R3WURWUjBUQVFILwpCQVV3QXdFQi96QU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFNTHhyZ0ZWTXVOUnEyd0F3Y0J0N1NuTlI1Q2Z6CjJNdlhxNUVVbXVhd0lVaTlrYVlqd2RWaURSRUdTams3SlcxN3ZsNTc2SGpEa2RmUndpNEUyOFN5ZFJJblpmNkoKaThIWmNaN2NhSDZEeFIzMzVmZ0hWekxpNU5pVGNlL09qTkJRelEyTUpYVkRkOERCbUc1ZnlhdEppT0pRNGJXRQpBN0ZsUDBSZFAzQ08zR1dFME01aVhPQjJtMXFXa0UyZXlPNFVIdndUcU5RTGRyZEFYZ0RRbGJhbTllNEJHM0dnCmQvNnRoQWtXRGJ0L1FOVCtFSkhEQ3ZoRFJLaDFSdUdIeWcrWSsvbmViVFdXckZXc2t0UnJiT29IQ1ppQ3BYSTEKM2VYRTZudDBZa2d0RHhHMjJLcW5ocEFnOWdVU3MyaGxob3h5dmt6eUYwbXU2TmhQbHdBZ25xNysvUT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md index 19d79a73962..ed4050fb347 100644 --- a/site/content/en/latest/api/extension_types.md +++ b/site/content/en/latest/api/extension_types.md @@ -298,6 +298,7 @@ _Appears in:_ - [OIDCProvider](#oidcprovider) - [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) - [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [RemoteJWKS](#remotejwks) - [TracingProvider](#tracingprovider) | Field | Type | Required | Default | Description | @@ -359,6 +360,7 @@ _Appears in:_ - [OIDCProvider](#oidcprovider) - [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) - [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [RemoteJWKS](#remotejwks) - [TracingProvider](#tracingprovider) | Field | Type | Required | Default | Description | @@ -739,6 +741,7 @@ _Appears in:_ - [OIDCProvider](#oidcprovider) - [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) - [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [RemoteJWKS](#remotejwks) - [TracingProvider](#tracingprovider) | Field | Type | Required | Default | Description | @@ -3756,15 +3759,17 @@ _Appears in:_ -RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote -HTTP/HTTPS endpoint. +RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. _Appears in:_ - [JWTProvider](#jwtprovider) | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `uri` | _string_ | true | | URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to
validate the server certificate. | +| `backendRef` | _[BackendObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.BackendObjectReference)_ | false | | BackendRef references a Kubernetes object that represents the
backend server to which the authorization request will be sent.

Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | | BackendRefs references a Kubernetes object that represents the
backend server to which the authorization request will be sent. | +| `backendSettings` | _[ClusterSettings](#clustersettings)_ | false | | BackendSettings holds configuration for managing the connection
to the backend. | +| `uri` | _string_ | true | | URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to validate the server certificate.
If a custom trust bundle is needed, it can be specified in a BackendTLSConfig resource and target the BackendRefs. | #### ReplaceRegexMatch diff --git a/site/content/zh/latest/api/extension_types.md b/site/content/zh/latest/api/extension_types.md index 19d79a73962..ed4050fb347 100644 --- a/site/content/zh/latest/api/extension_types.md +++ b/site/content/zh/latest/api/extension_types.md @@ -298,6 +298,7 @@ _Appears in:_ - [OIDCProvider](#oidcprovider) - [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) - [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [RemoteJWKS](#remotejwks) - [TracingProvider](#tracingprovider) | Field | Type | Required | Default | Description | @@ -359,6 +360,7 @@ _Appears in:_ - [OIDCProvider](#oidcprovider) - [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) - [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [RemoteJWKS](#remotejwks) - [TracingProvider](#tracingprovider) | Field | Type | Required | Default | Description | @@ -739,6 +741,7 @@ _Appears in:_ - [OIDCProvider](#oidcprovider) - [OpenTelemetryEnvoyProxyAccessLog](#opentelemetryenvoyproxyaccesslog) - [ProxyOpenTelemetrySink](#proxyopentelemetrysink) +- [RemoteJWKS](#remotejwks) - [TracingProvider](#tracingprovider) | Field | Type | Required | Default | Description | @@ -3756,15 +3759,17 @@ _Appears in:_ -RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote -HTTP/HTTPS endpoint. +RemoteJWKS defines how to fetch and cache JSON Web Key Sets (JWKS) from a remote HTTP/HTTPS endpoint. _Appears in:_ - [JWTProvider](#jwtprovider) | Field | Type | Required | Default | Description | | --- | --- | --- | --- | --- | -| `uri` | _string_ | true | | URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to
validate the server certificate. | +| `backendRef` | _[BackendObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.BackendObjectReference)_ | false | | BackendRef references a Kubernetes object that represents the
backend server to which the authorization request will be sent.

Deprecated: Use BackendRefs instead. | +| `backendRefs` | _[BackendRef](#backendref) array_ | false | | BackendRefs references a Kubernetes object that represents the
backend server to which the authorization request will be sent. | +| `backendSettings` | _[ClusterSettings](#clustersettings)_ | false | | BackendSettings holds configuration for managing the connection
to the backend. | +| `uri` | _string_ | true | | URI is the HTTPS URI to fetch the JWKS. Envoy's system trust bundle is used to validate the server certificate.
If a custom trust bundle is needed, it can be specified in a BackendTLSConfig resource and target the BackendRefs. | #### ReplaceRegexMatch diff --git a/test/e2e/testdata/jwt-backend-remote-jwks.yaml b/test/e2e/testdata/jwt-backend-remote-jwks.yaml new file mode 100644 index 00000000000..17c335df424 --- /dev/null +++ b/test/e2e/testdata/jwt-backend-remote-jwks.yaml @@ -0,0 +1,192 @@ +--- +apiVersion: v1 +kind: Secret +metadata: + name: remote-jwks-server-secret + namespace: gateway-conformance-infra +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURuVENDQW9XZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREF0TVJVd0V3WURWUVFLREF4bGVHRnQKY0d4bElFbHVZeTR4RkRBU0JnTlZCQU1NQzJWNFlXMXdiR1V1WTI5dE1CNFhEVEkxTURFd056RXpNVEF5TTFvWApEVE0xTURFd05URXpNVEF5TTFvd1ZqRTFNRE1HQTFVRUF3d3NjbVZ0YjNSbExXcDNhM010YzJWeWRtVnlMbWRoCmRHVjNZWGt0WTI5dVptOXliV0Z1WTJVdGFXNW1jbUV4SFRBYkJnTlZCQW9NRkdWNFlXMXdiR1VnYjNKbllXNXAKZW1GMGFXOXVNSUlCSWpBTkJna3Foa2lHOXcwQkFRRUZBQU9DQVE4QU1JSUJDZ0tDQVFFQTJFRGFxMXpWaVE1dwpJMGNTVjNJS1lMelRCQVQrb3RHcVVISmhMc1pNU0xxc0pydlVDSjB2a3h0TWh4a09QNGlhdVJBRmhlaEE5a3p4Cm9zOXFUMjM2Si9DZGVxWUYwUGJsOVBZeXVDb2Z3RWdGMHhlZHNUbUVoMWljeEpWTjFLa2ZKTU1jZFhHYzJKdU0KbGdKUmVmbzkvaFRmdWFaek5xZnZ3ckFuT3ZWV3BlK29lUGwzOWhnbEc0V1MvYVhiTmdkTEhsUitRZUVLS3NIdgpNemNrMHQ0SE8ybVBPTHZ3R0l6YU1icll5dmh6Y25ETmFmVnczRHBEdW8yeFRaMVZuenh6bmFsTDgwWmsyRGYwCmlMNTgwcCtkTlBjbHB5L3Vtc2ttR00wdGF6eFd3Q0ovK1cxb0xKSFJQdmoxdUk1YnVKMDZGODZBb0ZDRUpURGIKdFJRNk5sOTZNUUlEQVFBQm80R2VNSUdiTUFzR0ExVWREd1FFQXdJRm9EQVRCZ05WSFNVRUREQUtCZ2dyQmdFRgpCUWNEQVRBM0JnTlZIUkVFTURBdWdpeHlaVzF2ZEdVdGFuZHJjeTF6WlhKMlpYSXVaMkYwWlhkaGVTMWpiMjVtCmIzSnRZVzVqWlMxcGJtWnlZVEFkQmdOVkhRNEVGZ1FVQVA1eWdpQk9LUWI2cVV0MmpiWlZ4ZGp4ZWhJd0h3WUQKVlIwakJCZ3dGb0FVTTlmSW9ydkQ5NEdNRHNMTGtSdUdNZzFVTHNjd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQgpBRGlXSDVIZUhiRWZScjY2bHFqaUwwR0RFeWN3YmJBUmc5TTEvdlV6SVVlMk15U1ZjOW9lN3RKMVBKWER5REFpCnBDc1lOQ25MN0EzT0Jha2Q3dWFhRTlGOURRWVd1aHcwNW1PREVxWjVJSU90c2RWc2VjRHdycDdjdENCVW5ja1IKN0tiQUYyU0hhYkNBaEEweVhFTTFaWUJUbG1PVlBMaWxjSytKOTdmY3FGSVM4OUV3ZUw2RGdQNDNyU0lqdUxGcgpHSGJMaS80ZUJSSlFqdExYVC82RVcvamZRMWM0K0ZkNWVZRXZqK2FzU3JnbTJyRmZBWk5sWEtGYnhtUjJMQ1RiCmluclhJUzhQeWxvM3U5L2JvVDlxOUpkK2tQZ0tqWGx5eFNkRjJDakZiNWcwT2hqWE5qWGZ6NnVMUUUwNnlZV1EKY0lIVWZ0ZWhOWk9na2thY2NYVGVPRUU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRRFlRTnFyWE5XSkRuQWoKUnhKWGNncGd2Tk1FQlA2aTBhcFFjbUV1eGt4SXVxd211OVFJblMrVEcweUhHUTQvaUpxNUVBV0Y2RUQyVFBHaQp6MnBQYmZvbjhKMTZwZ1hROXVYMDlqSzRLaC9BU0FYVEY1MnhPWVNIV0p6RWxVM1VxUjhrd3h4MWNaelltNHlXCkFsRjUrajMrRk4rNXBuTTJwKy9Dc0NjNjlWYWw3Nmg0K1hmMkdDVWJoWkw5cGRzMkIwc2VWSDVCNFFvcXdlOHoKTnlUUzNnYzdhWTg0dS9BWWpOb3h1dGpLK0hOeWNNMXA5WERjT2tPNmpiRk5uVldmUEhPZHFVdnpSbVRZTi9TSQp2bnpTbjUwMDl5V25MKzZheVNZWXpTMXJQRmJBSW4vNWJXZ3NrZEUrK1BXNGpsdTRuVG9Yem9DZ1VJUWxNTnUxCkZEbzJYM294QWdNQkFBRUNnZ0VBS0NRWnN2ZGZkN3BqWEZrRDhaRnNsYnBYSFFia1VVckQ1M3pqeHkvdDF3NDMKaUZVVExhb251NUcwcWRzZnh2RlBid3luU2N6cnlneE1TaUZnSlhCUG4veE03d2hFU2g2YVh0Y1lZUkVJcGNONAp1VTlINlM2NUIvcU4xdnV6Mzhhb3prVWRVanVObHJPQTdCTndGa2s2R3FDN1NwVzRDeXd2R0I5a21OQVRqbWRQCnJTRW9YbldPNVhDemNFVWlhaDl0R1ZIVlgwYzFMWFJxZzB6TG9NUER0TjJNTWdsQ294MDRvZkxJeWxLZVhYODkKcC83VDNMMTlBWDg0RktsSEU2MFFUTllWMU9QaEh1Q3NyTFloVTRib0JtT0huSmJ4NkFCQkRjRG9XN0VpaU56VQp0RmtzbTlUOEdCblRrUTFEbVJid0dJVXBhbUtPTkVGM2hRYzBXQjUydFFLQmdRRHU4cGVvR0pVTEkwVGVTOW9ICmVSSTRIV3hUK0ZXMkV1bEdPRlZleGV4YUdEVS8rcVE3RTBTZXNoTkx5VklOQ1FLYlorNno0d2ZZMW8zam8yZEMKZFRGK2JBZnVzRTZRTERnb29DaGNLYXdBTEFVYTh5aEc5NzRWdStZTC81UGRVQlhkL1ZEYmRXOFFESUN0ZEZjOQpyY1dSakZLbVExdzRsbEFSc2hDWmk0L1lKd0tCZ1FEbnI2Y2V6eTdGVDFIMjVrU1FJN0tIVEVSTlhZQzJkYllGCnVnOWJQWmJuV2xyeDY5ZlljMmlFNnVMR0tONE1BU1VRZFhhaHl5LzdWamloaWNaaEFjTDg1ZWZ1aDFpWE9vTnEKUEQySUdTeWNKUmMwbVdzcFQ1OGJibzgvb0lmL3JXS0Z1QTdQRVNEY3ZlNFNpek5BckYxZUw5VDBYS2tIakVhSwpuZXk1VXU5NTV3S0JnUUNpeG9ZRGtBTndXKzFkVmVUSU5IVHgzekZkbm8yZEJCTC9yLzZRR2xxaElWNmRIL3hpCjlnUkg2MTF6d2todjh0UmcwNU5yM2R3Sm5sZDRYR2RLZ1pWZTN1OGtiZHlISUdoOVhHVkNLMjB0ak05SmhaM0oKZ3BsdUt0dFREeDlHbzNqU0NlL2NJSXF4THlNMWhreXNDc1hOR2Y5dm5mR2o1dG5TeEMvRXVhc2Evd0tCZ1FEVQpzUWgyM0RSUHBwWFVWMmd4K3ROMkthbTZiRkF4TUxhOVl5V2QyVmlqWXV1Q2s4US9UUk55a2o5Rk0xZEZKZmZrCnVERUVMd2dKY0FubElob2dEQUg1TVFaT2o2bmdpek1CWC9RTThTOW0yUllJajU4MCtZZFRJNWdXRFVWTWp0dVgKYm5VSjJ1dVVPamhJaGNtellZa0ZZbHZaU1FkVGlvOW55YnI4RndzSm1RS0JnR3U3dkdUWnZicVJjR1o0Lzh2UgpZRHRtVVVUZnpXaVB5c2dOQXl4dEpCb1k3RitNbTlJd2FBbGhsdEloRy9kVEFpclpjV3ZXR1cwY291SnBocFpCCkdOZ05PQzdPajJoMFNEdW5TMXZRVm1IZ0hrNTVUVlF6bWxTd2twUjBCbnVvYU5lelNhYTlGdzZSTEZ5ZjBLdUMKcEE0VzJWOVh6RjU1T3QvOG1sQXE3YUJBCi0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: remote-jwks-server-ca + namespace: gateway-conformance-infra +data: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIDOzCCAiOgAwIBAgIUYozfdHNlpdxcE7TCuF+wDOrxi9kwDQYJKoZIhvcNAQEL + BQAwLTEVMBMGA1UECgwMZXhhbXBsZSBJbmMuMRQwEgYDVQQDDAtleGFtcGxlLmNv + bTAeFw0yNTAxMDcxMzEwMjJaFw0zNTAxMDUxMzEwMjJaMC0xFTATBgNVBAoMDGV4 + YW1wbGUgSW5jLjEUMBIGA1UEAwwLZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEB + AQUAA4IBDwAwggEKAoIBAQDm+2qqe20PGAVzGU4cuOp5K74tdtRiEVj8Jps9tVZx + I9UIYbVHvJgDnX2yNyHgPs8s0hQ2q8Q2HAbqeUCRhmcuWhHkag+3rpKWjdGZWLHy + 9lAYv2RSybeTwQAGVDwSz8SrKog6aE6XvvJvUEpfStsJep2blACX2MOpERXiHs6w + avIu1FTF6a31GyICqNYG07o533X5gfJDRYV3N6ari08Nd+iAaP8HVepc8wziBgFj + 3pdvCvkB1FPHKIbClQdIFgViDwLQYiagaR7esFYcPg6gdwvDJOoAh3GV66HNo7ev + slY6KHQlRdlorjwxPt5dkGrkN7hiXRbItKqhQCy5GlmLAgMBAAGjUzBRMB0GA1Ud + DgQWBBQz18iiu8P3gYwOwsuRG4YyDVQuxzAfBgNVHSMEGDAWgBQz18iiu8P3gYwO + wsuRG4YyDVQuxzAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQAg + C4yDPACAHUjE09m3jmuJUtSSEr+FdXRZ9fkpTezYed6ebefz+4qbTb8HohsEC0K8 + 52hg81Knh6n26FN/5S73/6k4LGcX4sN3WslKnRGdVuoXR3o1UGB4Rb0wtdNwOjZF + 5eI7Yxfg8nbsNS+6L+t/xgUj09wR34+fdv43XxxNjFPkWIagOItfG4jyyy+2ap/j + kyzLypQx9SXeh6ELL4+I5AbNYwaLdoXUyPJHZfyqAABOZ+PVTUabBPiEJsCNmcbo + toXi8O3UIo+oldyE771XJCGwMLLq75SJ79UZgy0yj3AGLVpirpgqEJT3Nd3VSWGF + vjYDV/+uv3wEkMby25WT + -----END CERTIFICATE----- +--- +apiVersion: v1 +kind: Service +metadata: + name: remote-jwks-server + namespace: gateway-conformance-infra +spec: + selector: + app: remote-jwks-server + ports: + - protocol: TCP + port: 443 + targetPort: 8443 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: remote-jwks-server + namespace: gateway-conformance-infra + labels: + app: remote-jwks-server +spec: + replicas: 1 + selector: + matchLabels: + app: remote-jwks-server + template: + metadata: + labels: + app: remote-jwks-server + spec: + containers: + - name: remote-jwks-server + image: envoyproxy/gateway-static-file-server + imagePullPolicy: IfNotPresent + args: + - --port=8443 + - --certPath=/etc/certs + volumeMounts: + - name: remote-jwks-server-secret + mountPath: /etc/certs + volumes: + - name: remote-jwks-server-secret + secret: + secretName: remote-jwks-server-secret + items: + - key: tls.crt + path: tls.crt + - key: tls.key + path: tls.key +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: Backend +metadata: + name: remote-jwks + namespace: gateway-conformance-infra +spec: + endpoints: + - fqdn: + hostname: 'remote-jwks-server.gateway-conformance-infra' + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1alpha3 +kind: BackendTLSPolicy +metadata: + name: remote-jwks-btls + namespace: gateway-conformance-infra +spec: + targetRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: remote-jwks + validation: + caCertificateRefs: + - name: remote-jwks-server-ca + group: '' + kind: ConfigMap + hostname: remote-jwks-server.gateway-conformance-infra +--- +apiVersion: gateway.envoyproxy.io/v1alpha1 +kind: SecurityPolicy +metadata: + name: jwt-with-backend-remot-jwks + namespace: gateway-conformance-infra +spec: + targetRef: + group: gateway.networking.k8s.io + kind: HTTPRoute + name: jwt-claim-routing + jwt: + providers: + - name: example + recomputeRoute: true + claimToHeaders: + - claim: sub + header: x-sub + - claim: admin + header: x-admin + - claim: name + header: x-name + remoteJWKS: + backendRefs: + - group: gateway.envoyproxy.io + kind: Backend + name: remote-jwks + port: 443 + backendSettings: + retry: + numRetries: 3 + perRetry: + backOff: + baseInterval: 1s + maxInterval: 5s + retryOn: + triggers: ["5xx", "gateway-error", "reset"] + uri: https://remote-jwks-server.gateway-conformance-infra/jwt/jwks.json +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: jwt-claim-routing + namespace: gateway-conformance-infra +spec: + parentRefs: + - name: same-namespace + rules: + - backendRefs: + - kind: Service + name: infra-backend-v1 + port: 8080 + weight: 1 + matches: + - headers: + - name: x-name + value: John Doe + - backendRefs: + - kind: Service + name: infra-backend-v2 + port: 8080 + weight: 1 + matches: + - headers: + - name: x-name + value: Tom + # catch all + - backendRefs: + - kind: Service + name: infra-backend-invalid + port: 8080 + weight: 1 + matches: + - path: + type: PathPrefix + value: / diff --git a/test/e2e/tests/jwt-backend-remote-jwks.go b/test/e2e/tests/jwt-backend-remote-jwks.go new file mode 100644 index 00000000000..601ae78ccc0 --- /dev/null +++ b/test/e2e/tests/jwt-backend-remote-jwks.go @@ -0,0 +1,29 @@ +// 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. + +//go:build e2e + +package tests + +import ( + "testing" + + "sigs.k8s.io/gateway-api/conformance/utils/suite" +) + +func init() { + ConformanceTests = append(ConformanceTests, JWTBackendRemoteJWKSTest) +} + +var JWTBackendRemoteJWKSTest = suite.ConformanceTest{ + ShortName: "JWTBackendRemoteJWKS", + Description: "JWT with Backend as remote JWKS", + Manifests: []string{"testdata/jwt-backend-remote-jwks.yaml"}, + Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { + t.Run("jwt claim base routing", func(t *testing.T) { + testClaimBasedRouting(t, suite) + }) + }, +} diff --git a/test/e2e/tests/jwt.go b/test/e2e/tests/jwt.go index 2ab756fcf3c..38af783d1ff 100644 --- a/test/e2e/tests/jwt.go +++ b/test/e2e/tests/jwt.go @@ -39,75 +39,79 @@ var JWTTest = suite.ConformanceTest{ Manifests: []string{"testdata/jwt.yaml"}, Test: func(t *testing.T, suite *suite.ConformanceTestSuite) { t.Run("jwt claim base routing", func(t *testing.T) { - ns := "gateway-conformance-infra" - routeNN := types.NamespacedName{Name: "jwt-claim-routing", Namespace: ns} - gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} - gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + testClaimBasedRouting(t, suite) + }) + }, +} - testCases := []http.ExpectedResponse{ - { - Request: http.Request{ - Path: "/get", - Headers: map[string]string{ - "Authorization": "Bearer " + v1Token, - }, - }, - Backend: "infra-backend-v1", - Response: http.Response{ - StatusCode: 200, - }, - Namespace: ns, +func testClaimBasedRouting(t *testing.T, suite *suite.ConformanceTestSuite) { + ns := "gateway-conformance-infra" + routeNN := types.NamespacedName{Name: "jwt-claim-routing", Namespace: ns} + gwNN := types.NamespacedName{Name: "same-namespace", Namespace: ns} + gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN) + + testCases := []http.ExpectedResponse{ + { + Request: http.Request{ + Path: "/get", + Headers: map[string]string{ + "Authorization": "Bearer " + v1Token, }, - { - Request: http.Request{ - Path: "/get", - Headers: map[string]string{ - "Authorization": "Bearer " + v2Token, - }, - }, - Backend: "infra-backend-v2", - Response: http.Response{ - StatusCode: 200, - }, - Namespace: ns, + }, + Backend: "infra-backend-v1", + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + }, + { + Request: http.Request{ + Path: "/get", + Headers: map[string]string{ + "Authorization": "Bearer " + v2Token, }, - { - Request: http.Request{ - Path: "/get", - Headers: map[string]string{ - "Authorization": "Bearer " + anotherToken, - }, - }, - Backend: "infra-backend-v1", - Response: http.Response{ - StatusCode: 500, - }, - Namespace: ns, + }, + Backend: "infra-backend-v2", + Response: http.Response{ + StatusCode: 200, + }, + Namespace: ns, + }, + { + Request: http.Request{ + Path: "/get", + Headers: map[string]string{ + "Authorization": "Bearer " + anotherToken, }, - { - Request: http.Request{ - Path: "/get", - Headers: map[string]string{ - "x-name": "Tom", - }, - }, - Backend: "infra-backend-v2", - Response: http.Response{ - StatusCode: 401, - }, - Namespace: ns, + }, + Backend: "infra-backend-v1", + Response: http.Response{ + StatusCode: 500, + }, + Namespace: ns, + }, + { + Request: http.Request{ + Path: "/get", + Headers: map[string]string{ + "x-name": "Tom", }, - } + }, + Backend: "infra-backend-v2", + Response: http.Response{ + StatusCode: 401, + }, + Namespace: ns, + }, + } - for i := range testCases { - tc := testCases[i] - t.Run(tc.GetTestCaseName(i), func(t *testing.T) { - t.Parallel() - http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, tc) - }) - } + for i := range testCases { + tc := testCases[i] + t.Run(tc.GetTestCaseName(i), func(t *testing.T) { + t.Parallel() + http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, tc) }) - }, + } } var OptionalJWTTest = suite.ConformanceTest{