Repository: envoyproxy/gateway
Pull request #6197 (2025-05-27 04:08:02)

feat: improve egctl collect
1. support collect other namespace for GNM(gateway namesapce mode)
2. mute klog
3. correct prometheus scrape logic


Patch:
diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go
index 9eb39c97b..61476ee6b 100644
--- a/api/v1alpha1/oidc_types.go
+++ b/api/v1alpha1/oidc_types.go
@@ -65,7 +65,6 @@ type OIDC struct {
 	// Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider.
 	// This behavior can be useful for AJAX or machine requests.
 	// +optional
-	// +notImplementedHide
 	DenyRedirect *OIDCDenyRedirect `json:"denyRedirect,omitempty"`
 
 	// The path to log a user out, clearing their credential cookies.
@@ -160,14 +159,14 @@ type OIDCProvider struct {
 }
 
 // OIDCDenyRedirect defines headers to match against the request to deny redirect to the OIDC Provider.
-// +notImplementedHide
 type OIDCDenyRedirect struct {
 	// Defines the headers to match against the request to deny redirect to the OIDC Provider.
+	// +kubebuilder:validation:MinItems=1
+	// +kubebuilder:validation:MaxItems=16
 	Headers []OIDCDenyRedirectHeader `json:"headers"`
 }
 
 // OIDCDenyRedirectHeader defines how a header is matched
-// +notImplementedHide
 type OIDCDenyRedirectHeader struct {
 	// Specifies the name of the header in the request.
 	// +kubebuilder:validation:MinLength=1
diff --git a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
index 6b63d560a..27534cabe 100644
--- a/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-crds-helm/templates/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -3674,6 +3674,8 @@ spec:
                           - name
                           - value
                           type: object
+                        maxItems: 16
+                        minItems: 1
                         type: array
                     required:
                     - headers
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 104b417cd..535eeda93 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -3673,6 +3673,8 @@ spec:
                           - name
                           - value
                           type: object
+                        maxItems: 16
+                        minItems: 1
                         type: array
                     required:
                     - headers
diff --git a/internal/cmd/egctl/collect.go b/internal/cmd/egctl/collect.go
index e95db983e..d412ed6ae 100644
--- a/internal/cmd/egctl/collect.go
+++ b/internal/cmd/egctl/collect.go
@@ -16,6 +16,7 @@ import (
 	"time"
 
 	"github.com/spf13/cobra"
+	"k8s.io/apimachinery/pkg/util/sets"
 	cmdutil "k8s.io/kubectl/pkg/cmd/util"
 
 	"github.com/envoyproxy/gateway/internal/cmd/options"
@@ -26,6 +27,7 @@ import (
 type collectOptions struct {
 	outPath               string
 	envoyGatewayNamespace string
+	proxyNamespaces       []string
 }
 
 func newCollectCommand() *cobra.Command {
@@ -48,6 +50,8 @@ func newCollectCommand() *cobra.Command {
 		"Specify the output file path for collected data. If not specified, a timestamped file will be created in the current directory.")
 	collectCommand.PersistentFlags().StringVarP(&collectOpts.envoyGatewayNamespace, "envoy-system-namespace", "", "envoy-gateway-system",
 		"Specify the namespace where the Envoy Gateway controller is installed.")
+	collectCommand.PersistentFlags().StringArrayVarP(&collectOpts.proxyNamespaces, "envoy-proxy-namespaces", "", []string{},
+		"Specify the namespaces where Envoy proxies are running.")
 
 	return collectCommand
 }
@@ -88,8 +92,16 @@ func runCollect(collectOpts collectOptions) error {
 		return fmt.Errorf("create bundle dir: %w", err)
 	}
 
-	result := tb.CollectResult(ctx, restConfig, bundlePath, collectOpts.envoyGatewayNamespace)
-	return result.ArchiveSupportBundle(bundlePath, fmt.Sprintf("%s.tar.gz", basename))
+	proxyNamespaces := sets.New(collectOpts.proxyNamespaces...)
+	opts := tb.CollectOptions{
+		BundlePath:          bundlePath,
+		CollectedNamespaces: []string{collectOpts.envoyGatewayNamespace},
+	}
+	if len(proxyNamespaces) > 0 {
+		opts.CollectedNamespaces = append(opts.CollectedNamespaces, proxyNamespaces.UnsortedList()...)
+	}
+	result := tb.CollectResult(ctx, restConfig, opts)
+	return result.ArchiveBundle(bundlePath, fmt.Sprintf("%s.tar.gz", basename))
 }
 
 func waitForSignal(c context.Context, cancel context.CancelFunc) {
diff --git a/internal/cmd/egctl/root.go b/internal/cmd/egctl/root.go
index 7143dc1e0..f94f8e23b 100644
--- a/internal/cmd/egctl/root.go
+++ b/internal/cmd/egctl/root.go
@@ -5,11 +5,17 @@
 
 package egctl
 
-import "github.com/spf13/cobra"
+import (
+	"github.com/go-logr/logr"
+	"github.com/spf13/cobra"
+	"k8s.io/klog/v2"
+)
 
 // GetRootCommand returns the root cobra command to be executed
 // by egctl main.
 func GetRootCommand() *cobra.Command {
+	// discard klog logger
+	klog.SetLogger(logr.Discard())
 	rootCmd := &cobra.Command{
 		Use:               "egctl",
 		Long:              "A command line utility for operating Envoy Gateway",
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index fce38a2e5..f9fd86003 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -961,6 +961,7 @@ func (t *Translator) buildOIDC(
 		CookieDomain:           policy.Spec.OIDC.CookieDomain,
 		HMACSecret:             hmacData,
 		PassThroughAuthHeader:  passThroughAuthHeader,
+		DenyRedirect:           oidc.DenyRedirect,
 	}, nil
 }
 
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 5d02d7df4..000b5c235 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -1115,6 +1115,11 @@ type OIDC struct {
 	// filter, normally "Authorization: Bearer ...". This is typically used for non-browser clients that
 	// may not be able to handle OIDC redirects and wish to directly supply a token instead.
 	PassThroughAuthHeader bool `json:"passThroughAuthHeader,omitempty"`
+
+	// Any request that matches any of the provided matchers won’t be redirected to OAuth server when tokens are not valid.
+	// Automatic access token refresh will be performed for these requests, if enabled.
+	// This behavior can be useful for AJAX requests.
+	DenyRedirect *egv1a1.OIDCDenyRedirect `json:"denyRedirect,omitempty"`
 }
 
 // OIDCProvider defines the schema for the OIDC Provider.
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 1b6b74cd6..5dda160da 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -2362,6 +2362,11 @@ func (in *OIDC) DeepCopyInto(out *OIDC) {
 		*out = new(string)
 		**out = **in
 	}
+	if in.DenyRedirect != nil {
+		in, out := &in.DenyRedirect, &out.DenyRedirect
+		*out = new(v1alpha1.OIDCDenyRedirect)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC.
diff --git a/internal/troubleshoot/collect.go b/internal/troubleshoot/collect.go
index 9e7b6dc9e..00a80813b 100644
--- a/internal/troubleshoot/collect.go
+++ b/internal/troubleshoot/collect.go
@@ -16,7 +16,12 @@ import (
 	"github.com/envoyproxy/gateway/internal/troubleshoot/collect"
 )
 
-func CollectResult(ctx context.Context, restConfig *rest.Config, bundlePath, egNamespace string) tbcollect.CollectorResult {
+type CollectOptions struct {
+	CollectedNamespaces []string
+	BundlePath          string
+}
+
+func CollectResult(ctx context.Context, restConfig *rest.Config, opts CollectOptions) tbcollect.CollectorResult {
 	var result tbcollect.CollectorResult
 
 	progressChan := make(chan interface{})
@@ -33,40 +38,44 @@ func CollectResult(ctx context.Context, restConfig *rest.Config, bundlePath, egN
 		// Collect the custom resources from Gateway API and EG
 		collect.CustomResource{
 			ClientConfig: restConfig,
-			BundlePath:   bundlePath,
+			BundlePath:   opts.BundlePath,
 			IncludeGroups: []string{
 				"gateway.envoyproxy.io",
 				"gateway.networking.k8s.io",
 			},
 		},
-		// Collect resources from EnvoyGateway system namespace
-		collect.EnvoyGatewayResource{
-			ClientConfig: restConfig,
-			BundlePath:   bundlePath,
-			Namespace:    egNamespace,
-		},
-		// Collect logs from EnvoyGateway system namespace
-		&tbcollect.CollectLogs{
-			Collector: &troubleshootv1b2.Logs{
-				Name:      "pod-logs",
-				Namespace: egNamespace,
+	}
+	for _, ns := range opts.CollectedNamespaces {
+		bundlePath := opts.BundlePath
+		collectors = append(collectors,
+			// Collect resources from EnvoyGateway system namespace
+			collect.EnvoyGatewayResource{
+				ClientConfig: restConfig,
+				BundlePath:   bundlePath,
+				Namespace:    ns,
 			},
-			ClientConfig: restConfig,
-			BundlePath:   bundlePath,
-			Context:      ctx,
-		},
-		// Collect prometheus metrics from EnvoyGateway system namespace
-		collect.PrometheusMetric{
-			BundlePath:   bundlePath,
-			ClientConfig: restConfig,
-			Namespace:    egNamespace,
-		},
-		// Collect config dump from EnvoyGateway system namespace
-		collect.ConfigDump{
-			BundlePath:   bundlePath,
-			ClientConfig: restConfig,
-			Namespace:    egNamespace,
-		},
+			// Collect logs from EnvoyGateway system namespace
+			&tbcollect.CollectLogs{
+				Collector: &troubleshootv1b2.Logs{
+					Name:      "pod-logs",
+					Namespace: ns,
+				},
+				ClientConfig: restConfig,
+				BundlePath:   bundlePath,
+				Context:      ctx,
+			},
+			// Collect prometheus metrics from EnvoyGateway system namespace
+			collect.PrometheusMetric{
+				BundlePath:   bundlePath,
+				ClientConfig: restConfig,
+				Namespace:    ns,
+			},
+			// Collect config dump from EnvoyGateway system namespace
+			collect.ConfigDump{
+				BundlePath:   bundlePath,
+				ClientConfig: restConfig,
+				Namespace:    ns,
+			})
 	}
 	total := len(collectors)
 	allCollectedData := make(map[string][]byte)
diff --git a/internal/troubleshoot/collect/config_dump.go b/internal/troubleshoot/collect/config_dump.go
index 7a5c9b383..e54850e61 100644
--- a/internal/troubleshoot/collect/config_dump.go
+++ b/internal/troubleshoot/collect/config_dump.go
@@ -81,8 +81,7 @@ func (cd ConfigDump) Collect(_ chan<- interface{}) (tbcollect.CollectorResult, e
 			continue
 		}
 
-		k := fmt.Sprintf("%s-%s.json", pod.Namespace, pod.Name)
-		_ = output.SaveResult(cd.BundlePath, path.Join("config-dumps", k), bytes.NewBuffer(data))
+		_ = output.SaveResult(cd.BundlePath, path.Join("config-dumps", pod.Namespace, fmt.Sprintf("%s.json", pod.Name)), bytes.NewBuffer(data))
 	}
 	if len(logs) > 0 {
 		_ = output.SaveResult(cd.BundlePath, path.Join("config-dumps", "errors.log"), marshalErrors(logs))
diff --git a/internal/troubleshoot/collect/prometheus_metrics.go b/internal/troubleshoot/collect/prometheus_metrics.go
index 9f659a54a..f8cbc10b2 100644
--- a/internal/troubleshoot/collect/prometheus_metrics.go
+++ b/internal/troubleshoot/collect/prometheus_metrics.go
@@ -25,6 +25,7 @@ import (
 	"k8s.io/client-go/rest"
 
 	kube "github.com/envoyproxy/gateway/internal/kubernetes"
+	"github.com/envoyproxy/gateway/internal/utils/str"
 )
 
 var _ tbcollect.Collector = &PrometheusMetric{}
@@ -76,32 +77,23 @@ func (p PrometheusMetric) Collect(_ chan<- interface{}) (tbcollect.CollectorResu
 
 	logs := make([]string, 0)
 	for _, pod := range pods {
-		annos := pod.GetAnnotations()
-		if v, ok := annos["prometheus.io/scrape"]; !ok || v != "true" {
-			logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of missing annotation prometheus.io/scrape", pod.Namespace, pod.Name))
-			continue
-		}
 
-		nn, port, reqPath := types.NamespacedName{Namespace: pod.Namespace, Name: pod.Name}, 19001, "/metrics"
-		if v, ok := annos["prometheus.io/port"]; !ok {
-			port, err = strconv.Atoi(v)
-			if err != nil {
-				logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of invalid prometheus.io/port", pod.Namespace, pod.Name))
-				continue
-			}
+		scrape, reqPath, port, err := getPrometheusPathAndPort(&pod)
+		if err != nil {
+			logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of err: %v", pod.Namespace, pod.Name, err))
 		}
-		if v, ok := annos["prometheus.io/path"]; ok {
-			reqPath = v
+		if !scrape {
+			logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of annotation prometheus.io/scrape=false", pod.Namespace, pod.Name))
+			continue
 		}
 
-		data, err := RequestWithPortForwarder(cliClient, nn, port, reqPath)
+		data, err := RequestWithPortForwarder(cliClient, types.NamespacedName{Name: pod.Name, Namespace: pod.Namespace}, port, reqPath)
 		if err != nil {
-			logs = append(logs, fmt.Sprintf("pod %s/%s is skipped because of err: %v", pod.Namespace, pod.Name, err))
+			logs = append(logs, fmt.Sprintf("pod %s/%s:%v%s is skipped because of err: %v", pod.Namespace, pod.Name, port, reqPath, err))
 			continue
 		}
 
-		k := fmt.Sprintf("%s-%s.prom", pod.Namespace, pod.Name)
-		_ = output.SaveResult(p.BundlePath, path.Join("prometheus-metrics", k), bytes.NewBuffer(data))
+		_ = output.SaveResult(p.BundlePath, path.Join("prometheus-metrics", pod.Namespace, fmt.Sprintf("%s.prom", pod.Name)), bytes.NewBuffer(data))
 	}
 	if len(logs) > 0 {
 		_ = output.SaveResult(p.BundlePath, path.Join("prometheus-metrics", "error.log"), bytes.NewBuffer([]byte(strings.Join(logs, "\n"))))
@@ -110,6 +102,32 @@ func (p PrometheusMetric) Collect(_ chan<- interface{}) (tbcollect.CollectorResu
 	return output, nil
 }
 
+func getPrometheusPathAndPort(pod *corev1.Pod) (bool, string, int, error) {
+	reqPath := "/metrics"
+	port := 9090
+	scrape := false
+	annotations := pod.GetAnnotations()
+	for k, v := range annotations {
+		switch str.SanitizeLabelName(k) {
+		case "prometheus_io_scrape":
+			if v != "true" {
+				return false, "", 0, fmt.Errorf("pod %s/%s is skipped because of missing annotation prometheus.io/scrape", pod.Namespace, pod.Name)
+			}
+			scrape = true
+		case "prometheus_io_port":
+			p, err := strconv.Atoi(v)
+			if err != nil {
+				return false, "", 0, fmt.Errorf("failed to parse port from annotation: %w", err)
+			}
+			port = p
+		case "prometheus_io_path":
+			reqPath = v
+		}
+	}
+
+	return scrape, reqPath, port, nil
+}
+
 func listPods(ctx context.Context, client kubernetes.Interface, namespace string, selector labels.Selector) ([]corev1.Pod, error) {
 	pods, err := client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{
 		LabelSelector: selector.String(),
diff --git a/internal/utils/str/strconv.go b/internal/utils/str/strconv.go
new file mode 100644
index 000000000..3e5db95f4
--- /dev/null
+++ b/internal/utils/str/strconv.go
@@ -0,0 +1,22 @@
+// 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 str
+
+// This file contains utility functions copied from github.com/prometheus/prometheus/util/strutil,
+// which is conflicting with the current package.
+
+import "regexp"
+
+var invalidLabelCharRE = regexp.MustCompile(`[^a-zA-Z0-9_]`)
+
+// SanitizeLabelName replaces anything that doesn't match
+// client_label.LabelNameRE with an underscore.
+// Note: this does not handle all Prometheus label name restrictions (such as
+// not starting with a digit 0-9), and hence should only be used if the label
+// name is prefixed with a known valid string.
+func SanitizeLabelName(name string) string {
+	return invalidLabelCharRE.ReplaceAllString(name, "_")
+}
diff --git a/internal/xds/translator/oidc.go b/internal/xds/translator/oidc.go
index 426502610..266e8df58 100644
--- a/internal/xds/translator/oidc.go
+++ b/internal/xds/translator/oidc.go
@@ -228,9 +228,57 @@ func oauth2Config(securityFeatures *ir.SecurityFeatures) (*oauth2v3.OAuth2, erro
 		oauth2.Config.PassThroughMatcher = buildHeaderMatchers(securityFeatures.JWT)
 	}
 
+	if oidc.DenyRedirect != nil {
+		oauth2.Config.DenyRedirectMatcher = buildDenyRedirectMatcher(oidc)
+	}
+
 	return oauth2, nil
 }
 
+func buildDenyRedirectMatcher(oidc *ir.OIDC) []*routev3.HeaderMatcher {
+	denyRedirectPathMatchers := make([]*routev3.HeaderMatcher, 0, len(oidc.DenyRedirect.Headers))
+
+	for _, m := range oidc.DenyRedirect.Headers {
+		var stringMatcher *matcherv3.StringMatcher
+
+		if m.Type == nil { // if no type is specified, default to exact match on value
+			stringMatcher = &matcherv3.StringMatcher{
+				MatchPattern: &matcherv3.StringMatcher_Exact{Exact: m.Value},
+			}
+		} else { // if type is specified, use it
+			switch *m.Type {
+			case egv1a1.StringMatchExact:
+				stringMatcher = &matcherv3.StringMatcher{
+					MatchPattern: &matcherv3.StringMatcher_Exact{Exact: m.Value},
+				}
+			case egv1a1.StringMatchPrefix:
+				stringMatcher = &matcherv3.StringMatcher{
+					MatchPattern: &matcherv3.StringMatcher_Prefix{Prefix: m.Value},
+				}
+			case egv1a1.StringMatchSuffix:
+				stringMatcher = &matcherv3.StringMatcher{
+					MatchPattern: &matcherv3.StringMatcher_Suffix{Suffix: m.Value},
+				}
+			case egv1a1.StringMatchRegularExpression:
+				stringMatcher = &matcherv3.StringMatcher{
+					MatchPattern: &matcherv3.StringMatcher_SafeRegex{
+						SafeRegex: &matcherv3.RegexMatcher{Regex: m.Value},
+					},
+				}
+			}
+		}
+
+		denyRedirectPathMatchers = append(denyRedirectPathMatchers, &routev3.HeaderMatcher{
+			Name: m.Name,
+			HeaderMatchSpecifier: &routev3.HeaderMatcher_StringMatch{
+				StringMatch: stringMatcher,
+			},
+		})
+	}
+
+	return denyRedirectPathMatchers
+}
+
 func buildHeaderMatchers(jwt *ir.JWT) []*routev3.HeaderMatcher {
 	// Bypass OIDC if a header that will be handled by JWT is passed.
 	passThroughMatchers := make([]*routev3.HeaderMatcher, 0, len(jwt.Providers))
diff --git a/internal/xds/translator/ratelimit.go b/internal/xds/translator/ratelimit.go
index 56a9e6c49..9f8501c5d 100644
--- a/internal/xds/translator/ratelimit.go
+++ b/internal/xds/translator/ratelimit.go
@@ -9,6 +9,7 @@ import (
 	"bytes"
 	"fmt"
 	"net/url"
+	"sort"
 	"strconv"
 	"strings"
 
@@ -24,6 +25,7 @@ import (
 	"google.golang.org/protobuf/types/known/durationpb"
 	"google.golang.org/protobuf/types/known/wrapperspb"
 	goyaml "gopkg.in/yaml.v3" // nolint: depguard
+	"k8s.io/apimachinery/pkg/util/sets"
 
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/ir"
@@ -72,50 +74,33 @@ func (t *Translator) buildRateLimitFilter(irListener *ir.HTTPListener) []*hcmv3.
 	if irListener == nil || irListener.Routes == nil {
 		return nil
 	}
-
-	filters := []*hcmv3.HttpFilter{}
-	created := make(map[string]bool)
-
+	domains := sets.New[string]()
 	for _, route := range irListener.Routes {
 		if !isValidGlobalRateLimit(route) {
 			continue
 		}
-
-		hasShared, hasNonShared := false, false
-		var sharedRuleName string
-
 		for _, rule := range route.Traffic.RateLimit.Global.Rules {
 			if isRuleShared(rule) {
-				hasShared = true
-				sharedRuleName = stripRuleIndexSuffix(rule.Name)
-			} else {
-				hasNonShared = true
+				domains.Insert(stripRuleIndexSuffix(rule.Name))
 			}
 		}
+	}
+	domains.Insert(irListener.Name)
 
-		if hasShared {
-			sharedDomain := sharedRuleName
-			if !created[sharedDomain] {
-				filterName := fmt.Sprintf("%s/%s", egv1a1.EnvoyFilterRateLimit.String(), sharedRuleName)
-				if filter := createRateLimitFilter(t, irListener, sharedDomain, filterName); filter != nil {
-					filters = append(filters, filter)
-				}
-				created[sharedDomain] = true
-			}
-		}
+	// Deterministic order otherwise tests break
+	domainList := sets.List(domains)
+	sort.Strings(domainList)
 
-		if hasNonShared {
-			nonSharedDomain := irListener.Name
-			if !created[nonSharedDomain] {
-				filterName := egv1a1.EnvoyFilterRateLimit.String()
-				if filter := createRateLimitFilter(t, irListener, nonSharedDomain, filterName); filter != nil {
-					filters = append(filters, filter)
-				}
-				created[nonSharedDomain] = true
-			}
+	var filters []*hcmv3.HttpFilter
+	for _, domain := range domainList {
+		filterName := egv1a1.EnvoyFilterRateLimit.String()
+		if domain != irListener.Name {
+			filterName += "/" + domain
+		}
+		if filter := createRateLimitFilter(t, irListener, domain, filterName); filter != nil {
+			filters = append(filters, filter)
 		}
 	}
-
 	return filters
 }
 
@@ -438,13 +423,15 @@ func BuildRateLimitServiceConfig(irListeners []*ir.HTTPListener) []*rlsconfv3.Ra
 				continue
 			}
 
-			// Process shared rules - add to traffic policy domain
-			sharedDomain := getDomainSharedName(route)
-			addRateLimitDescriptor(route, descriptors, sharedDomain, domainDesc, true)
-
-			// Process non-shared rules - add to listener-specific domain
-			listenerDomain := irListener.Name
-			addRateLimitDescriptor(route, descriptors, listenerDomain, domainDesc, false)
+			// For each rule, add to the correct domain only
+			for rIdx, rule := range route.Traffic.RateLimit.Global.Rules {
+				descriptor := descriptors[rIdx]
+				domain := irListener.Name
+				if isRuleShared(rule) {
+					domain = stripRuleIndexSuffix(rule.Name)
+				}
+				addRateLimitDescriptor(route, rule, descriptor, domain, domainDesc)
+			}
 		}
 	}
 
@@ -488,7 +475,7 @@ func descriptorsEqual(a, b *rlsconfv3.RateLimitDescriptor) bool {
 	return true
 }
 
-// addRateLimitDescriptor adds rate limit descriptors to the domain descriptor map.
+// addRateLimitDescriptor adds rate limit descriptors from a single rule to the domain descriptor map.
 // Handles both shared and route-specific rate limits.
 //
 // An example of route descriptor looks like this:
@@ -501,54 +488,48 @@ func descriptorsEqual(a, b *rlsconfv3.RateLimitDescriptor) bool {
 //   - ...
 func addRateLimitDescriptor(
 	route *ir.HTTPRoute,
-	serviceDescriptors []*rlsconfv3.RateLimitDescriptor,
+	rule *ir.RateLimitRule,
+	descriptor *rlsconfv3.RateLimitDescriptor,
 	domain string,
 	domainDescriptors map[string][]*rlsconfv3.RateLimitDescriptor,
-	includeShared bool,
 ) {
-	if !isValidGlobalRateLimit(route) || len(serviceDescriptors) == 0 {
+	if !isValidGlobalRateLimit(route) || descriptor == nil {
 		return
 	}
 
-	for i, rule := range route.Traffic.RateLimit.Global.Rules {
-		if i >= len(serviceDescriptors) || (includeShared != isRuleShared(rule)) {
-			continue
-		}
-
-		var descriptorKey string
-		if isRuleShared(rule) {
-			descriptorKey = rule.Name
-		} else {
-			descriptorKey = getRouteDescriptor(route.Name)
-		}
+	var descriptorKey string
+	if isRuleShared(rule) {
+		descriptorKey = rule.Name
+	} else {
+		descriptorKey = getRouteDescriptor(route.Name)
+	}
 
-		// Find or create descriptor in domainDescriptors[domain]
-		var descriptorRule *rlsconfv3.RateLimitDescriptor
-		found := false
-		for _, d := range domainDescriptors[domain] {
-			if d.Key == descriptorKey {
-				descriptorRule = d
-				found = true
-				break
-			}
-		}
-		if !found {
-			descriptorRule = &rlsconfv3.RateLimitDescriptor{Key: descriptorKey, Value: descriptorKey}
-			domainDescriptors[domain] = append(domainDescriptors[domain], descriptorRule)
+	// Find or create descriptor in domainDescriptors[domain]
+	var descriptorRule *rlsconfv3.RateLimitDescriptor
+	found := false
+	for _, d := range domainDescriptors[domain] {
+		if d.Key == descriptorKey {
+			descriptorRule = d
+			found = true
+			break
 		}
+	}
+	if !found {
+		descriptorRule = &rlsconfv3.RateLimitDescriptor{Key: descriptorKey, Value: descriptorKey}
+		domainDescriptors[domain] = append(domainDescriptors[domain], descriptorRule)
+	}
 
-		// Ensure no duplicate descriptors
-		alreadyExists := false
-		for _, existing := range descriptorRule.Descriptors {
-			if descriptorsEqual(existing, serviceDescriptors[i]) {
-				alreadyExists = true
-				break
-			}
-		}
-		if !alreadyExists {
-			descriptorRule.Descriptors = append(descriptorRule.Descriptors, serviceDescriptors[i])
+	// Ensure no duplicate descriptors
+	alreadyExists := false
+	for _, existing := range descriptorRule.Descriptors {
+		if descriptorsEqual(existing, descriptor) {
+			alreadyExists = true
+			break
 		}
 	}
+	if !alreadyExists {
+		descriptorRule.Descriptors = append(descriptorRule.Descriptors, descriptor)
+	}
 }
 
 // isSharedRateLimit checks if a route has at least one shared rate limit rule.
@@ -579,16 +560,6 @@ func isRuleShared(rule *ir.RateLimitRule) bool {
 	return rule != nil && rule.Shared != nil && *rule.Shared
 }
 
-// Helper function to check if a specific rule in a route is shared
-func isRuleAtIndexShared(route *ir.HTTPRoute, ruleIndex int) bool {
-	if route == nil || route.Traffic == nil || route.Traffic.RateLimit == nil ||
-		route.Traffic.RateLimit.Global == nil || len(route.Traffic.RateLimit.Global.Rules) <= ruleIndex || ruleIndex < 0 {
-		return false
-	}
-
-	return isRuleShared(route.Traffic.RateLimit.Global.Rules[ruleIndex])
-}
-
 // Helper function to map a global rule index to a domain-specific rule index
 // This ensures that both shared and non-shared rules have indices starting from 0 in their own domains.
 func getDomainRuleIndex(rules []*ir.RateLimitRule, globalRuleIdx int, ruleIsShared bool) int {
@@ -710,19 +681,8 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 		// 3) No Match (apply to all traffic)
 		if !rule.IsMatchSet() {
 			pbDesc := new(rlsconfv3.RateLimitDescriptor)
-
-			// Determine if we should use the shared rate limit key (rule-based) or a generic route key
-			if isRuleAtIndexShared(route, rIdx) {
-				// For shared rate limits, use rule name
-				pbDesc.Key = rule.Name
-				pbDesc.Value = rule.Name
-			} else {
-				// Use generic key for non-shared rate limits, with prefix for uniqueness
-				descriptorKey := getRouteRuleDescriptor(domainRuleIdx, -1)
-				pbDesc.Key = descriptorKey
-				pbDesc.Value = pbDesc.Key
-			}
-
+			pbDesc.Key = getRouteRuleDescriptor(domainRuleIdx, -1)
+			pbDesc.Value = getRouteRuleDescriptor(domainRuleIdx, -1)
 			head = pbDesc
 			cur = head
 		}
@@ -735,11 +695,6 @@ func buildRateLimitServiceDescriptors(route *ir.HTTPRoute) []*rlsconfv3.RateLimi
 	return pbDescriptors
 }
 
-// getDomainSharedName returns the shared domain (stripped policy name), stripRuleIndexSuffix is used to remove the rule index suffix.
-func getDomainSharedName(route *ir.HTTPRoute) string {
-	return stripRuleIndexSuffix(route.Traffic.RateLimit.Global.Rules[0].Name)
-}
-
 func getRouteRuleDescriptor(ruleIndex, matchIndex int) string {
 	return "rule-" + strconv.Itoa(ruleIndex) + "-match-" + strconv.Itoa(matchIndex)
 }
diff --git a/internal/xds/translator/translator.go b/internal/xds/translator/translator.go
index a737fe50f..79e59b39a 100644
--- a/internal/xds/translator/translator.go
+++ b/internal/xds/translator/translator.go
@@ -38,10 +38,16 @@ import (
 
 const (
 	AuthorityHeaderKey = ":authority"
-	// The dummy cluster for TCP listeners that have no routes
+	// The dummy cluster name for TCP/UDP listeners that have no routes
 	emptyClusterName = "EmptyCluster"
 )
 
+// The dummy cluster for TCP/UDP listeners that have no routes
+var emptyRouteCluster = &clusterv3.Cluster{
+	Name:                 emptyClusterName,
+	ClusterDiscoveryType: &clusterv3.Cluster_Type{Type: clusterv3.Cluster_STATIC},
+}
+
 // Translator translates the xDS IR into xDS resources.
 type Translator struct {
 	// ControllerNamespace is the namespace of the Gateway API controller
@@ -731,11 +737,6 @@ func (t *Translator) processTCPListenerXdsTranslation(
 		// If there are no routes, add a route without a destination to the listener to create a filter chain
 		// This is needed because Envoy requires a filter chain to be present in the listener, otherwise it will reject the listener and report a warning
 		if len(tcpListener.Routes) == 0 {
-			emptyRouteCluster := &clusterv3.Cluster{
-				Name:                 emptyClusterName,
-				ClusterDiscoveryType: &clusterv3.Cluster_Type{Type: clusterv3.Cluster_STATIC},
-			}
-
 			if findXdsCluster(tCtx, emptyClusterName) == nil {
 				if err := tCtx.AddXdsResource(resourcev3.ClusterType, emptyRouteCluster); err != nil {
 					errs = errors.Join(errs, err)
@@ -770,29 +771,41 @@ func processUDPListenerXdsTranslation(
 		// There won't be multiple UDP listeners on the same port since it's already been checked at the gateway api
 		// translator
 		if udpListener.Route != nil {
-			route := udpListener.Route
-
-			xdsListener, err := buildXdsUDPListener(route.Destination.Name, udpListener, accesslog)
-			if err != nil {
-				// skip this listener if failed to build xds listener
-				errs = errors.Join(errs, err)
-				continue
-			}
-			if err := tCtx.AddXdsResource(resourcev3.ListenerType, xdsListener); err != nil {
-				// skip this listener if failed to add xds listener to the resource version table
-				errs = errors.Join(errs, err)
-				continue
-			}
-
 			// 1:1 between IR UDPRoute and xDS Cluster
 			if err := processXdsCluster(tCtx,
-				route.Destination.Name,
-				route.Destination.Settings,
-				&UDPRouteTranslator{route},
+				udpListener.Route.Destination.Name,
+				udpListener.Route.Destination.Settings,
+				&UDPRouteTranslator{udpListener.Route},
 				&ExtraArgs{metrics: metrics},
-				route.Destination.Metadata); err != nil {
+				udpListener.Route.Destination.Metadata); err != nil {
 				errs = errors.Join(errs, err)
 			}
+		} else {
+			udpListener.Route = &ir.UDPRoute{
+				Name: emptyClusterName,
+				Destination: &ir.RouteDestination{
+					Name: emptyClusterName,
+				},
+			}
+
+			// Add empty cluster for UDP listener which have no Route, when empty cluster is not found.
+			if findXdsCluster(tCtx, emptyClusterName) == nil {
+				if err := tCtx.AddXdsResource(resourcev3.ClusterType, emptyRouteCluster); err != nil {
+					errs = errors.Join(errs, err)
+				}
+			}
+		}
+
+		xdsListener, err := buildXdsUDPListener(udpListener.Route.Destination.Name, udpListener, accesslog)
+		if err != nil {
+			// skip this listener if failed to build xds listener
+			errs = errors.Join(errs, err)
+			continue
+		}
+		if err := tCtx.AddXdsResource(resourcev3.ListenerType, xdsListener); err != nil {
+			// skip this listener if failed to add xds listener to the resource version table
+			errs = errors.Join(errs, err)
+			continue
 		}
 	}
 	return errs
diff --git a/release-notes/current.yaml b/release-notes/current.yaml
index decd1f725..d269adce3 100644
--- a/release-notes/current.yaml
+++ b/release-notes/current.yaml
@@ -20,6 +20,7 @@ new features: |
 bug fixes: |
   Handle integer zone annotation values
   Fixed issue where WASM cache init failure caused routes with WASM-less EnvoyExtensionPolicies to have 500 direct responses.
+  Fixed issue which UDP listeners were not created in the Envoy proxy config when Gateway was created.
 
 # Enhancements that improve performance.
 performance improvements: |
diff --git a/site/content/en/v1.4/tasks/operations/egctl.md b/site/content/en/v1.4/tasks/operations/egctl.md
index 36c1f9979..8aa865a9b 100644
--- a/site/content/en/v1.4/tasks/operations/egctl.md
+++ b/site/content/en/v1.4/tasks/operations/egctl.md
@@ -4,6 +4,294 @@ title: "Use egctl"
 
 `egctl` is a command line tool to provide additional functionality for Envoy Gateway users.
 
+Follow the instructions [here](./../../install/install-egctl.md) to install `egctl`.
+
+## egctl config
+
+The `egctl config` subcommand provides tools for viewing and validating the Envoy Proxy and Envoy RateLimit configuration configured in Envoy Proxy.
+
+Here's an example of retrieving the [xDS Route Configuration](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route.proto#envoy-v3-api-msg-config-route-v3-routeconfiguration) from all the proxies. 
+
+```console
+~ egctl config envoy-proxy route
+{
+  "envoy-gateway-system": {
+    "envoy-default-eg-e41e7b31-6d749cdb85-hcfrk": {
+      "@type": "type.googleapis.com/envoy.admin.v3.RoutesConfigDump",
+      "dynamicRouteConfigs": [
+        {
+          "lastUpdated": "2025-05-22T17:51:06.237Z",
+          "routeConfig": {
+            "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+            "ignorePortInHostMatching": true,
+            "name": "default/eg/http",
+            "virtualHosts": [
+              {
+                "domains": [
+                  "www.example.com"
+                ],
+                "metadata": {
+                  "filterMetadata": {
+                    "envoy-gateway": {
+                      "resources": [
+                        {
+                          "kind": "Gateway",
+                          "name": "eg",
+                          "namespace": "default",
+                          "sectionName": "http"
+                        }
+                      ]
+                    }
+                  }
+                },
+                "name": "default/eg/http/www_example_com",
+                "routes": [
+                  {
+                    "match": {
+                      "prefix": "/"
+                    },
+                    "metadata": {
+                      "filterMetadata": {
+                        "envoy-gateway": {
+                          "resources": [
+                            {
+                              "kind": "HTTPRoute",
+                              "name": "backend",
+                              "namespace": "default"
+                            }
+                          ]
+                        }
+                      }
+                    },
+                    "name": "httproute/default/backend/rule/0/match/0/www_example_com",
+                    "route": {
+                      "cluster": "httproute/default/backend/rule/0",
+                      "upgradeConfigs": [
+                        {
+                          "upgradeType": "websocket"
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            ]
+          },
+          "versionInfo": "bc44981323d08bcb24abad6867942912675bb35bb674f2ee9ae866efbe1375dd"
+        }
+      ],
+      "staticRouteConfigs": [
+        {
+          "lastUpdated": "2025-05-22T17:51:06.198Z",
+          "routeConfig": {
+            "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+            "name": "local_route",
+            "virtualHosts": [
+              {
+                "domains": [
+                  "*"
+                ],
+                "name": "prometheus_stats",
+                "routes": [
+                  {
+                    "match": {
+                      "headers": [
+                        {
+                          "name": ":method",
+                          "stringMatch": {
+                            "exact": "GET"
+                          }
+                        }
+                      ],
+                      "path": "/stats/prometheus"
+                    },
+                    "route": {
+                      "cluster": "prometheus_stats"
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        },
+        {
+          "lastUpdated": "2025-05-22T17:51:06.219Z",
+          "routeConfig": {
+            "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+            "name": "ready_route",
+            "virtualHosts": [
+              {
+                "domains": [
+                  "*"
+                ],
+                "name": "ready_route",
+                "routes": [
+                  {
+                    "directResponse": {
+                      "status": 500
+                    },
+                    "match": {
+                      "prefix": "/"
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      ]
+    }
+  }
+}
+```
+
+## egctl experimental status
+
+This subcommand allows users to show the summary of the status of specific or all resource types, in order to quickly find
+out the status of any resources.
+
+By default, `egctl x status` display all the conditions for one resource type. You can either add `--quiet` to only
+display the latest condition, or add `--verbose` to display more details about current status.
+
+{{% alert title="Note" color="primary" %}}
+
+The resource types that this subcommand currently supports:
+
+- `xRoute`, `HTTPRoute`, `GRPCRoute`, `TLSRoute`, `TCPRoute`, `UDPRoute`
+- `xPolicy`, `BackendTLSPolicy`, `BackendTrafficPolicy`, `ClientTrafficPolicy`, `EnvoyPatchPolicy`, `SecurityPolicy`
+- `all`, `GatewayClass`, `Gateway`
+
+{{% /alert %}}
+
+Some examples of this command after installing [Multi-tenancy][] example manifest:
+
+- Show the summary of GatewayClass.
+
+```console
+~ egctl x status gatewayclass
+
+NAME           TYPE       STATUS    REASON
+eg-marketing   Accepted   True      Accepted
+eg-product     Accepted   True      Accepted
+```
+
+- Show the summary of all resource types under all namespaces, the resource type with empty status will be ignored.
+
+```console
+~ egctl x status all -A
+
+NAME                        TYPE       STATUS    REASON
+gatewayclass/eg-marketing   Accepted   True      Accepted
+gatewayclass/eg-product     Accepted   True      Accepted
+
+NAMESPACE   NAME         TYPE         STATUS    REASON
+marketing   gateway/eg   Programmed   True      Programmed
+                         Accepted     True      Accepted
+product     gateway/eg   Programmed   True      Programmed
+                         Accepted     True      Accepted
+
+NAMESPACE   NAME                PARENT       TYPE           STATUS    REASON
+marketing   httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+                                             Accepted       True      Accepted
+product     httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+                                             Accepted       True      Accepted
+```
+
+- Show the summary of all the Gateways with details under all namespaces.
+
+```console
+~ egctl x status gateway --verbose --all-namespaces
+
+NAMESPACE   NAME      TYPE         STATUS    REASON       MESSAGE                                                                    OBSERVED GENERATION   LAST TRANSITION TIME
+marketing   eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
+                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:50:39 +0800 CST
+product     eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
+                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:52:42 +0800 CST
+```
+
+- Show the summary of the latest Gateways condition under `product` namespace.
+
+```console
+~ egctl x status gateway --quiet -n product
+
+NAME      TYPE         STATUS    REASON
+eg        Programmed   True      Programmed
+```
+
+- Show the summary of latest HTTPRoutes condition under all namespaces.
+
+```console
+~ egctl x status httproute --quiet --all-namespaces
+
+NAMESPACE   NAME      PARENT       TYPE           STATUS    REASON
+marketing   backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+product     backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+```
+
+[Multi-tenancy]: ../deployment-mode#multi-tenancy
+[EnvoyProxy]: ../../../api/extension_types#envoyproxy
+
+## egctl experimental dashboard
+
+This subcommand streamlines the process for users to access the Envoy admin dashboard. By executing the following command:
+
+```bash
+egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
+```
+
+You will see the following output:
+
+```bash
+egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
+http://localhost:19000
+```
+
+the Envoy admin dashboard will automatically open in your default web browser. This eliminates the need to manually locate and expose the admin port.
+
+
+## egctl experimental install
+
+This subcommand can be used to install envoy-gateway.
+
+```bash
+egctl x install
+```
+
+By default, this will install both the envoy-gateway workload resource and the required gateway-api and envoy-gatewayCRDs.
+
+We can specify to install only workload resources via `--skip-crds`
+
+```bash
+egctl x install --skip-crds
+```
+
+We can specify to install only CRDs resources via `--only-crds`
+
+```bash
+egctl x install --only-crds
+```
+
+We can specify `--name` and `--namespace` to install envoy-gateway in different places to support multi-tenant mode.
+> Note: If CRDs are already installed, then we need to specify `--skip-crds` to avoid repeated installation of CRDs resources.
+
+```bash
+egctl x install --name shop-backend --namespace shop
+```
+
+## egctl experimental uninstall
+
+This subcommand can be used to uninstall envoy-gateway.
+
+```bash
+egctl x uninstall
+```
+
+By default, this will only uninstall the envoy-gateway workload resource, if we want to also uninstall CRDs, we need to specify `--with-crds`
+
+```bash
+egctl x uninstall --with-crds
+```
+
 ## egctl experimental translate
 
 This subcommand allows users to translate from an input configuration type to an output configuration type.
@@ -755,152 +1043,3 @@ xds:
   envoy-gateway-system-eg:
     '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump
 ```
-
-## egctl experimental status
-
-This subcommand allows users to show the summary of the status of specific or all resource types, in order to quickly find
-out the status of any resources.
-
-By default, `egctl x status` display all the conditions for one resource type. You can either add `--quiet` to only
-display the latest condition, or add `--verbose` to display more details about current status.
-
-{{% alert title="Note" color="primary" %}}
-
-The resource types that this subcommand currently supports:
-
-- `xRoute`, `HTTPRoute`, `GRPCRoute`, `TLSRoute`, `TCPRoute`, `UDPRoute`
-- `xPolicy`, `BackendTLSPolicy`, `BackendTrafficPolicy`, `ClientTrafficPolicy`, `EnvoyPatchPolicy`, `SecurityPolicy`
-- `all`, `GatewayClass`, `Gateway`
-
-{{% /alert %}}
-
-Some examples of this command after installing [Multi-tenancy][] example manifest:
-
-- Show the summary of GatewayClass.
-
-```console
-~ egctl x status gatewayclass
-
-NAME           TYPE       STATUS    REASON
-eg-marketing   Accepted   True      Accepted
-eg-product     Accepted   True      Accepted
-```
-
-- Show the summary of all resource types under all namespaces, the resource type with empty status will be ignored.
-
-```console
-~ egctl x status all -A
-
-NAME                        TYPE       STATUS    REASON
-gatewayclass/eg-marketing   Accepted   True      Accepted
-gatewayclass/eg-product     Accepted   True      Accepted
-
-NAMESPACE   NAME         TYPE         STATUS    REASON
-marketing   gateway/eg   Programmed   True      Programmed
-                         Accepted     True      Accepted
-product     gateway/eg   Programmed   True      Programmed
-                         Accepted     True      Accepted
-
-NAMESPACE   NAME                PARENT       TYPE           STATUS    REASON
-marketing   httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-                                             Accepted       True      Accepted
-product     httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-                                             Accepted       True      Accepted
-```
-
-- Show the summary of all the Gateways with details under all namespaces.
-
-```console
-~ egctl x status gateway --verbose --all-namespaces
-
-NAMESPACE   NAME      TYPE         STATUS    REASON       MESSAGE                                                                    OBSERVED GENERATION   LAST TRANSITION TIME
-marketing   eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
-                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:50:39 +0800 CST
-product     eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
-                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:52:42 +0800 CST
-```
-
-- Show the summary of the latest Gateways condition under `product` namespace.
-
-```console
-~ egctl x status gateway --quiet -n product
-
-NAME      TYPE         STATUS    REASON
-eg        Programmed   True      Programmed
-```
-
-- Show the summary of latest HTTPRoutes condition under all namespaces.
-
-```console
-~ egctl x status httproute --quiet --all-namespaces
-
-NAMESPACE   NAME      PARENT       TYPE           STATUS    REASON
-marketing   backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-product     backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-```
-
-[Multi-tenancy]: ../deployment-mode#multi-tenancy
-[EnvoyProxy]: ../../../api/extension_types#envoyproxy
-
-
-## egctl experimental dashboard
-
-This subcommand streamlines the process for users to access the Envoy admin dashboard. By executing the following command:
-
-```bash
-egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
-```
-
-You will see the following output:
-
-```bash
-egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
-http://localhost:19000
-```
-
-the Envoy admin dashboard will automatically open in your default web browser. This eliminates the need to manually locate and expose the admin port.
-
-
-## egctl experimental install
-
-This subcommand can be used to install envoy-gateway.
-
-```bash
-egctl x install
-```
-
-By default, this will install both the envoy-gateway workload resource and the required gateway-api and envoy-gatewayCRDs.
-
-We can specify to install only workload resources via `--skip-crds`
-
-```bash
-egctl x install --skip-crds
-```
-
-We can specify to install only CRDs resources via `--only-crds`
-
-```bash
-egctl x install --only-crds
-```
-
-We can specify `--name` and `--namespace` to install envoy-gateway in different places to support multi-tenant mode.
-> Note: If CRDs are already installed, then we need to specify `--skip-crds` to avoid repeated installation of CRDs resources.
-
-```bash
-egctl x install --name shop-backend --namespace shop
-```
-
-
-## egctl experimental uninstall
-
-This subcommand can be used to uninstall envoy-gateway.
-
-```bash
-egctl x uninstall
-```
-
-By default, this will only uninstall the envoy-gateway workload resource, if we want to also uninstall CRDs, we need to specify `--with-crds`
-
-```bash
-egctl x uninstall --with-crds
-```
\ No newline at end of file


Test patch:
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-deny-redirect.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-deny-redirect.in.yaml
new file mode 100644
index 000000000..12d7a1bc9
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-deny-redirect.in.yaml
@@ -0,0 +1,89 @@
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway
+    name: client1-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway-system
+    name: envoy-oidc-hmac
+  data:
+    hmac-secret: qrOYACHXoe7UEDI/raOjNSx+Z9ufXSc/22C3T6X/zPY=
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: envoy-gateway
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-1
+  spec:
+    hostnames:
+    - www.example.com
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway  # This policy should attach httproute-2
+    uid: b8284d0f-de82-4c65-b204-96a0d3f258a1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        name: "client1-secret"
+      redirectURL: "https://www.example.com/bar/oauth2/callback"
+      logoutPath: "/bar/logout"
+      denyRedirect:
+        headers:
+        - name: :path
+          type: Prefix
+          value: /api
+        - name: X-No-Redirect
+          type: RegularExpression
+          value: ".*"
+        - name: X-No-Redirect
+          type: Exact
+          value: "foobar"
+        - name: :path
+          type: Suffix
+          value: "bar"
+        - name: test-5
+          value: "bar"
+      cookieNames:
+        idToken: "CustomIdTokenCookie"
+        accessToken: "CustomAccessTokenCookie"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-deny-redirect.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-deny-redirect.out.yaml
new file mode 100644
index 000000000..cf1eba57f
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-deny-redirect.out.yaml
@@ -0,0 +1,242 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: envoy-gateway
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 1
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - www.example.com
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          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: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+infraIR:
+  envoy-gateway/gateway-1:
+    proxy:
+      listeners:
+      - address: null
+        name: envoy-gateway/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: envoy-gateway
+      name: envoy-gateway/gateway-1
+      namespace: envoy-gateway-system
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-gateway
+    namespace: envoy-gateway
+    uid: b8284d0f-de82-4c65-b204-96a0d3f258a1
+  spec:
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client1-secret
+      cookieNames:
+        accessToken: CustomAccessTokenCookie
+        idToken: CustomIdTokenCookie
+      denyRedirect:
+        headers:
+        - name: :path
+          type: Prefix
+          value: /api
+        - name: X-No-Redirect
+          type: RegularExpression
+          value: .*
+        - name: X-No-Redirect
+          type: Exact
+          value: foobar
+        - name: :path
+          type: Suffix
+          value: bar
+        - name: test-5
+          value: bar
+      logoutPath: /bar/logout
+      provider:
+        issuer: https://accounts.google.com
+      redirectURL: https://www.example.com/bar/oauth2/callback
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    ancestors:
+    - ancestorRef:
+        group: gateway.networking.k8s.io
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+      conditions:
+      - lastTransitionTime: null
+        message: Policy has been accepted.
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      json:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      metadata:
+        kind: Gateway
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+      name: envoy-gateway/gateway-1/http
+      path:
+        escapedSlashesAction: UnescapeAndRedirect
+        mergeSlashes: true
+      port: 10080
+      routes:
+      - destination:
+          metadata:
+            kind: HTTPRoute
+            name: httproute-1
+            namespace: default
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - addressType: IP
+            endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            metadata:
+              name: service-1
+              namespace: default
+              sectionName: "8080"
+            name: httproute/default/httproute-1/rule/0/backend/0
+            protocol: HTTP
+            weight: 1
+        hostname: www.example.com
+        isHTTP2: false
+        metadata:
+          kind: HTTPRoute
+          name: httproute-1
+          namespace: default
+        name: httproute/default/httproute-1/rule/0/match/0/www_example_com
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+        security:
+          oidc:
+            clientID: client1.apps.googleusercontent.com
+            clientSecret: '[redacted]'
+            cookieNameOverrides:
+              accessToken: CustomAccessTokenCookie
+              idToken: CustomIdTokenCookie
+            cookieSuffix: b0a1b740
+            denyRedirect:
+              headers:
+              - name: :path
+                type: Prefix
+                value: /api
+              - name: X-No-Redirect
+                type: RegularExpression
+                value: .*
+              - name: X-No-Redirect
+                type: Exact
+                value: foobar
+              - name: :path
+                type: Suffix
+                value: bar
+              - name: test-5
+                value: bar
+            hmacSecret: '[redacted]'
+            logoutPath: /bar/logout
+            name: securitypolicy/envoy-gateway/policy-for-gateway
+            provider:
+              authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth
+              tokenEndpoint: https://oauth2.googleapis.com/token
+            redirectPath: /bar/oauth2/callback
+            redirectURL: https://www.example.com/bar/oauth2/callback
+            scopes:
+            - openid
+    readyListener:
+      address: 0.0.0.0
+      ipFamily: IPv4
+      path: /ready
+      port: 19003
diff --git a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml
index cf3a3f9ad..0c3c60fd5 100644
--- a/internal/xds/translator/testdata/in/xds-ir/oidc.yaml
+++ b/internal/xds/translator/testdata/in/xds-ir/oidc.yaml
@@ -70,6 +70,22 @@ http:
         - api
         redirectURL: "https://www.example.com/bar/oauth2/callback"
         redirectPath: "/bar/oauth2/callback"
+        denyRedirect:
+          headers:
+          - name: test-exact
+            type: Exact
+            value: /api
+          - name: test-regex
+            type: RegularExpression
+            value: .*
+          - name: test-suffix
+            type: Suffix
+            value: bar
+          - name: test-prefix
+            type: Prefix
+            value: foo
+          - name: test-no-type
+            value: foobar
         logoutPath: "/bar/logout"
         cookieSuffix: 5f93c2e4
         cookieNameOverrides:
diff --git a/internal/xds/translator/testdata/in/xds-ir/tcp-route-with-no-routes.yaml b/internal/xds/translator/testdata/in/xds-ir/tcp-route-with-no-routes.yaml
new file mode 100644
index 000000000..dafca1cf2
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/tcp-route-with-no-routes.yaml
@@ -0,0 +1,4 @@
+tcp:
+- name: "tcp-listener-with-no-routes"
+  address: "::"
+  port: 10080
diff --git a/internal/xds/translator/testdata/in/xds-ir/udp-route-with-no-routes.yaml b/internal/xds/translator/testdata/in/xds-ir/udp-route-with-no-routes.yaml
new file mode 100644
index 000000000..fc6a647cb
--- /dev/null
+++ b/internal/xds/translator/testdata/in/xds-ir/udp-route-with-no-routes.yaml
@@ -0,0 +1,4 @@
+udp:
+- name: "udp-route-with-no-routes"
+  address: "::"
+  port: 10080
diff --git a/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml
index 714f4f17e..3a94b3a8b 100644
--- a/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/oidc.listeners.yaml
@@ -92,6 +92,23 @@
                   sdsConfig:
                     ads: {}
                     resourceApiVersion: V3
+              denyRedirectMatcher:
+              - name: test-exact
+                stringMatch:
+                  exact: /api
+              - name: test-regex
+                stringMatch:
+                  safeRegex:
+                    regex: .*
+              - name: test-suffix
+                stringMatch:
+                  suffix: bar
+              - name: test-prefix
+                stringMatch:
+                  prefix: foo
+              - name: test-no-type
+                stringMatch:
+                  exact: foobar
               preserveAuthorizationHeader: true
               redirectPathMatcher:
                 path:
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-global-shared.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-global-shared.listeners.yaml
index fd03d88dc..fd3c8f946 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit-global-shared.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-global-shared.listeners.yaml
@@ -14,30 +14,30 @@
           initialStreamWindowSize: 65536
           maxConcurrentStreams: 100
         httpFilters:
-        - name: envoy.filters.http.ratelimit/test-namespace/test-policy-1
+        - name: envoy.filters.http.ratelimit
           typedConfig:
             '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
-            domain: test-namespace/test-policy-1
+            domain: first-listener
             enableXRatelimitHeaders: DRAFT_VERSION_03
             rateLimitService:
               grpcService:
                 envoyGrpc:
                   clusterName: ratelimit_cluster
               transportApiVersion: V3
-        - name: envoy.filters.http.ratelimit/test-namespace/test-policy-2
+        - name: envoy.filters.http.ratelimit/test-namespace/test-policy-1
           typedConfig:
             '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
-            domain: test-namespace/test-policy-2
+            domain: test-namespace/test-policy-1
             enableXRatelimitHeaders: DRAFT_VERSION_03
             rateLimitService:
               grpcService:
                 envoyGrpc:
                   clusterName: ratelimit_cluster
               transportApiVersion: V3
-        - name: envoy.filters.http.ratelimit
+        - name: envoy.filters.http.ratelimit/test-namespace/test-policy-2
           typedConfig:
             '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
-            domain: first-listener
+            domain: test-namespace/test-policy-2
             enableXRatelimitHeaders: DRAFT_VERSION_03
             rateLimitService:
               grpcService:
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-multi-global-shared.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-multi-global-shared.listeners.yaml
index 0e7613089..fd3c8f946 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit-multi-global-shared.listeners.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-multi-global-shared.listeners.yaml
@@ -14,6 +14,16 @@
           initialStreamWindowSize: 65536
           maxConcurrentStreams: 100
         httpFilters:
+        - name: envoy.filters.http.ratelimit
+          typedConfig:
+            '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
+            domain: first-listener
+            enableXRatelimitHeaders: DRAFT_VERSION_03
+            rateLimitService:
+              grpcService:
+                envoyGrpc:
+                  clusterName: ratelimit_cluster
+              transportApiVersion: V3
         - name: envoy.filters.http.ratelimit/test-namespace/test-policy-1
           typedConfig:
             '@type': type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.clusters.yaml
new file mode 100644
index 000000000..0764d46f0
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.clusters.yaml
@@ -0,0 +1,2 @@
+- name: EmptyCluster
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.endpoints.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.endpoints.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.listeners.yaml
new file mode 100644
index 000000000..4250f0a6b
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.listeners.yaml
@@ -0,0 +1,14 @@
+- address:
+    socketAddress:
+      address: '::'
+      portValue: 10080
+  filterChains:
+  - filters:
+    - name: envoy.filters.network.tcp_proxy
+      typedConfig:
+        '@type': type.googleapis.com/envoy.extensions.filters.network.tcp_proxy.v3.TcpProxy
+        cluster: EmptyCluster
+        statPrefix: tcp-10080
+    name: EmptyCluster
+  name: tcp-listener-with-no-routes
+  perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-with-no-routes.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.clusters.yaml
new file mode 100644
index 000000000..0764d46f0
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.clusters.yaml
@@ -0,0 +1,2 @@
+- name: EmptyCluster
+  type: STATIC
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.endpoints.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.endpoints.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.listeners.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.listeners.yaml
new file mode 100644
index 000000000..ce76497d0
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.listeners.yaml
@@ -0,0 +1,18 @@
+- address:
+    socketAddress:
+      address: '::'
+      portValue: 10080
+      protocol: UDP
+  listenerFilters:
+  - name: envoy.filters.udp_listener.udp_proxy
+    typedConfig:
+      '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.UdpProxyConfig
+      matcher:
+        onNoMatch:
+          action:
+            name: route
+            typedConfig:
+              '@type': type.googleapis.com/envoy.extensions.filters.udp.udp_proxy.v3.Route
+              cluster: EmptyCluster
+      statPrefix: service
+  name: udp-route-with-no-routes
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.routes.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.routes.yaml
new file mode 100644
index 000000000..fe51488c7
--- /dev/null
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route-with-no-routes.routes.yaml
@@ -0,0 +1,1 @@
+[]
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 657a643f0..b59558dbe 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -3148,6 +3148,7 @@ _Appears in:_
 | `scopes` | _string array_ |  false  |  | The OIDC scopes to be used in the<br />[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).<br />The "openid" scope is always added to the list of scopes if not already<br />specified. |
 | `resources` | _string array_ |  false  |  | The OIDC resources to be used in the<br />[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). |
 | `redirectURL` | _string_ |  true  |  | The redirect URL to be used in the OIDC<br />[Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).<br />If not specified, uses the default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" |
+| `denyRedirect` | _[OIDCDenyRedirect](#oidcdenyredirect)_ |  false  |  | Any request that matches any of the provided matchers (with either tokens that are expired or missing tokens) will not be redirected to the OIDC Provider.<br />This behavior can be useful for AJAX or machine requests. |
 | `logoutPath` | _string_ |  true  |  | The path to log a user out, clearing their credential cookies.<br />If not specified, uses a default logout path "/logout" |
 | `forwardAccessToken` | _boolean_ |  false  |  | ForwardAccessToken indicates whether the Envoy should forward the access token<br />via the Authorization header Bearer scheme to the upstream.<br />If not specified, defaults to false. |
 | `defaultTokenTTL` | _[Duration](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#duration-v1-meta)_ |  false  |  | DefaultTokenTTL is the default lifetime of the id token and access token.<br />Please note that Envoy will always use the expiry time from the response<br />of the authorization server if it is provided. This field is only used when<br />the expiry time is not provided by the authorization.<br />If not specified, defaults to 0. In this case, the "expires_in" field in<br />the authorization response must be set by the authorization server, or the<br />OAuth flow will fail. |
diff --git a/site/content/en/latest/tasks/operations/egctl.md b/site/content/en/latest/tasks/operations/egctl.md
index 36c1f9979..8aa865a9b 100644
--- a/site/content/en/latest/tasks/operations/egctl.md
+++ b/site/content/en/latest/tasks/operations/egctl.md
@@ -4,6 +4,294 @@ title: "Use egctl"
 
 `egctl` is a command line tool to provide additional functionality for Envoy Gateway users.
 
+Follow the instructions [here](./../../install/install-egctl.md) to install `egctl`.
+
+## egctl config
+
+The `egctl config` subcommand provides tools for viewing and validating the Envoy Proxy and Envoy RateLimit configuration configured in Envoy Proxy.
+
+Here's an example of retrieving the [xDS Route Configuration](https://www.envoyproxy.io/docs/envoy/latest/api-v3/config/route/v3/route.proto#envoy-v3-api-msg-config-route-v3-routeconfiguration) from all the proxies. 
+
+```console
+~ egctl config envoy-proxy route
+{
+  "envoy-gateway-system": {
+    "envoy-default-eg-e41e7b31-6d749cdb85-hcfrk": {
+      "@type": "type.googleapis.com/envoy.admin.v3.RoutesConfigDump",
+      "dynamicRouteConfigs": [
+        {
+          "lastUpdated": "2025-05-22T17:51:06.237Z",
+          "routeConfig": {
+            "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+            "ignorePortInHostMatching": true,
+            "name": "default/eg/http",
+            "virtualHosts": [
+              {
+                "domains": [
+                  "www.example.com"
+                ],
+                "metadata": {
+                  "filterMetadata": {
+                    "envoy-gateway": {
+                      "resources": [
+                        {
+                          "kind": "Gateway",
+                          "name": "eg",
+                          "namespace": "default",
+                          "sectionName": "http"
+                        }
+                      ]
+                    }
+                  }
+                },
+                "name": "default/eg/http/www_example_com",
+                "routes": [
+                  {
+                    "match": {
+                      "prefix": "/"
+                    },
+                    "metadata": {
+                      "filterMetadata": {
+                        "envoy-gateway": {
+                          "resources": [
+                            {
+                              "kind": "HTTPRoute",
+                              "name": "backend",
+                              "namespace": "default"
+                            }
+                          ]
+                        }
+                      }
+                    },
+                    "name": "httproute/default/backend/rule/0/match/0/www_example_com",
+                    "route": {
+                      "cluster": "httproute/default/backend/rule/0",
+                      "upgradeConfigs": [
+                        {
+                          "upgradeType": "websocket"
+                        }
+                      ]
+                    }
+                  }
+                ]
+              }
+            ]
+          },
+          "versionInfo": "bc44981323d08bcb24abad6867942912675bb35bb674f2ee9ae866efbe1375dd"
+        }
+      ],
+      "staticRouteConfigs": [
+        {
+          "lastUpdated": "2025-05-22T17:51:06.198Z",
+          "routeConfig": {
+            "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+            "name": "local_route",
+            "virtualHosts": [
+              {
+                "domains": [
+                  "*"
+                ],
+                "name": "prometheus_stats",
+                "routes": [
+                  {
+                    "match": {
+                      "headers": [
+                        {
+                          "name": ":method",
+                          "stringMatch": {
+                            "exact": "GET"
+                          }
+                        }
+                      ],
+                      "path": "/stats/prometheus"
+                    },
+                    "route": {
+                      "cluster": "prometheus_stats"
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        },
+        {
+          "lastUpdated": "2025-05-22T17:51:06.219Z",
+          "routeConfig": {
+            "@type": "type.googleapis.com/envoy.config.route.v3.RouteConfiguration",
+            "name": "ready_route",
+            "virtualHosts": [
+              {
+                "domains": [
+                  "*"
+                ],
+                "name": "ready_route",
+                "routes": [
+                  {
+                    "directResponse": {
+                      "status": 500
+                    },
+                    "match": {
+                      "prefix": "/"
+                    }
+                  }
+                ]
+              }
+            ]
+          }
+        }
+      ]
+    }
+  }
+}
+```
+
+## egctl experimental status
+
+This subcommand allows users to show the summary of the status of specific or all resource types, in order to quickly find
+out the status of any resources.
+
+By default, `egctl x status` display all the conditions for one resource type. You can either add `--quiet` to only
+display the latest condition, or add `--verbose` to display more details about current status.
+
+{{% alert title="Note" color="primary" %}}
+
+The resource types that this subcommand currently supports:
+
+- `xRoute`, `HTTPRoute`, `GRPCRoute`, `TLSRoute`, `TCPRoute`, `UDPRoute`
+- `xPolicy`, `BackendTLSPolicy`, `BackendTrafficPolicy`, `ClientTrafficPolicy`, `EnvoyPatchPolicy`, `SecurityPolicy`
+- `all`, `GatewayClass`, `Gateway`
+
+{{% /alert %}}
+
+Some examples of this command after installing [Multi-tenancy][] example manifest:
+
+- Show the summary of GatewayClass.
+
+```console
+~ egctl x status gatewayclass
+
+NAME           TYPE       STATUS    REASON
+eg-marketing   Accepted   True      Accepted
+eg-product     Accepted   True      Accepted
+```
+
+- Show the summary of all resource types under all namespaces, the resource type with empty status will be ignored.
+
+```console
+~ egctl x status all -A
+
+NAME                        TYPE       STATUS    REASON
+gatewayclass/eg-marketing   Accepted   True      Accepted
+gatewayclass/eg-product     Accepted   True      Accepted
+
+NAMESPACE   NAME         TYPE         STATUS    REASON
+marketing   gateway/eg   Programmed   True      Programmed
+                         Accepted     True      Accepted
+product     gateway/eg   Programmed   True      Programmed
+                         Accepted     True      Accepted
+
+NAMESPACE   NAME                PARENT       TYPE           STATUS    REASON
+marketing   httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+                                             Accepted       True      Accepted
+product     httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+                                             Accepted       True      Accepted
+```
+
+- Show the summary of all the Gateways with details under all namespaces.
+
+```console
+~ egctl x status gateway --verbose --all-namespaces
+
+NAMESPACE   NAME      TYPE         STATUS    REASON       MESSAGE                                                                    OBSERVED GENERATION   LAST TRANSITION TIME
+marketing   eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
+                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:50:39 +0800 CST
+product     eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
+                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:52:42 +0800 CST
+```
+
+- Show the summary of the latest Gateways condition under `product` namespace.
+
+```console
+~ egctl x status gateway --quiet -n product
+
+NAME      TYPE         STATUS    REASON
+eg        Programmed   True      Programmed
+```
+
+- Show the summary of latest HTTPRoutes condition under all namespaces.
+
+```console
+~ egctl x status httproute --quiet --all-namespaces
+
+NAMESPACE   NAME      PARENT       TYPE           STATUS    REASON
+marketing   backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+product     backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
+```
+
+[Multi-tenancy]: ../deployment-mode#multi-tenancy
+[EnvoyProxy]: ../../../api/extension_types#envoyproxy
+
+## egctl experimental dashboard
+
+This subcommand streamlines the process for users to access the Envoy admin dashboard. By executing the following command:
+
+```bash
+egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
+```
+
+You will see the following output:
+
+```bash
+egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
+http://localhost:19000
+```
+
+the Envoy admin dashboard will automatically open in your default web browser. This eliminates the need to manually locate and expose the admin port.
+
+
+## egctl experimental install
+
+This subcommand can be used to install envoy-gateway.
+
+```bash
+egctl x install
+```
+
+By default, this will install both the envoy-gateway workload resource and the required gateway-api and envoy-gatewayCRDs.
+
+We can specify to install only workload resources via `--skip-crds`
+
+```bash
+egctl x install --skip-crds
+```
+
+We can specify to install only CRDs resources via `--only-crds`
+
+```bash
+egctl x install --only-crds
+```
+
+We can specify `--name` and `--namespace` to install envoy-gateway in different places to support multi-tenant mode.
+> Note: If CRDs are already installed, then we need to specify `--skip-crds` to avoid repeated installation of CRDs resources.
+
+```bash
+egctl x install --name shop-backend --namespace shop
+```
+
+## egctl experimental uninstall
+
+This subcommand can be used to uninstall envoy-gateway.
+
+```bash
+egctl x uninstall
+```
+
+By default, this will only uninstall the envoy-gateway workload resource, if we want to also uninstall CRDs, we need to specify `--with-crds`
+
+```bash
+egctl x uninstall --with-crds
+```
+
 ## egctl experimental translate
 
 This subcommand allows users to translate from an input configuration type to an output configuration type.
@@ -755,152 +1043,3 @@ xds:
   envoy-gateway-system-eg:
     '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump
 ```
-
-## egctl experimental status
-
-This subcommand allows users to show the summary of the status of specific or all resource types, in order to quickly find
-out the status of any resources.
-
-By default, `egctl x status` display all the conditions for one resource type. You can either add `--quiet` to only
-display the latest condition, or add `--verbose` to display more details about current status.
-
-{{% alert title="Note" color="primary" %}}
-
-The resource types that this subcommand currently supports:
-
-- `xRoute`, `HTTPRoute`, `GRPCRoute`, `TLSRoute`, `TCPRoute`, `UDPRoute`
-- `xPolicy`, `BackendTLSPolicy`, `BackendTrafficPolicy`, `ClientTrafficPolicy`, `EnvoyPatchPolicy`, `SecurityPolicy`
-- `all`, `GatewayClass`, `Gateway`
-
-{{% /alert %}}
-
-Some examples of this command after installing [Multi-tenancy][] example manifest:
-
-- Show the summary of GatewayClass.
-
-```console
-~ egctl x status gatewayclass
-
-NAME           TYPE       STATUS    REASON
-eg-marketing   Accepted   True      Accepted
-eg-product     Accepted   True      Accepted
-```
-
-- Show the summary of all resource types under all namespaces, the resource type with empty status will be ignored.
-
-```console
-~ egctl x status all -A
-
-NAME                        TYPE       STATUS    REASON
-gatewayclass/eg-marketing   Accepted   True      Accepted
-gatewayclass/eg-product     Accepted   True      Accepted
-
-NAMESPACE   NAME         TYPE         STATUS    REASON
-marketing   gateway/eg   Programmed   True      Programmed
-                         Accepted     True      Accepted
-product     gateway/eg   Programmed   True      Programmed
-                         Accepted     True      Accepted
-
-NAMESPACE   NAME                PARENT       TYPE           STATUS    REASON
-marketing   httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-                                             Accepted       True      Accepted
-product     httproute/backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-                                             Accepted       True      Accepted
-```
-
-- Show the summary of all the Gateways with details under all namespaces.
-
-```console
-~ egctl x status gateway --verbose --all-namespaces
-
-NAMESPACE   NAME      TYPE         STATUS    REASON       MESSAGE                                                                    OBSERVED GENERATION   LAST TRANSITION TIME
-marketing   eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
-                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:50:39 +0800 CST
-product     eg        Programmed   True      Programmed   Address assigned to the Gateway, 1/1 envoy Deployment replicas available   1                     2024-02-02 18:17:14 +0800 CST
-                      Accepted     True      Accepted     The Gateway has been scheduled by Envoy Gateway                            1                     2024-02-01 17:52:42 +0800 CST
-```
-
-- Show the summary of the latest Gateways condition under `product` namespace.
-
-```console
-~ egctl x status gateway --quiet -n product
-
-NAME      TYPE         STATUS    REASON
-eg        Programmed   True      Programmed
-```
-
-- Show the summary of latest HTTPRoutes condition under all namespaces.
-
-```console
-~ egctl x status httproute --quiet --all-namespaces
-
-NAMESPACE   NAME      PARENT       TYPE           STATUS    REASON
-marketing   backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-product     backend   gateway/eg   ResolvedRefs   True      ResolvedRefs
-```
-
-[Multi-tenancy]: ../deployment-mode#multi-tenancy
-[EnvoyProxy]: ../../../api/extension_types#envoyproxy
-
-
-## egctl experimental dashboard
-
-This subcommand streamlines the process for users to access the Envoy admin dashboard. By executing the following command:
-
-```bash
-egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
-```
-
-You will see the following output:
-
-```bash
-egctl x dashboard envoy-proxy -n envoy-gateway-system envoy-engw-eg-a9c23fbb-558f94486c-82wh4
-http://localhost:19000
-```
-
-the Envoy admin dashboard will automatically open in your default web browser. This eliminates the need to manually locate and expose the admin port.
-
-
-## egctl experimental install
-
-This subcommand can be used to install envoy-gateway.
-
-```bash
-egctl x install
-```
-
-By default, this will install both the envoy-gateway workload resource and the required gateway-api and envoy-gatewayCRDs.
-
-We can specify to install only workload resources via `--skip-crds`
-
-```bash
-egctl x install --skip-crds
-```
-
-We can specify to install only CRDs resources via `--only-crds`
-
-```bash
-egctl x install --only-crds
-```
-
-We can specify `--name` and `--namespace` to install envoy-gateway in different places to support multi-tenant mode.
-> Note: If CRDs are already installed, then we need to specify `--skip-crds` to avoid repeated installation of CRDs resources.
-
-```bash
-egctl x install --name shop-backend --namespace shop
-```
-
-
-## egctl experimental uninstall
-
-This subcommand can be used to uninstall envoy-gateway.
-
-```bash
-egctl x uninstall
-```
-
-By default, this will only uninstall the envoy-gateway workload resource, if we want to also uninstall CRDs, we need to specify `--with-crds`
-
-```bash
-egctl x uninstall --with-crds
-```
\ No newline at end of file
diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go
index de792dddd..1016bbfff 100644
--- a/test/e2e/e2e_test.go
+++ b/test/e2e/e2e_test.go
@@ -61,6 +61,15 @@ func TestE2E(t *testing.T) {
 		)
 	}
 
+	// TODO: make these tests work in GatewayNamespaceMode
+	if tests.IsGatewayNamespaceMode() {
+		skipTests = append(skipTests,
+			tests.HTTPWasmTest.ShortName,
+			tests.OCIWasmTest.ShortName,
+			tests.ZoneAwareRoutingTest.ShortName,
+		)
+	}
+
 	cSuite, err := suite.NewConformanceTestSuite(suite.ConformanceOptions{
 		Client:               c,
 		RestConfig:           cfg,
diff --git a/test/e2e/testdata/envoyproxy-daemonset.yaml b/test/e2e/testdata/envoyproxy-daemonset.yaml
index a7f1e4509..0338b3417 100644
--- a/test/e2e/testdata/envoyproxy-daemonset.yaml
+++ b/test/e2e/testdata/envoyproxy-daemonset.yaml
@@ -1,7 +1,7 @@
 apiVersion: gateway.networking.k8s.io/v1
 kind: Gateway
 metadata:
-  name: eg-daemonset
+  name: eg-ds
   namespace: gateway-conformance-infra
 spec:
   gatewayClassName: "{GATEWAY_CLASS_NAME}"
@@ -16,12 +16,12 @@ spec:
     parametersRef:
       group: gateway.envoyproxy.io
       kind: EnvoyProxy
-      name: eg-daemonset
+      name: eg-ds
 ---
 apiVersion: gateway.envoyproxy.io/v1alpha1
 kind: EnvoyProxy
 metadata:
-  name: eg-daemonset
+  name: eg-ds
   namespace: gateway-conformance-infra
 spec:
   ipFamily: IPv4
@@ -43,11 +43,11 @@ spec:
 apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
-  name: foo-route
+  name: daemonset-route
   namespace: gateway-conformance-infra
 spec:
   parentRefs:
-    - name: eg-daemonset
+    - name: eg-ds
   rules:
     - backendRefs:
         - name: infra-backend-v1
@@ -55,4 +55,4 @@ spec:
       matches:
         - path:
             type: PathPrefix
-            value: /foo
+            value: /daemonset
diff --git a/test/e2e/testdata/ratelimit-global-shared-and-unshared-header-match.yaml b/test/e2e/testdata/ratelimit-global-shared-and-unshared-header-match.yaml
index 639bbd725..c51ef1fac 100644
--- a/test/e2e/testdata/ratelimit-global-shared-and-unshared-header-match.yaml
+++ b/test/e2e/testdata/ratelimit-global-shared-and-unshared-header-match.yaml
@@ -93,6 +93,10 @@ spec:
     type: Global
     global:
       rules:
+        - limit:
+            requests: 21
+            unit: Hour
+          shared: true
         - clientSelectors:
             - headers:
                 - name: x-user-id
diff --git a/test/e2e/tests/envoyproxy_daemonset.go b/test/e2e/tests/envoyproxy_daemonset.go
index 25c5253d5..d3ee87d61 100644
--- a/test/e2e/tests/envoyproxy_daemonset.go
+++ b/test/e2e/tests/envoyproxy_daemonset.go
@@ -13,6 +13,7 @@ import (
 	"time"
 
 	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
@@ -34,31 +35,30 @@ var EnvoyProxyDaemonSetTest = suite.ConformanceTest{
 	Manifests:   []string{"testdata/envoyproxy-daemonset.yaml"},
 	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
 		t.Run("RunAndDelete", func(t *testing.T) {
+			t.Cleanup(func() {
+				if t.Failed() {
+					CollectAndDump(t, suite.RestConfig)
+				}
+			})
+
 			ns := "gateway-conformance-infra"
-			routeNN := types.NamespacedName{Name: "foo-route", Namespace: ns}
-			gwNN := types.NamespacedName{Name: "eg-daemonset", Namespace: ns}
+			routeNN := types.NamespacedName{Name: "daemonset-route", Namespace: ns}
+			gwNN := types.NamespacedName{Name: "eg-ds", Namespace: ns}
 			gwAddr := kubernetes.GatewayAndHTTPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, kubernetes.NewGatewayRef(gwNN), routeNN)
-			OkResp := http.ExpectedResponse{
-				Request: http.Request{
-					Path: "/foo",
-				},
-				Response: http.Response{
-					StatusCode: 200,
-				},
-				Namespace: ns,
-			}
 
+			gwPodNamespace := GetGatewayResourceNamespace()
+			gwPodSelector := map[string]string{
+				"app.kubernetes.io/managed-by":                   "envoy-gateway",
+				"app.kubernetes.io/name":                         "envoy",
+				"gateway.envoyproxy.io/owning-gateway-name":      gwNN.Name,
+				"gateway.envoyproxy.io/owning-gateway-namespace": gwNN.Namespace,
+			}
 			// Make sure there's no deployment for the gateway
 			err := wait.PollUntilContextTimeout(context.TODO(), time.Second, suite.TimeoutConfig.DeleteTimeout, true, func(ctx context.Context) (bool, error) {
 				deploys := &appsv1.DeploymentList{}
 				err := suite.Client.List(ctx, deploys, &client.ListOptions{
-					Namespace: "envoy-gateway-system",
-					LabelSelector: labels.SelectorFromSet(map[string]string{
-						"app.kubernetes.io/managed-by":                   "envoy-gateway",
-						"app.kubernetes.io/name":                         "envoy",
-						"gateway.envoyproxy.io/owning-gateway-name":      gwNN.Name,
-						"gateway.envoyproxy.io/owning-gateway-namespace": gwNN.Namespace,
-					}),
+					Namespace:     gwPodNamespace,
+					LabelSelector: labels.SelectorFromSet(gwPodSelector),
 				})
 				if err != nil {
 					return false, err
@@ -70,8 +70,18 @@ var EnvoyProxyDaemonSetTest = suite.ConformanceTest{
 				t.Fatalf("Failed to check no deployments for the Gateway: %v", err)
 			}
 
+			WaitForPods(t, suite.Client, gwPodNamespace, gwPodSelector, corev1.PodRunning, PodReady)
+
 			// Send a request to a valid path and expect a successful response
-			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, OkResp)
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr, http.ExpectedResponse{
+				Request: http.Request{
+					Path: "/daemonset",
+				},
+				Response: http.Response{
+					StatusCode: 200,
+				},
+				Namespace: ns,
+			})
 
 			// Delete the Gateway and wait for the DaemonSet to be deleted
 			gtw := &gwapiv1.Gateway{
@@ -87,22 +97,17 @@ var EnvoyProxyDaemonSetTest = suite.ConformanceTest{
 			err = wait.PollUntilContextTimeout(context.TODO(), time.Second, suite.TimeoutConfig.DeleteTimeout, true, func(ctx context.Context) (bool, error) {
 				dsList := &appsv1.DaemonSetList{}
 				err := suite.Client.List(ctx, dsList, &client.ListOptions{
-					Namespace: "envoy-gateway-system",
-					LabelSelector: labels.SelectorFromSet(map[string]string{
-						"app.kubernetes.io/managed-by":                   "envoy-gateway",
-						"app.kubernetes.io/name":                         "envoy",
-						"gateway.envoyproxy.io/owning-gateway-name":      gwNN.Name,
-						"gateway.envoyproxy.io/owning-gateway-namespace": gwNN.Namespace,
-					}),
+					Namespace:     gwPodNamespace,
+					LabelSelector: labels.SelectorFromSet(gwPodSelector),
 				})
 				if err != nil {
 					return false, err
 				}
 
-				return len(dsList.Items) == 0, err
+				return len(dsList.Items) == 0, nil
 			})
 			if err != nil {
-				t.Fatalf("Failed to delete Gateway: %v", err)
+				t.Fatalf("Failed to delete DaemonSet Gateway: %v", err)
 			}
 		})
 	},
diff --git a/test/e2e/tests/ratelimit.go b/test/e2e/tests/ratelimit.go
index b3522af68..1a96198aa 100644
--- a/test/e2e/tests/ratelimit.go
+++ b/test/e2e/tests/ratelimit.go
@@ -965,34 +965,54 @@ var RateLimitGlobalMergeTest = suite.ConformanceTest{
 
 			expectOk1 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
 			expectOk2 := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
-			expectOk3 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
 			expectLimit := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
 
-			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr2, expectOk1)
+			require.Eventually(t, func() bool {
+				_, cRes, err := suite.RoundTripper.CaptureRoundTrip(http.MakeRequest(t, &expectOk1, gwAddr2, "HTTP", "http"))
+				if err != nil {
+					return false
+				}
+				vals := cRes.Headers["X-Ratelimit-Limit"]
+				return len(vals) > 0 && vals[0] == "3, 3;w=3600"
+			}, suite.TimeoutConfig.MaxTimeToConsistency, suite.TimeoutConfig.RequestTimeout, "rate limit header not yet present")
 
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expectOk1, gwAddr2, "HTTP", "http"), expectOk1)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expectOk2, gwAddr1, "HTTP", "http"), expectOk2)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expectOk3, gwAddr2, "HTTP", "http"), expectOk3)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expectLimit, gwAddr1, "HTTP", "http"), expectLimit)
+			for _, expect := range []http.ExpectedResponse{expectOk1, expectOk2} {
+				if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expect, gwAddr2, "HTTP", "http"), expect); err != nil {
+					t.Errorf("expected 200 response: %v", err)
+				}
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expectLimit, gwAddr1, "HTTP", "http"), expectLimit); err != nil {
+				t.Errorf("expected 429 response: %v", err)
+			}
 		})
 
 		t.Run("unshared_route_policy_x-user-id=two", func(t *testing.T) {
 			headers := map[string]string{"x-user-id": "two"}
 
-			// Route 1 (/foo)
-			ok1 := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
-			limit1 := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
-			// Route 2 (/bar)
-			ok2 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
-			limit2 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
+			okFoo := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
+			limitFoo := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
+			okBar := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
+			limitBar := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
 
-			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr1, ok1)
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr1, okFoo)
 
-			_ = GotExactExpectedResponse(t, 3, suite.RoundTripper, http.MakeRequest(t, &ok1, gwAddr1, "HTTP", "http"), ok1)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit1, gwAddr1, "HTTP", "http"), limit1)
+			for i := 0; i < 2; i++ {
+				if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &okFoo, gwAddr1, "HTTP", "http"), okFoo); err != nil {
+					t.Errorf("foo request #%d failed: %v", i+1, err)
+				}
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limitFoo, gwAddr1, "HTTP", "http"), limitFoo); err != nil {
+				t.Errorf("expected 429 on 4th foo: %v", err)
+			}
 
-			_ = GotExactExpectedResponse(t, 3, suite.RoundTripper, http.MakeRequest(t, &ok2, gwAddr2, "HTTP", "http"), ok2)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit2, gwAddr2, "HTTP", "http"), limit2)
+			for i := 0; i < 3; i++ {
+				if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &okBar, gwAddr2, "HTTP", "http"), okBar); err != nil {
+					t.Errorf("bar request #%d failed: %v", i+1, err)
+				}
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limitBar, gwAddr2, "HTTP", "http"), limitBar); err != nil {
+				t.Errorf("expected 429 on 4th bar: %v", err)
+			}
 		})
 
 		t.Run("shared_gateway_policy_x-user-id=three", func(t *testing.T) {
@@ -1000,32 +1020,58 @@ var RateLimitGlobalMergeTest = suite.ConformanceTest{
 
 			ok1 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
 			ok2 := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
-			ok3 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
 			limit := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
 
 			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr2, ok1)
 
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &ok1, gwAddr2, "HTTP", "http"), ok1)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &ok2, gwAddr1, "HTTP", "http"), ok2)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &ok3, gwAddr2, "HTTP", "http"), ok3)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit, gwAddr1, "HTTP", "http"), limit)
+			for _, expect := range []http.ExpectedResponse{ok1, ok2} {
+				if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &expect, gwAddr2, "HTTP", "http"), expect); err != nil {
+					t.Errorf("expected 200 response: %v", err)
+				}
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit, gwAddr1, "HTTP", "http"), limit); err != nil {
+				t.Errorf("expected 429 response: %v", err)
+			}
 		})
 
 		t.Run("unshared_gateway_policy__x-user-id=four", func(t *testing.T) {
 			headers := map[string]string{"x-user-id": "four"}
 
-			ok1 := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
-			limit1 := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
-			ok2 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
-			limit2 := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
+			okFoo := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
+			limitFoo := http.ExpectedResponse{Request: http.Request{Path: "/foo", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
+			okBar := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 200}, Namespace: ns}
+			limitBar := http.ExpectedResponse{Request: http.Request{Path: "/bar", Headers: headers}, Response: http.Response{StatusCode: 429}, Namespace: ns}
 
-			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr1, ok1)
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr1, okFoo)
+
+			for i := 0; i < 2; i++ {
+				if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &okFoo, gwAddr1, "HTTP", "http"), okFoo); err != nil {
+					t.Errorf("foo request #%d failed: %v", i+1, err)
+				}
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limitFoo, gwAddr1, "HTTP", "http"), limitFoo); err != nil {
+				t.Errorf("expected 429 on 4th foo: %v", err)
+			}
+
+			for i := 0; i < 3; i++ {
+				if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &okBar, gwAddr2, "HTTP", "http"), okBar); err != nil {
+					t.Errorf("bar request #%d failed: %v", i+1, err)
+				}
+			}
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limitBar, gwAddr2, "HTTP", "http"), limitBar); err != nil {
+				t.Errorf("expected 429 on 4th bar: %v", err)
+			}
+		})
 
-			_ = GotExactExpectedResponse(t, 3, suite.RoundTripper, http.MakeRequest(t, &ok1, gwAddr1, "HTTP", "http"), ok1)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit1, gwAddr1, "HTTP", "http"), limit1)
+		t.Run("shared_no_client_selectors", func(t *testing.T) {
+			ok1 := http.ExpectedResponse{Request: http.Request{Path: "/bar"}, Response: http.Response{StatusCode: 200}, Namespace: ns}
+			limit := http.ExpectedResponse{Request: http.Request{Path: "/bar"}, Response: http.Response{StatusCode: 429}, Namespace: ns}
 
-			_ = GotExactExpectedResponse(t, 3, suite.RoundTripper, http.MakeRequest(t, &ok2, gwAddr2, "HTTP", "http"), ok2)
-			_ = GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit2, gwAddr2, "HTTP", "http"), limit2)
+			http.MakeRequestAndExpectEventuallyConsistentResponse(t, suite.RoundTripper, suite.TimeoutConfig, gwAddr1, ok1)
+
+			if err := GotExactExpectedResponse(t, 1, suite.RoundTripper, http.MakeRequest(t, &limit, gwAddr1, "HTTP", "http"), limit); err != nil {
+				t.Errorf("expected 429 for third request: %v", err)
+			}
 		})
 	},
 }
diff --git a/test/e2e/tests/utils.go b/test/e2e/tests/utils.go
index 1a628a215..7f6fb996b 100644
--- a/test/e2e/tests/utils.go
+++ b/test/e2e/tests/utils.go
@@ -602,7 +602,15 @@ type LokiQueryResponse struct {
 // CollectAndDump collects and dumps the cluster data for troubleshooting and log.
 // This function should be call within t.Cleanup.
 func CollectAndDump(t *testing.T, rest *rest.Config) {
-	result := tb.CollectResult(context.TODO(), rest, "", "envoy-gateway-system")
+	dumpedNamespaces := []string{"envoy-gateway-system"}
+	if IsGatewayNamespaceMode() {
+		dumpedNamespaces = append(dumpedNamespaces, ConformanceInfraNamespace)
+	}
+
+	result := tb.CollectResult(context.TODO(), rest, tb.CollectOptions{
+		BundlePath:          "",
+		CollectedNamespaces: dumpedNamespaces,
+	})
 	for r, data := range result {
 		tlog.Logf(t, "\nfilename: %s", r)
 		tlog.Logf(t, "\ndata: \n%s", data)
diff --git a/test/helm/gateway-crds-helm/all.out.yaml b/test/helm/gateway-crds-helm/all.out.yaml
index 15562aa62..111971c6b 100644
--- a/test/helm/gateway-crds-helm/all.out.yaml
+++ b/test/helm/gateway-crds-helm/all.out.yaml
@@ -42110,6 +42110,8 @@ spec:
                           - name
                           - value
                           type: object
+                        maxItems: 16
+                        minItems: 1
                         type: array
                     required:
                     - headers
diff --git a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
index 98ce60dc8..1ba17b7fb 100644
--- a/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
+++ b/test/helm/gateway-crds-helm/envoy-gateway-crds.out.yaml
@@ -24798,6 +24798,8 @@ spec:
                           - name
                           - value
                           type: object
+                        maxItems: 16
+                        minItems: 1
                         type: array
                     required:
                     - headers