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{