Repository: envoyproxy/gateway
Pull request #2140 (2023-10-31 15:39:54)

Add E2E Tests for UDPRoute
**What type of PR is this?**
<!--
Your PR title should be descriptive, and generally start with type that contains a subsystem name with `()` if necessary 
and summary followed by a colon. format `chore/docs/feat/fix/refactor/style/test: summary`.
Examples:
* "docs: fix grammar error"
* "feat(translator): add new feature"
* "fix: fix xx bug"
* "chore: change ci & build tools etc"
-->

**What this PR does / why we need it**:

**Which issue(s) this PR fixes**:
<!--
*Automatically closes linked issue when PR is merged.
Usage: `Fixes #<issue number>`, or `Fixes (paste link of issue)`.
-->
Fixes https://github.com/envoyproxy/gateway/issues/2011


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/envoyproxy/gateway/pull/2140?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) Report
> Merging [#2140](https://app.codecov.io/gh/envoyproxy/gateway/pull/2140?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) (93ef3cd) into [main](https://app.codecov.io/gh/envoyproxy/gateway/commit/71c09f285fcc88ddaf234a7c3864263d8a5bbbd0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy) (71c09f2) will **decrease** coverage by `0.02%`.
> The diff coverage is `n/a`.

```diff
@@            Coverage Diff             @@
##             main    #2140      +/-   ##
==========================================
- Coverage   64.25%   64.23%   -0.02%     
==========================================
  Files         107      107              
  Lines       14628    14628              
==========================================
- Hits         9399     9397       -2     
- Misses       4664     4666       +2     
  Partials      565      565              
```


[see 1 file with indirect coverage changes](https://app.codecov.io/gh/envoyproxy/gateway/pull/2140/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=envoyproxy)



Patch:
diff --git a/api/v1alpha1/accesslogging_types.go b/api/v1alpha1/accesslogging_types.go
index ba877c9fc..483a224fa 100644
--- a/api/v1alpha1/accesslogging_types.go
+++ b/api/v1alpha1/accesslogging_types.go
@@ -35,6 +35,9 @@ const (
 // ProxyAccessLogFormat defines the format of accesslog.
 // By default accesslogs are written to standard output.
 // +union
+//
+// +kubebuilder:validation:XValidation:rule="self.type == 'Text' ? has(self.text) : !has(self.text)",message="If AccessLogFormat type is Text, text field needs to be set."
+// +kubebuilder:validation:XValidation:rule="self.type == 'JSON' ? has(self.json) : !has(self.json)",message="If AccessLogFormat type is JSON, json field needs to be set."
 type ProxyAccessLogFormat struct {
 	// Type defines the type of accesslog format.
 	// +kubebuilder:validation:Enum=Text;JSON
@@ -65,9 +68,15 @@ const (
 	ProxyAccessLogSinkTypeOpenTelemetry ProxyAccessLogSinkType = "OpenTelemetry"
 )
 
+// ProxyAccessLogSink defines the sink of accesslog.
+// +union
+//
+// +kubebuilder:validation:XValidation:rule="self.type == 'File' ? has(self.file) : !has(self.file)",message="If AccessLogSink type is File, file field needs to be set."
+// +kubebuilder:validation:XValidation:rule="self.type == 'OpenTelemetry' ? has(self.openTelemetry) : !has(self.openTelemetry)",message="If AccessLogSink type is OpenTelemetry, openTelemetry field needs to be set."
 type ProxyAccessLogSink struct {
 	// Type defines the type of accesslog sink.
 	// +kubebuilder:validation:Enum=File;OpenTelemetry
+	// +unionDiscriminator
 	Type ProxyAccessLogSinkType `json:"type,omitempty"`
 	// File defines the file accesslog sink.
 	// +optional
diff --git a/api/v1alpha1/envoygateway_types.go b/api/v1alpha1/envoygateway_types.go
index 88357b12e..d479d3f7f 100644
--- a/api/v1alpha1/envoygateway_types.go
+++ b/api/v1alpha1/envoygateway_types.go
@@ -192,7 +192,8 @@ type EnvoyGatewayKubernetesProvider struct {
 	// +optional
 	Deploy *KubernetesDeployMode `json:"deploy,omitempty"`
 	// OverwriteControlPlaneCerts updates the secrets containing the control plane certs, when set.
-	OverwriteControlPlaneCerts bool `json:"overwrite_control_plane_certs,omitempty"`
+	// +optional
+	OverwriteControlPlaneCerts *bool `json:"overwriteControlPlaneCerts,omitempty"`
 }
 
 const (
diff --git a/api/v1alpha1/envoyproxy_metric_types.go b/api/v1alpha1/envoyproxy_metric_types.go
index 2dce4655e..71fb42ca6 100644
--- a/api/v1alpha1/envoyproxy_metric_types.go
+++ b/api/v1alpha1/envoyproxy_metric_types.go
@@ -29,14 +29,21 @@ type ProxyMetrics struct {
 	EnableVirtualHostStats bool `json:"enableVirtualHostStats,omitempty"`
 }
 
+// ProxyMetricSink defines the sink of metrics.
+// Default metrics sink is OpenTelemetry.
+// +union
+//
+// +kubebuilder:validation:XValidation:rule="self.type == 'OpenTelemetry' ? has(self.openTelemetry) : !has(self.openTelemetry)",message="If MetricSink type is OpenTelemetry, openTelemetry field needs to be set."
 type ProxyMetricSink struct {
 	// Type defines the metric sink type.
 	// EG currently only supports OpenTelemetry.
 	// +kubebuilder:validation:Enum=OpenTelemetry
 	// +kubebuilder:default=OpenTelemetry
+	// +unionDiscriminator
 	Type MetricSinkType `json:"type"`
 	// OpenTelemetry defines the configuration for OpenTelemetry sink.
 	// It's required if the sink type is OpenTelemetry.
+	// +optional
 	OpenTelemetry *ProxyOpenTelemetrySink `json:"openTelemetry,omitempty"`
 }
 
diff --git a/api/v1alpha1/groupversion_info.go b/api/v1alpha1/groupversion_info.go
index 1b13b602e..be4c68a53 100644
--- a/api/v1alpha1/groupversion_info.go
+++ b/api/v1alpha1/groupversion_info.go
@@ -10,9 +10,12 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/scheme"
 )
 
+const GroupName = "gateway.envoyproxy.io"
+
 var (
+
 	// GroupVersion is group version used to register these objects
-	GroupVersion = schema.GroupVersion{Group: "gateway.envoyproxy.io", Version: "v1alpha1"}
+	GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha1"}
 
 	// SchemeBuilder is used to add go types to the GroupVersionKind scheme
 	SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion}
diff --git a/api/v1alpha1/oidc_types.go b/api/v1alpha1/oidc_types.go
new file mode 100644
index 000000000..f690cfd7c
--- /dev/null
+++ b/api/v1alpha1/oidc_types.go
@@ -0,0 +1,74 @@
+// 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 v1alpha1
+
+import (
+	gwapiv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
+)
+
+const OIDCClientSecretKey = "client-secret"
+
+// OIDC defines the configuration for the OpenID Connect (OIDC) authentication.
+type OIDC struct {
+	// The OIDC Provider configuration.
+	Provider OIDCProvider `json:"provider"`
+
+	// The client ID to be used in the OIDC
+	// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+	//
+	// +kubebuilder:validation:MinLength=1
+	ClientID string `json:"clientID"`
+
+	// The Kubernetes secret which contains the OIDC client secret to be used in the
+	// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+	//
+	// This is an Opaque secret. The client secret should be stored in the key
+	// "client-secret".
+	// +kubebuilder:validation:Required
+	ClientSecret gwapiv1b1.SecretObjectReference `json:"clientSecret"`
+
+	// The OIDC scopes to be used in the
+	// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+	// The "openid" scope is always added to the list of scopes if not already
+	// specified.
+	// +optional
+	Scopes []string `json:"scopes,omitempty"`
+}
+
+// OIDCProvider defines the OIDC Provider configuration.
+// To make the EG OIDC config easy to use, some of the low-level ouath2 filter
+// configuration knobs are hidden from the user, and default values will be provided
+// when translating to XDS. For example:
+//
+// * redirect_uri: uses a default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback"
+//
+// * signout_path: uses a default signout path "/signout"
+//
+// * redirect_path_matcher: uses a default redirect path matcher "/oauth2/callback"
+//
+// If we get requests to expose these knobs, we can always do so later.
+type OIDCProvider struct {
+	// The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery).
+	// Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST
+	// be https, a host component, and optionally, port and path components and
+	// no query or fragment components.
+	// +kubebuilder:validation:MinLength=1
+	Issuer string `json:"issuer"`
+
+	// TODO zhaohuabing validate the issuer
+
+	// The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint).
+	// If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse).
+	//
+	// +optional
+	AuthorizationEndpoint *string `json:"authorizationEndpoint,omitempty"`
+
+	// The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint).
+	// If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse).
+	//
+	// +optional
+	TokenEndpoint *string `json:"tokenEndpoint,omitempty"`
+}
diff --git a/api/v1alpha1/securitypolicy_types.go b/api/v1alpha1/securitypolicy_types.go
index e5c608820..3d4efcebb 100644
--- a/api/v1alpha1/securitypolicy_types.go
+++ b/api/v1alpha1/securitypolicy_types.go
@@ -56,6 +56,11 @@ type SecurityPolicySpec struct {
 	//
 	// +optional
 	JWT *JWT `json:"jwt,omitempty"`
+
+	// OIDC defines the configuration for the OpenID Connect (OIDC) authentication.
+	//
+	// +optional
+	OIDC *OIDC `json:"oidc,omitempty"`
 }
 
 // SecurityPolicyStatus defines the state of SecurityPolicy
diff --git a/api/v1alpha1/shared_types.go b/api/v1alpha1/shared_types.go
index 532d4001b..1764558d7 100644
--- a/api/v1alpha1/shared_types.go
+++ b/api/v1alpha1/shared_types.go
@@ -260,16 +260,16 @@ type StringMatch struct {
 type StringMatchType string
 
 const (
-	// MatchExact :the input string must match exactly the match value.
+	// StringMatchExact :the input string must match exactly the match value.
 	StringMatchExact StringMatchType = "Exact"
 
-	// MatchPrefix :the input string must start with the match value.
+	// StringMatchPrefix :the input string must start with the match value.
 	StringMatchPrefix StringMatchType = "Prefix"
 
-	// MatchSuffix :the input string must end with the match value.
+	// StringMatchSuffix :the input string must end with the match value.
 	StringMatchSuffix StringMatchType = "Suffix"
 
-	// MatchRegularExpression :The input string must match the regular expression
+	// StringMatchRegularExpression :The input string must match the regular expression
 	// specified in the match value.
 	// The regex string must adhere to the syntax documented in
 	// https://github.com/google/re2/wiki/Syntax.
diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go
index 60ad334e9..0f9464200 100644
--- a/api/v1alpha1/zz_generated.deepcopy.go
+++ b/api/v1alpha1/zz_generated.deepcopy.go
@@ -498,6 +498,11 @@ func (in *EnvoyGatewayKubernetesProvider) DeepCopyInto(out *EnvoyGatewayKubernet
 		*out = new(KubernetesDeployMode)
 		**out = **in
 	}
+	if in.OverwriteControlPlaneCerts != nil {
+		in, out := &in.OverwriteControlPlaneCerts, &out.OverwriteControlPlaneCerts
+		*out = new(bool)
+		**out = **in
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new EnvoyGatewayKubernetesProvider.
@@ -1522,6 +1527,53 @@ func (in *LoadBalancer) DeepCopy() *LoadBalancer {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OIDC) DeepCopyInto(out *OIDC) {
+	*out = *in
+	in.Provider.DeepCopyInto(&out.Provider)
+	in.ClientSecret.DeepCopyInto(&out.ClientSecret)
+	if in.Scopes != nil {
+		in, out := &in.Scopes, &out.Scopes
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC.
+func (in *OIDC) DeepCopy() *OIDC {
+	if in == nil {
+		return nil
+	}
+	out := new(OIDC)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OIDCProvider) DeepCopyInto(out *OIDCProvider) {
+	*out = *in
+	if in.AuthorizationEndpoint != nil {
+		in, out := &in.AuthorizationEndpoint, &out.AuthorizationEndpoint
+		*out = new(string)
+		**out = **in
+	}
+	if in.TokenEndpoint != nil {
+		in, out := &in.TokenEndpoint, &out.TokenEndpoint
+		*out = new(string)
+		**out = **in
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDCProvider.
+func (in *OIDCProvider) DeepCopy() *OIDCProvider {
+	if in == nil {
+		return nil
+	}
+	out := new(OIDCProvider)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OpenTelemetryEnvoyProxyAccessLog) DeepCopyInto(out *OpenTelemetryEnvoyProxyAccessLog) {
 	*out = *in
@@ -2099,6 +2151,11 @@ func (in *SecurityPolicySpec) DeepCopyInto(out *SecurityPolicySpec) {
 		*out = new(JWT)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.OIDC != nil {
+		in, out := &in.OIDC, &out.OIDC
+		*out = new(OIDC)
+		(*in).DeepCopyInto(*out)
+	}
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecurityPolicySpec.
diff --git a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
index 17f135598..2b9373716 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_envoyproxies.yaml
@@ -5350,9 +5350,18 @@ spec:
                                   - JSON
                                   type: string
                               type: object
+                              x-kubernetes-validations:
+                              - message: If AccessLogFormat type is Text, text field
+                                  needs to be set.
+                                rule: 'self.type == ''Text'' ? has(self.text) : !has(self.text)'
+                              - message: If AccessLogFormat type is JSON, json field
+                                  needs to be set.
+                                rule: 'self.type == ''JSON'' ? has(self.json) : !has(self.json)'
                             sinks:
                               description: Sinks defines the sinks of accesslog.
                               items:
+                                description: ProxyAccessLogSink defines the sink of
+                                  accesslog.
                                 properties:
                                   file:
                                     description: File defines the file accesslog sink.
@@ -5397,6 +5406,15 @@ spec:
                                     - OpenTelemetry
                                     type: string
                                 type: object
+                                x-kubernetes-validations:
+                                - message: If AccessLogSink type is File, file field
+                                    needs to be set.
+                                  rule: 'self.type == ''File'' ? has(self.file) :
+                                    !has(self.file)'
+                                - message: If AccessLogSink type is OpenTelemetry,
+                                    openTelemetry field needs to be set.
+                                  rule: 'self.type == ''OpenTelemetry'' ? has(self.openTelemetry)
+                                    : !has(self.openTelemetry)'
                               minItems: 1
                               type: array
                           required:
@@ -5459,6 +5477,8 @@ spec:
                         description: Sinks defines the metric sinks where metrics
                           are sent to.
                         items:
+                          description: ProxyMetricSink defines the sink of metrics.
+                            Default metrics sink is OpenTelemetry.
                           properties:
                             openTelemetry:
                               description: OpenTelemetry defines the configuration
@@ -5489,6 +5509,11 @@ spec:
                           required:
                           - type
                           type: object
+                          x-kubernetes-validations:
+                          - message: If MetricSink type is OpenTelemetry, openTelemetry
+                              field needs to be set.
+                            rule: 'self.type == ''OpenTelemetry'' ? has(self.openTelemetry)
+                              : !has(self.openTelemetry)'
                         type: array
                     type: object
                   tracing:
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 7fefb8b9d..bc4833e5c 100644
--- a/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
+++ b/charts/gateway-helm/crds/generated/gateway.envoyproxy.io_securitypolicies.yaml
@@ -186,6 +186,92 @@ spec:
                 required:
                 - providers
                 type: object
+              oidc:
+                description: OIDC defines the configuration for the OpenID Connect
+                  (OIDC) authentication.
+                properties:
+                  clientID:
+                    description: The client ID to be used in the OIDC [Authentication
+                      Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+                    minLength: 1
+                    type: string
+                  clientSecret:
+                    description: "The Kubernetes secret which contains the OIDC client
+                      secret to be used in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+                      \n This is an Opaque secret. The client secret should be stored
+                      in the key \"client-secret\"."
+                    properties:
+                      group:
+                        default: ""
+                        description: Group is the group of the referent. For example,
+                          "gateway.networking.k8s.io". When unspecified or empty string,
+                          core API group is inferred.
+                        maxLength: 253
+                        pattern: ^$|^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$
+                        type: string
+                      kind:
+                        default: Secret
+                        description: Kind is kind of the referent. For example "Secret".
+                        maxLength: 63
+                        minLength: 1
+                        pattern: ^[a-zA-Z]([-a-zA-Z0-9]*[a-zA-Z0-9])?$
+                        type: string
+                      name:
+                        description: Name is the name of the referent.
+                        maxLength: 253
+                        minLength: 1
+                        type: string
+                      namespace:
+                        description: "Namespace is the namespace of the referenced
+                          object. When unspecified, the local namespace is inferred.
+                          \n Note that when a namespace different than the local namespace
+                          is specified, a ReferenceGrant object is required in the
+                          referent namespace to allow that namespace's owner to accept
+                          the reference. See the ReferenceGrant documentation for
+                          details. \n Support: Core"
+                        maxLength: 63
+                        minLength: 1
+                        pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$
+                        type: string
+                    required:
+                    - name
+                    type: object
+                  provider:
+                    description: The OIDC Provider configuration.
+                    properties:
+                      authorizationEndpoint:
+                        description: The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint).
+                          If not provided, EG will try to discover it from the provider's
+                          [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse).
+                        type: string
+                      issuer:
+                        description: The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery).
+                          Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component
+                          that MUST be https, a host component, and optionally, port
+                          and path components and no query or fragment components.
+                        minLength: 1
+                        type: string
+                      tokenEndpoint:
+                        description: The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint).
+                          If not provided, EG will try to discover it from the provider's
+                          [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse).
+                        type: string
+                    required:
+                    - issuer
+                    type: object
+                  scopes:
+                    description: The OIDC scopes to be used in the [Authentication
+                      Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+                      The "openid" scope is always added to the list of scopes if
+                      not already specified.
+                    items:
+                      type: string
+                    type: array
+                required:
+                - clientID
+                - clientSecret
+                - provider
+                type: object
               targetRef:
                 description: TargetRef is the name of the Gateway resource this policy
                   is being attached to. This Policy and the TargetRef MUST be in the
diff --git a/charts/gateway-helm/templates/certgen.yaml b/charts/gateway-helm/templates/certgen.yaml
index e59981bc0..4d49597fe 100644
--- a/charts/gateway-helm/templates/certgen.yaml
+++ b/charts/gateway-helm/templates/certgen.yaml
@@ -31,8 +31,6 @@ spec:
               fieldPath: metadata.namespace
         - name: KUBERNETES_CLUSTER_DOMAIN
           value: {{ .Values.kubernetesClusterDomain }}
-        - name: ENVOY_GATEWAY_CERTIFICATE_EXPIRY_DAYS
-          value: "{{ .Values.deployment.envoyGateway.cert.expiryDays }}"
         image: {{ .Values.deployment.envoyGateway.image.repository }}:{{ .Values.deployment.envoyGateway.image.tag | default .Chart.AppVersion }}
         imagePullPolicy: {{ .Values.deployment.envoyGateway.imagePullPolicy }}
         name: envoy-gateway-certgen
diff --git a/charts/gateway-helm/values.tmpl.yaml b/charts/gateway-helm/values.tmpl.yaml
index b4236aa37..a65b0233b 100644
--- a/charts/gateway-helm/values.tmpl.yaml
+++ b/charts/gateway-helm/values.tmpl.yaml
@@ -1,7 +1,5 @@
 deployment:
   envoyGateway:
-    cert:
-      expiryDays: 365
     image:
       repository: ${ImageRepository}
       tag: '${ImageTag}'
diff --git a/go.mod b/go.mod
index 84b0b0dc8..191de5508 100644
--- a/go.mod
+++ b/go.mod
@@ -15,6 +15,7 @@ require (
 	github.com/golang/protobuf v1.5.3
 	github.com/google/go-cmp v0.6.0
 	github.com/grafana/tempo v1.5.0
+	github.com/miekg/dns v1.1.46
 	github.com/pkg/errors v0.9.1
 	github.com/prometheus/common v0.45.0
 	github.com/spf13/cobra v1.8.0
@@ -26,7 +27,7 @@ require (
 	go.opentelemetry.io/otel v1.20.0
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.43.0
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0
-	go.opentelemetry.io/otel/exporters/prometheus v0.42.0
+	go.opentelemetry.io/otel/exporters/prometheus v0.43.0
 	go.opentelemetry.io/otel/metric v1.20.0
 	go.opentelemetry.io/otel/sdk/metric v1.20.0
 	go.opentelemetry.io/proto/otlp v1.0.0
@@ -55,7 +56,9 @@ require (
 	go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect
 	go.opentelemetry.io/otel/sdk v1.20.0 // indirect
 	go.opentelemetry.io/otel/trace v1.20.0 // indirect
-	golang.org/x/sync v0.3.0 // indirect
+	golang.org/x/mod v0.13.0 // indirect
+	golang.org/x/sync v0.4.0 // indirect
+	golang.org/x/tools v0.14.0 // indirect
 )
 
 require (
diff --git a/go.sum b/go.sum
index 69427b2cb..1438a7369 100644
--- a/go.sum
+++ b/go.sum
@@ -335,6 +335,8 @@ github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzp
 github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
 github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
+github.com/miekg/dns v1.1.46 h1:uzwpxRtSVxtcIZmz/4Uz6/Rn7G11DvsaslXoy5LxQio=
+github.com/miekg/dns v1.1.46/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME=
 github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
 github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
 github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
@@ -481,6 +483,7 @@ github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd
 github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
 github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
 github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
 github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
 go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
 go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -497,8 +500,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.43.0 h1:tFU
 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.43.0/go.mod h1:hbzqqcIxyywu6UQ5J1wb4ntla8nCwCfNBZnMo2Dgh48=
 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 h1:wNMDy/LVGLj2h3p6zg4d0gypKfWKSWI14E1C4smOgl8=
 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0/go.mod h1:YfbDdXAAkemWJK3H/DshvlrxqFB2rtW4rY6ky/3x/H0=
-go.opentelemetry.io/otel/exporters/prometheus v0.42.0 h1:jwV9iQdvp38fxXi8ZC+lNpxjK16MRcZlpDYvbuO1FiA=
-go.opentelemetry.io/otel/exporters/prometheus v0.42.0/go.mod h1:f3bYiqNqhoPxkvI2LrXqQVC546K7BuRDL/kKuxkujhA=
+go.opentelemetry.io/otel/exporters/prometheus v0.43.0 h1:Skkl6akzvdWweXX6LLAY29tyFSO6hWZ26uDbVGTDXe8=
+go.opentelemetry.io/otel/exporters/prometheus v0.43.0/go.mod h1:nZStMoc1H/YJpRjSx9IEX4abBMekORTLQcTUT1CgLkg=
 go.opentelemetry.io/otel/metric v1.20.0 h1:ZlrO8Hu9+GAhnepmRGhSU7/VkpjrNowxRN9GyKR4wzA=
 go.opentelemetry.io/otel/metric v1.20.0/go.mod h1:90DRw3nfK4D7Sm/75yQ00gTJxtkBxX+wu6YaNymbpVM=
 go.opentelemetry.io/otel/sdk v1.20.0 h1:5Jf6imeFZlZtKv9Qbo6qt2ZkmWtdWx/wzcCbNUlAWGM=
@@ -541,7 +544,10 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
 golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
 golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
+golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
 golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -564,6 +570,8 @@ golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
 golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
@@ -579,9 +587,10 @@ golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJ
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
-golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
+golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ=
+golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
 golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -607,8 +616,12 @@ golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -624,6 +637,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
 golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
 golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
@@ -649,6 +663,7 @@ golang.org/x/tools v0.0.0-20190920225731-5eefd052ad72/go.mod h1:b+2E5dAYhXwXZwtn
 golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
 golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
 golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
 golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
diff --git a/internal/cmd/certgen.go b/internal/cmd/certgen.go
index 71ec551a7..83dafca84 100644
--- a/internal/cmd/certgen.go
+++ b/internal/cmd/certgen.go
@@ -66,7 +66,8 @@ func outputCerts(ctx context.Context, cli client.Client, cfg *config.Server, cer
 	if cfg.EnvoyGateway != nil &&
 		cfg.EnvoyGateway.Provider != nil &&
 		cfg.EnvoyGateway.Provider.Kubernetes != nil &&
-		cfg.EnvoyGateway.Provider.Kubernetes.OverwriteControlPlaneCerts {
+		cfg.EnvoyGateway.Provider.Kubernetes.OverwriteControlPlaneCerts != nil &&
+		*cfg.EnvoyGateway.Provider.Kubernetes.OverwriteControlPlaneCerts {
 		updateSecrets = true
 	}
 	secrets, err := kubernetes.CreateOrUpdateSecrets(ctx, cli, kubernetes.CertsToSecret(cfg.Namespace, certs), updateSecrets)
diff --git a/internal/crypto/certgen.go b/internal/crypto/certgen.go
index e347639ff..4f0e86f6b 100644
--- a/internal/crypto/certgen.go
+++ b/internal/crypto/certgen.go
@@ -28,6 +28,9 @@ const (
 	// DefaultEnvoyDNSPrefix defines the default Envoy DNS prefix.
 	DefaultEnvoyDNSPrefix = "*"
 
+	// DefaultCertificateLifetime holds the default certificate lifetime (in days).
+	DefaultCertificateLifetime = 365 * 5
+
 	// keySize sets the RSA key size to 2048 bits. This is minimum recommended size
 	// for RSA keys.
 	keySize = 2048
@@ -94,7 +97,7 @@ func GenerateCerts(cfg *config.Server) (*Certificates, error) {
 	switch certCfg.Provider.Type {
 	case ProviderTypeEnvoyGateway:
 		now := time.Now()
-		expiry := now.Add(24 * time.Duration(cfg.CertificateExpiryDays) * time.Hour)
+		expiry := now.Add(24 * time.Duration(DefaultCertificateLifetime) * time.Hour)
 		caCertPEM, caKeyPEM, err := newCA(DefaultEnvoyGatewayDNSPrefix, expiry)
 		if err != nil {
 			return nil, err
diff --git a/internal/envoygateway/config/config.go b/internal/envoygateway/config/config.go
index 259f0d563..4c9674a88 100644
--- a/internal/envoygateway/config/config.go
+++ b/internal/envoygateway/config/config.go
@@ -23,8 +23,6 @@ const (
 	EnvoyGatewayServiceName = "envoy-gateway"
 	// EnvoyPrefix is the prefix applied to the Envoy ConfigMap, Service, Deployment, and ServiceAccount.
 	EnvoyPrefix = "envoy"
-	// DefaultCertificateExpiryDays holds the default certificate lifetime (in days).
-	DefaultCertificateExpiryDays = 365
 )
 
 // Server wraps the EnvoyGateway configuration and additional parameters
@@ -38,8 +36,6 @@ type Server struct {
 	DNSDomain string
 	// Logger is the logr implementation used by Envoy Gateway.
 	Logger logging.Logger
-	// CertificateExpiryDays holds the certificate lifetime (in days).
-	CertificateExpiryDays int
 }
 
 // New returns a Server with default parameters.
@@ -49,8 +45,7 @@ func New() (*Server, error) {
 		Namespace:    env.Lookup("ENVOY_GATEWAY_NAMESPACE", DefaultNamespace),
 		DNSDomain:    env.Lookup("KUBERNETES_CLUSTER_DOMAIN", DefaultDNSDomain),
 		// the default logger
-		Logger:                logging.DefaultLogger(v1alpha1.LogLevelInfo),
-		CertificateExpiryDays: env.Lookup("ENVOY_GATEWAY_CERTIFICATE_EXPIRY_DAYS", DefaultCertificateExpiryDays),
+		Logger: logging.DefaultLogger(v1alpha1.LogLevelInfo),
 	}, nil
 }
 
diff --git a/internal/gatewayapi/securitypolicy.go b/internal/gatewayapi/securitypolicy.go
index 801f28370..977ff6837 100644
--- a/internal/gatewayapi/securitypolicy.go
+++ b/internal/gatewayapi/securitypolicy.go
@@ -6,15 +6,20 @@
 package gatewayapi
 
 import (
+	"encoding/json"
 	"fmt"
+	"net/http"
 	"sort"
 	"strings"
 
+	v1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
 	gwv1a2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
 	gwv1b1 "sigs.k8s.io/gateway-api/apis/v1beta1"
 
+	"github.com/tetratelabs/multierror"
+
 	egv1a1 "github.com/envoyproxy/gateway/api/v1alpha1"
 	"github.com/envoyproxy/gateway/internal/ir"
 	"github.com/envoyproxy/gateway/internal/status"
@@ -24,6 +29,7 @@ import (
 func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.SecurityPolicy,
 	gateways []*GatewayContext,
 	routes []RouteContext,
+	resources *Resources,
 	xdsIR XdsIRMap) []*egv1a1.SecurityPolicy {
 	var res []*egv1a1.SecurityPolicy
 
@@ -54,7 +60,7 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 
 	// Translate
 	// 1. First translate Policies targeting xRoutes
-	// 2.. Finally, the policies targeting Gateways
+	// 2. Finally, the policies targeting Gateways
 
 	// Process the policies targeting xRoutes
 	for _, policy := range securityPolicies {
@@ -68,13 +74,20 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 				continue
 			}
 
-			t.translateSecurityPolicyForRoute(policy, route, xdsIR)
-
-			message := "SecurityPolicy has been accepted."
-			status.SetSecurityPolicyAcceptedIfUnset(&policy.Status, message)
+			err := t.translateSecurityPolicyForRoute(policy, route, resources, xdsIR)
+			if err != nil {
+				status.SetSecurityPolicyCondition(policy,
+					gwv1a2.PolicyConditionAccepted,
+					metav1.ConditionFalse,
+					gwv1a2.PolicyReasonInvalid,
+					status.Error2ConditionMsg(err),
+				)
+			} else {
+				message := "SecurityPolicy has been accepted."
+				status.SetSecurityPolicyAccepted(&policy.Status, message)
+			}
 		}
 	}
-
 	// Process the policies targeting Gateways
 	for _, policy := range securityPolicies {
 		if policy.Spec.TargetRef.Kind == KindGateway {
@@ -87,17 +100,27 @@ func (t *Translator) ProcessSecurityPolicies(securityPolicies []*egv1a1.Security
 				continue
 			}
 
-			t.translateSecurityPolicyForGateway(policy, gateway, xdsIR)
-
-			message := "SecurityPolicy has been accepted."
-			status.SetSecurityPolicyAcceptedIfUnset(&policy.Status, message)
+			err := t.translateSecurityPolicyForGateway(policy, gateway, resources, xdsIR)
+			if err != nil {
+				status.SetSecurityPolicyCondition(policy,
+					gwv1a2.PolicyConditionAccepted,
+					metav1.ConditionFalse,
+					gwv1a2.PolicyReasonInvalid,
+					status.Error2ConditionMsg(err),
+				)
+			} else {
+				message := "SecurityPolicy has been accepted."
+				status.SetSecurityPolicyAccepted(&policy.Status, message)
+			}
 		}
 	}
 
 	return res
 }
 
-func resolveSecurityPolicyGatewayTargetRef(policy *egv1a1.SecurityPolicy, gateways map[types.NamespacedName]*policyGatewayTargetContext) *GatewayContext {
+func resolveSecurityPolicyGatewayTargetRef(
+	policy *egv1a1.SecurityPolicy,
+	gateways map[types.NamespacedName]*policyGatewayTargetContext) *GatewayContext {
 	targetNs := policy.Spec.TargetRef.Namespace
 	// If empty, default to namespace of policy
 	if targetNs == nil {
@@ -107,7 +130,8 @@ func resolveSecurityPolicyGatewayTargetRef(policy *egv1a1.SecurityPolicy, gatewa
 	// Ensure Policy and target are in the same namespace
 	if policy.Namespace != string(*targetNs) {
 
-		message := fmt.Sprintf("Namespace:%s TargetRef.Namespace:%s, SecurityPolicy can only target a resource in the same namespace.",
+		message := fmt.Sprintf(
+			"Namespace:%s TargetRef.Namespace:%s, SecurityPolicy can only target a resource in the same namespace.",
 			policy.Namespace, *targetNs)
 		status.SetSecurityPolicyCondition(policy,
 			gwv1a2.PolicyConditionAccepted,
@@ -158,7 +182,9 @@ func resolveSecurityPolicyGatewayTargetRef(policy *egv1a1.SecurityPolicy, gatewa
 	return gateway.GatewayContext
 }
 
-func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes map[policyTargetRouteKey]*policyRouteTargetContext) RouteContext {
+func resolveSecurityPolicyRouteTargetRef(
+	policy *egv1a1.SecurityPolicy,
+	routes map[policyTargetRouteKey]*policyRouteTargetContext) RouteContext {
 	targetNs := policy.Spec.TargetRef.Namespace
 	// If empty, default to namespace of policy
 	if targetNs == nil {
@@ -168,7 +194,8 @@ func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes m
 	// Ensure Policy and target are in the same namespace
 	if policy.Namespace != string(*targetNs) {
 
-		message := fmt.Sprintf("Namespace:%s TargetRef.Namespace:%s, SecurityPolicy can only target a resource in the same namespace.",
+		message := fmt.Sprintf(
+			"Namespace:%s TargetRef.Namespace:%s, SecurityPolicy can only target a resource in the same namespace.",
 			policy.Namespace, *targetNs)
 		status.SetSecurityPolicyCondition(policy,
 			gwv1a2.PolicyConditionAccepted,
@@ -189,7 +216,10 @@ func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes m
 
 	// Route not found
 	if !ok {
-		message := fmt.Sprintf("%s/%s/%s not found.", policy.Spec.TargetRef.Kind, string(*targetNs), policy.Spec.TargetRef.Name)
+		message := fmt.Sprintf(
+			"%s/%s/%s not found.",
+			policy.Spec.TargetRef.Kind,
+			string(*targetNs), policy.Spec.TargetRef.Name)
 
 		status.SetSecurityPolicyCondition(policy,
 			gwv1a2.PolicyConditionAccepted,
@@ -202,7 +232,9 @@ func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes m
 
 	// Check if another policy targeting the same xRoute exists
 	if route.attached {
-		message := fmt.Sprintf("Unable to target %s, another SecurityPolicy has already attached to it", string(policy.Spec.TargetRef.Kind))
+		message := fmt.Sprintf(
+			"Unable to target %s, another SecurityPolicy has already attached to it",
+			string(policy.Spec.TargetRef.Kind))
 
 		status.SetSecurityPolicyCondition(policy,
 			gwv1a2.PolicyConditionAccepted,
@@ -220,22 +252,36 @@ func resolveSecurityPolicyRouteTargetRef(policy *egv1a1.SecurityPolicy, routes m
 	return route.RouteContext
 }
 
-func (t *Translator) translateSecurityPolicyForRoute(policy *egv1a1.SecurityPolicy, route RouteContext, xdsIR XdsIRMap) {
+func (t *Translator) translateSecurityPolicyForRoute(
+	policy *egv1a1.SecurityPolicy, route RouteContext,
+	resources *Resources, xdsIR XdsIRMap) error {
 	// Build IR
 	var (
-		cors *ir.CORS
-		jwt  *ir.JWT
+		cors      *ir.CORS
+		jwt       *ir.JWT
+		oidc      *ir.OIDC
+		err, errs error
 	)
 
 	if policy.Spec.CORS != nil {
-		cors = t.buildCORS(policy)
+		if cors, err = t.buildCORS(policy.Spec.CORS); err != nil {
+			errs = multierror.Append(errs, err)
+		}
 	}
 
 	if policy.Spec.JWT != nil {
-		jwt = t.buildJWT(policy)
+		jwt = t.buildJWT(policy.Spec.JWT)
+	}
+
+	if policy.Spec.OIDC != nil {
+		if oidc, err = t.buildOIDC(policy, resources); err != nil {
+			errs = multierror.Append(errs, err)
+		}
 	}
 
 	// Apply IR to all relevant routes
+	// Note: there are multiple features in a security policy, even if some of them
+	// are invalid, we still want to apply the valid ones.
 	prefix := irRoutePrefix(route)
 	for _, ir := range xdsIR {
 		for _, http := range ir.HTTP {
@@ -246,31 +292,52 @@ func (t *Translator) translateSecurityPolicyForRoute(policy *egv1a1.SecurityPoli
 				if strings.HasPrefix(r.Name, prefix) {
 					r.CORS = cors
 					r.JWT = jwt
+					r.OIDC = oidc
 				}
 			}
 		}
-
 	}
+	return errs
 }
 
-func (t *Translator) translateSecurityPolicyForGateway(policy *egv1a1.SecurityPolicy, gateway *GatewayContext, xdsIR XdsIRMap) {
+func (t *Translator) translateSecurityPolicyForGateway(
+	policy *egv1a1.SecurityPolicy, gateway *GatewayContext,
+	resources *Resources, xdsIR XdsIRMap) error {
 	// Build IR
 	var (
-		cors *ir.CORS
-		jwt  *ir.JWT
+		cors      *ir.CORS
+		jwt       *ir.JWT
+		oidc      *ir.OIDC
+		err, errs error
 	)
 
 	if policy.Spec.CORS != nil {
-		cors = t.buildCORS(policy)
+		cors, err = t.buildCORS(policy.Spec.CORS)
+		if err != nil {
+			errs = multierror.Append(errs, err)
+		}
 	}
 
 	if policy.Spec.JWT != nil {
-		jwt = t.buildJWT(policy)
+		jwt = t.buildJWT(policy.Spec.JWT)
+	}
+
+	if policy.Spec.OIDC != nil {
+		oidc, err = t.buildOIDC(policy, resources)
+		if err != nil {
+			errs = multierror.Append(errs, err)
+		}
 	}
 
 	// Apply IR to all the routes within the specific Gateway
 	// If the feature is already set, then skip it, since it must have be
 	// set by a policy attaching to the route
+	//
+	// It can be difficult to reason about the state of the system if we apply
+	// part of the policy and not the rest. Therefore, we either apply all of it
+	// or none of it (when get errors when translating the policy)
+	// Note: there are multiple features in a security policy, even if some of them
+	// are invalid, we still want to apply the valid ones.
 	irKey := t.getIRKey(gateway.Gateway)
 	// Should exist since we've validated this
 	ir := xdsIR[irKey]
@@ -284,15 +351,18 @@ func (t *Translator) translateSecurityPolicyForGateway(policy *egv1a1.SecurityPo
 			if r.JWT == nil {
 				r.JWT = jwt
 			}
+			if r.OIDC == nil {
+				r.OIDC = oidc
+			}
 		}
 	}
-
+	return errs
 }
 
-func (t *Translator) buildCORS(policy *egv1a1.SecurityPolicy) *ir.CORS {
+func (t *Translator) buildCORS(cors *egv1a1.CORS) (*ir.CORS, error) {
 	var allowOrigins []*ir.StringMatch
 
-	for _, origin := range policy.Spec.CORS.AllowOrigins {
+	for _, origin := range cors.AllowOrigins {
 		origin := origin.DeepCopy()
 
 		// matchType default to exact
@@ -316,23 +386,125 @@ func (t *Translator) buildCORS(policy *egv1a1.SecurityPolicy) *ir.CORS {
 				Suffix: &origin.Value,
 			})
 		case egv1a1.StringMatchRegularExpression:
+			if err := validateRegex(origin.Value); err != nil {
+				return nil, err // TODO zhaohuabing: also check regex in other places
+			}
 			allowOrigins = append(allowOrigins, &ir.StringMatch{
-				SafeRegex: &origin.Value, // TODO zhaohuabing: check if the value is a valid regex
+				SafeRegex: &origin.Value,
 			})
 		}
 	}
 
 	return &ir.CORS{
 		AllowOrigins:  allowOrigins,
-		AllowMethods:  policy.Spec.CORS.AllowMethods,
-		AllowHeaders:  policy.Spec.CORS.AllowHeaders,
-		ExposeHeaders: policy.Spec.CORS.ExposeHeaders,
-		MaxAge:        policy.Spec.CORS.MaxAge,
-	}
+		AllowMethods:  cors.AllowMethods,
+		AllowHeaders:  cors.AllowHeaders,
+		ExposeHeaders: cors.ExposeHeaders,
+		MaxAge:        cors.MaxAge,
+	}, nil
 }
 
-func (t *Translator) buildJWT(policy *egv1a1.SecurityPolicy) *ir.JWT {
+func (t *Translator) buildJWT(jwt *egv1a1.JWT) *ir.JWT {
 	return &ir.JWT{
-		Providers: policy.Spec.JWT.Providers,
+		Providers: jwt.Providers,
+	}
+}
+
+func (t *Translator) buildOIDC(
+	policy *egv1a1.SecurityPolicy,
+	resources *Resources) (*ir.OIDC, error) {
+	var (
+		oidc         = policy.Spec.OIDC
+		clientSecret *v1.Secret
+		err          error
+	)
+
+	from := crossNamespaceFrom{
+		group:     egv1a1.GroupName,
+		kind:      KindSecurityPolicy,
+		namespace: policy.Namespace,
 	}
+	if clientSecret, err = t.validateSecretRef(
+		false, from, oidc.ClientSecret, resources); err != nil {
+		return nil, err
+	}
+
+	clientSecretBytes, ok := clientSecret.Data[egv1a1.OIDCClientSecretKey]
+	if !ok || len(clientSecretBytes) == 0 {
+		return nil, fmt.Errorf(
+			"client secret not found in secret %s/%s",
+			clientSecret.Namespace, clientSecret.Name)
+	}
+
+	// Discover the token and authorization endpoints from the issuer's
+	// well-known url if not explicitly specified
+	provider := oidc.Provider.DeepCopy()
+	if err := discoverEndpointsFromIssuer(provider); err != nil {
+		return nil, err
+	}
+
+	scopes := appendOpenidScopeIfNotExist(oidc.Scopes)
+
+	return &ir.OIDC{
+		Provider:     *provider,
+		ClientID:     oidc.ClientID,
+		ClientSecret: clientSecretBytes,
+		Scopes:       scopes,
+	}, nil
+}
+
+// appendOpenidScopeIfNotExist appends the openid scope to the provided scopes
+// if it is not already present.
+// `openid` is a required scope for OIDC.
+// see https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims
+func appendOpenidScopeIfNotExist(scopes []string) []string {
+	const authScopeOpenID = "openid"
+
+	hasOpenIDScope := false
+	for _, scope := range scopes {
+		if scope == authScopeOpenID {
+			hasOpenIDScope = true
+		}
+	}
+	if !hasOpenIDScope {
+		scopes = append(scopes, authScopeOpenID)
+	}
+	return scopes
+}
+
+type OpenIDConfig struct {
+	TokenEndpoint         string `json:"token_endpoint"`
+	AuthorizationEndpoint string `json:"authorization_endpoint"`
+}
+
+// discoverEndpointsFromIssuer discovers the token and authorization endpoints from the issuer's well-known url
+// return error if failed to fetch the well-known configuration
+func discoverEndpointsFromIssuer(provider *egv1a1.OIDCProvider) error {
+	if provider.TokenEndpoint == nil || provider.AuthorizationEndpoint == nil {
+		tokenEndpoint, authorizationEndpoint, err := fetchEndpointsFromIssuer(provider.Issuer)
+		if err != nil {
+			return fmt.Errorf("error fetching endpoints from issuer: %w", err)
+		}
+		provider.TokenEndpoint = &tokenEndpoint
+		provider.AuthorizationEndpoint = &authorizationEndpoint
+	}
+	return nil
+}
+
+func fetchEndpointsFromIssuer(issuerURL string) (string, string, error) {
+	// Fetch the OpenID configuration from the issuer URL
+	resp, err := http.Get(fmt.Sprintf("%s/.well-known/openid-configuration", issuerURL))
+	if err != nil {
+		return "", "", err
+	}
+	defer resp.Body.Close()
+
+	// Parse the OpenID configuration response
+	var config OpenIDConfig
+	err = json.NewDecoder(resp.Body).Decode(&config)
+	if err != nil {
+		return "", "", err
+	}
+
+	return config.TokenEndpoint, config.AuthorizationEndpoint, nil
 }
diff --git a/internal/gatewayapi/translator.go b/internal/gatewayapi/translator.go
index f5d6b54bc..198b30680 100644
--- a/internal/gatewayapi/translator.go
+++ b/internal/gatewayapi/translator.go
@@ -14,18 +14,19 @@ import (
 )
 
 const (
-	KindEnvoyProxy    = "EnvoyProxy"
-	KindGateway       = "Gateway"
-	KindGatewayClass  = "GatewayClass"
-	KindGRPCRoute     = "GRPCRoute"
-	KindHTTPRoute     = "HTTPRoute"
-	KindNamespace     = "Namespace"
-	KindTLSRoute      = "TLSRoute"
-	KindTCPRoute      = "TCPRoute"
-	KindUDPRoute      = "UDPRoute"
-	KindService       = "Service"
-	KindServiceImport = "ServiceImport"
-	KindSecret        = "Secret"
+	KindEnvoyProxy     = "EnvoyProxy"
+	KindGateway        = "Gateway"
+	KindGatewayClass   = "GatewayClass"
+	KindGRPCRoute      = "GRPCRoute"
+	KindHTTPRoute      = "HTTPRoute"
+	KindNamespace      = "Namespace"
+	KindTLSRoute       = "TLSRoute"
+	KindTCPRoute       = "TCPRoute"
+	KindUDPRoute       = "UDPRoute"
+	KindService        = "Service"
+	KindServiceImport  = "ServiceImport"
+	KindSecret         = "Secret"
+	KindSecurityPolicy = "SecurityPolicy"
 
 	GroupMultiClusterService = "multicluster.x-k8s.io"
 	// OwningGatewayNamespaceLabel is the owner reference label used for managed infra.
@@ -187,9 +188,10 @@ func (t *Translator) Translate(resources *Resources) *TranslateResult {
 	// Process BackendTrafficPolicies
 	backendTrafficPolicies := t.ProcessBackendTrafficPolicies(
 		resources.BackendTrafficPolicies, gateways, routes, xdsIR)
+
 	// Process SecurityPolicies
 	securityPolicies := t.ProcessSecurityPolicies(
-		resources.SecurityPolicies, gateways, routes, xdsIR)
+		resources.SecurityPolicies, gateways, routes, resources, xdsIR)
 
 	// Sort xdsIR based on the Gateway API spec
 	sortXdsIRMap(xdsIR)
diff --git a/internal/gatewayapi/validate.go b/internal/gatewayapi/validate.go
index 1ada34033..8acc278a4 100644
--- a/internal/gatewayapi/validate.go
+++ b/internal/gatewayapi/validate.go
@@ -6,8 +6,10 @@
 package gatewayapi
 
 import (
+	"errors"
 	"fmt"
 	"net/netip"
+	"regexp"
 	"strings"
 
 	v1 "k8s.io/api/core/v1"
@@ -283,6 +285,7 @@ func (t *Translator) validateTerminateModeAndGetTLSSecrets(listener *ListenerCon
 
 	secrets := make([]*v1.Secret, 0)
 	for _, certificateRef := range listener.TLS.CertificateRefs {
+		// TODO zhaohuabing: reuse validateSecretRef
 		if certificateRef.Group != nil && string(*certificateRef.Group) != "" {
 			listener.SetCondition(
 				gwapiv1.ListenerConditionResolvedRefs,
@@ -722,3 +725,68 @@ func (t *Translator) validateHostname(hostname string) error {
 
 	return nil
 }
+
+// validateSecretRef checks three things:
+//  1. Does the secret reference have a valid Group and kind
+//  2. If the secret reference is a cross-namespace reference,
+//     is it permitted by any ReferenceGrant
+//  3. Does the secret exist
+func (t *Translator) validateSecretRef(
+	allowCrossNamespace bool,
+	from crossNamespaceFrom,
+	secretRef gwapiv1b1.SecretObjectReference,
+	resources *Resources) (*v1.Secret, error) {
+	if secretRef.Group != nil && string(*secretRef.Group) != "" {
+		return nil, errors.New("secret ref group must be unspecified/empty")
+	}
+
+	if secretRef.Kind != nil && string(*secretRef.Kind) != KindSecret {
+		return nil, fmt.Errorf("secret ref kind must be %s", KindSecret)
+	}
+
+	secretNamespace := from.namespace
+
+	if secretRef.Namespace != nil &&
+		string(*secretRef.Namespace) != "" &&
+		string(*secretRef.Namespace) != from.namespace {
+		if !allowCrossNamespace {
+			return nil, fmt.Errorf(
+				"secret ref namespace must be unspecified/empty or %s",
+				from.namespace)
+		}
+
+		if !t.validateCrossNamespaceRef(
+			from,
+			crossNamespaceTo{
+				group:     "",
+				kind:      KindSecret,
+				namespace: string(*secretRef.Namespace),
+				name:      string(secretRef.Name),
+			},
+			resources.ReferenceGrants,
+		) {
+			return nil,
+				fmt.Errorf(
+					"certificate ref to secret %s/%s not permitted by any ReferenceGrant",
+					*secretRef.Namespace, secretRef.Name)
+		}
+
+		secretNamespace = string(*secretRef.Namespace)
+	}
+
+	secret := resources.GetSecret(secretNamespace, string(secretRef.Name))
+
+	if secret == nil {
+		return nil, fmt.Errorf(
+			"secret %s/%s does not exist", secretNamespace, secretRef.Name)
+	}
+
+	return secret, nil
+}
+
+func validateRegex(regex string) error {
+	if _, err := regexp.Compile(regex); err != nil {
+		return fmt.Errorf("regex %q is invalid: %v", regex, err)
+	}
+	return nil
+}
diff --git a/internal/ir/xds.go b/internal/ir/xds.go
index 9411edba7..2c5179d9d 100644
--- a/internal/ir/xds.go
+++ b/internal/ir/xds.go
@@ -13,6 +13,7 @@ import (
 
 	"github.com/tetratelabs/multierror"
 	"golang.org/x/exp/slices"
+
 	apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@@ -149,6 +150,13 @@ func (x Xds) Printable() *Xds {
 	for _, listener := range out.HTTP {
 		// Omit field
 		listener.TLS = nil
+
+		for _, route := range listener.Routes {
+			// Omit field
+			if route.OIDC != nil {
+				route.OIDC.ClientSecret = []byte{}
+			}
+		}
 	}
 	return out
 }
@@ -281,6 +289,8 @@ type HTTPRoute struct {
 	CORS *CORS `json:"cors,omitempty" yaml:"cors,omitempty"`
 	// JWT defines the schema for authenticating HTTP requests using JSON Web Tokens (JWT).
 	JWT *JWT `json:"jwt,omitempty" yaml:"jwt,omitempty"`
+	// OIDC defines the schema for authenticating HTTP requests using OpenID Connect (OIDC).
+	OIDC *OIDC `json:"oidc,omitempty" yaml:"oidc,omitempty"`
 	// ExtensionRefs holds unstructured resources that were introduced by an extension and used on the HTTPRoute as extensionRef filters
 	ExtensionRefs []*UnstructuredRef `json:"extensionRefs,omitempty" yaml:"extensionRefs,omitempty"`
 }
@@ -319,6 +329,30 @@ type JWT struct {
 	Providers []egv1a1.JWTProvider `json:"providers,omitempty" yaml:"providers,omitempty"`
 }
 
+// OIDC defines the schema for authenticating HTTP requests using
+// OpenID Connect (OIDC).
+//
+// +k8s:deepcopy-gen=true
+type OIDC struct {
+	// The OIDC Provider configuration.
+	Provider egv1a1.OIDCProvider `json:"provider" yaml:"provider"`
+
+	// The OIDC client ID to be used in the
+	// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+	ClientID string `json:"clientID" yaml:"clientID"`
+
+	// The OIDC client secret to be used in the
+	// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+	//
+	// This is an Opaque secret. The client secret should be stored in the key "client-secret".
+
+	ClientSecret []byte `json:"clientSecret,omitempty" yaml:"clientSecret,omitempty"`
+
+	// The OIDC scopes to be used in the
+	// [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest).
+	Scopes []string `json:"scopes,omitempty" yaml:"scopes,omitempty"`
+}
+
 // Validate the fields within the HTTPRoute structure
 func (h HTTPRoute) Validate() error {
 	var errs error
diff --git a/internal/ir/zz_generated.deepcopy.go b/internal/ir/zz_generated.deepcopy.go
index 7ebe840dd..edf7e1146 100644
--- a/internal/ir/zz_generated.deepcopy.go
+++ b/internal/ir/zz_generated.deepcopy.go
@@ -461,6 +461,11 @@ func (in *HTTPRoute) DeepCopyInto(out *HTTPRoute) {
 		*out = new(JWT)
 		(*in).DeepCopyInto(*out)
 	}
+	if in.OIDC != nil {
+		in, out := &in.OIDC, &out.OIDC
+		*out = new(OIDC)
+		(*in).DeepCopyInto(*out)
+	}
 	if in.ExtensionRefs != nil {
 		in, out := &in.ExtensionRefs, &out.ExtensionRefs
 		*out = make([]*UnstructuredRef, len(*in))
@@ -682,6 +687,32 @@ func (in *Metrics) DeepCopy() *Metrics {
 	return out
 }
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *OIDC) DeepCopyInto(out *OIDC) {
+	*out = *in
+	in.Provider.DeepCopyInto(&out.Provider)
+	if in.ClientSecret != nil {
+		in, out := &in.ClientSecret, &out.ClientSecret
+		*out = make([]byte, len(*in))
+		copy(*out, *in)
+	}
+	if in.Scopes != nil {
+		in, out := &in.Scopes, &out.Scopes
+		*out = make([]string, len(*in))
+		copy(*out, *in)
+	}
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC.
+func (in *OIDC) DeepCopy() *OIDC {
+	if in == nil {
+		return nil
+	}
+	out := new(OIDC)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *OpenTelemetryAccessLog) DeepCopyInto(out *OpenTelemetryAccessLog) {
 	*out = *in
diff --git a/internal/provider/kubernetes/controller.go b/internal/provider/kubernetes/controller.go
index 97ade7212..cd54c0a4d 100644
--- a/internal/provider/kubernetes/controller.go
+++ b/internal/provider/kubernetes/controller.go
@@ -324,6 +324,9 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, _ reconcile.Reques
 		resourceTree.SecurityPolicies = append(resourceTree.SecurityPolicies, &policy)
 	}
 
+	// Add the referenced Secrets in SecurityPolicies to the resourceTree
+	r.processSecurityPolicySecretRefs(ctx, resourceTree, resourceMap)
+
 	// For this particular Gateway, and all associated objects, check whether the
 	// namespace exists. Add to the resourceTree.
 	for ns := range resourceMap.allAssociatedNamespaces {
@@ -388,6 +391,61 @@ func (r *gatewayAPIReconciler) Reconcile(ctx context.Context, _ reconcile.Reques
 	return reconcile.Result{}, nil
 }
 
+// processSecurityPolicySecretRefs adds the referenced Secrets in SecurityPolicies
+// to the resourceTree
+func (r *gatewayAPIReconciler) processSecurityPolicySecretRefs(
+	ctx context.Context, resourceTree *gatewayapi.Resources, resourceMap *resourceMappings) {
+	for _, policy := range resourceTree.SecurityPolicies {
+		oidc := policy.Spec.OIDC
+		if oidc != nil {
+			secret := new(corev1.Secret)
+			secretNamespace := gatewayapi.NamespaceDerefOr(oidc.ClientSecret.Namespace, policy.Namespace)
+			err := r.client.Get(ctx,
+				types.NamespacedName{Namespace: secretNamespace, Name: string(oidc.ClientSecret.Name)},
+				secret,
+			)
+			if err != nil && !kerrors.IsNotFound(err) {
+				r.log.Error(err, "unable to find the Secret for OIDC client secret")
+				// we don't return an error here, because we want to continue
+				// reconciling the rest of the resources despite that this
+				// SecurityPolicy is invalid.
+				// This SecurityPolicy will be marked as invalid in its status
+				// when translating to IR because the referenced secret can't be
+				// found.
+			}
+
+			if secretNamespace != policy.Namespace {
+				from := ObjectKindNamespacedName{
+					kind:      v1alpha1.KindSecurityPolicy,
+					namespace: policy.Namespace,
+					name:      policy.Name,
+				}
+				to := ObjectKindNamespacedName{
+					kind:      gatewayapi.KindSecret,
+					namespace: secretNamespace,
+					name:      secret.Name,
+				}
+				refGrant, err := r.findReferenceGrant(ctx, from, to)
+				switch {
+				case err != nil:
+					r.log.Error(err, "failed to find ReferenceGrant")
+				case refGrant == nil:
+					r.log.Info("no matching ReferenceGrants found", "from", from.kind,
+						"from namespace", from.namespace, "target", to.kind, "target namespace", to.namespace)
+				default:
+					// RefGrant found
+					resourceMap.allAssociatedRefGrants[utils.NamespacedName(refGrant)] = refGrant
+					r.log.Info("added ReferenceGrant to resource map", "namespace", refGrant.Namespace,
+						"name", refGrant.Name)
+				}
+			}
+			resourceMap.allAssociatedNamespaces[secretNamespace] = struct{}{} // TODO Zhaohuabing do we need this line?
+			resourceTree.Secrets = append(resourceTree.Secrets, secret)
+			r.log.Info("processing Secret", "namespace", secretNamespace, "name", string(oidc.ClientSecret.Name))
+		}
+	}
+}
+
 func (r *gatewayAPIReconciler) gatewayClassUpdater(ctx context.Context, gc *gwapiv1.GatewayClass, accepted bool, reason, msg string) error {
 	if r.statusUpdater != nil {
 		r.statusUpdater.Send(status.Update{
diff --git a/internal/status/conditions.go b/internal/status/conditions.go
index bd4b3114b..eb6419d15 100644
--- a/internal/status/conditions.go
+++ b/internal/status/conditions.go
@@ -16,6 +16,7 @@ package status
 import (
 	"fmt"
 	"time"
+	"unicode"
 
 	appsv1 "k8s.io/api/apps/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -138,3 +139,33 @@ func conditionChanged(a, b metav1.Condition) bool {
 		(a.Message != b.Message) ||
 		(a.ObservedGeneration != b.ObservedGeneration)
 }
+
+// Error2ConditionMsg format the error string to a Status condition message.
+// * Convert the first letter to capital
+// * Append "." to the string if it doesn't exit
+func Error2ConditionMsg(err error) string {
+	if err == nil {
+		return ""
+	}
+
+	message := err.Error()
+	if message == "" {
+		return message
+	}
+
+	// Convert the string to a rune slice for easier manipulation
+	runes := []rune(message)
+
+	// Check if the first rune is a letter and convert it to uppercase
+	if unicode.IsLetter(runes[0]) {
+		runes[0] = unicode.ToUpper(runes[0])
+	}
+
+	// check if the last rune is .
+	if runes[len(runes)-1] != '.' {
+		return string(runes) + "."
+	}
+
+	// Convert the rune slice back to a string
+	return string(runes)
+}
diff --git a/internal/status/securitypolicy.go b/internal/status/securitypolicy.go
index c538a9723..cf9e478c6 100644
--- a/internal/status/securitypolicy.go
+++ b/internal/status/securitypolicy.go
@@ -19,14 +19,7 @@ func SetSecurityPolicyCondition(c *egv1a1.SecurityPolicy, conditionType gwv1a2.P
 	c.Status.Conditions = MergeConditions(c.Status.Conditions, cond)
 }
 
-func SetSecurityPolicyAcceptedIfUnset(s *egv1a1.SecurityPolicyStatus, message string) {
-	// Return early if Accepted condition is already set
-	for _, c := range s.Conditions {
-		if c.Type == string(gwv1a2.PolicyConditionAccepted) {
-			return
-		}
-	}
-
+func SetSecurityPolicyAccepted(s *egv1a1.SecurityPolicyStatus, message string) {
 	cond := newCondition(string(gwv1a2.PolicyConditionAccepted), metav1.ConditionTrue, string(gwv1a2.PolicyReasonAccepted), message, time.Now(), 0)
 	s.Conditions = MergeConditions(s.Conditions, cond)
 }
diff --git a/internal/xds/translator/cluster.go b/internal/xds/translator/cluster.go
index a5be54e6d..4afdc47d7 100644
--- a/internal/xds/translator/cluster.go
+++ b/internal/xds/translator/cluster.go
@@ -91,7 +91,7 @@ func buildXdsCluster(args *xdsClusterArgs) *clusterv3.Cluster {
 
 func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.DestinationSetting) *endpointv3.ClusterLoadAssignment {
 	localities := make([]*endpointv3.LocalityLbEndpoints, 0, len(destSettings))
-	for _, ds := range destSettings {
+	for i, ds := range destSettings {
 
 		endpoints := make([]*endpointv3.LbEndpoint, 0, len(ds.Endpoints))
 
@@ -118,8 +118,13 @@ func buildXdsClusterLoadAssignment(clusterName string, destSettings []*ir.Destin
 			endpoints = append(endpoints, lbEndpoint)
 		}
 
+		// Envoy requires a distinct region to be set for each LocalityLbEndpoints.
+		// If we don't do this, Envoy will merge all LocalityLbEndpoints into one.
+		// We use the name of the backendRef as a pseudo region name.
 		locality := &endpointv3.LocalityLbEndpoints{
-			Locality:    &corev3.Locality{},
+			Locality: &corev3.Locality{
+				Region: fmt.Sprintf("%s/backend/%d", clusterName, i),
+			},
 			LbEndpoints: endpoints,
 			Priority:    0,
 		}
diff --git a/site/content/en/v0.6.0/user/http-redirect.md b/site/content/en/v0.6.0/user/http-redirect.md
index da61bdaf3..1ef88e53f 100644
--- a/site/content/en/v0.6.0/user/http-redirect.md
+++ b/site/content/en/v0.6.0/user/http-redirect.md
@@ -9,7 +9,7 @@ learn more about HTTP routing, refer to the [Gateway API documentation][].
 
 ## Prerequisites
 
-Follow the steps from the [Secure Gateways](secure-gateways.md) to install Envoy Gateway and the example manifest.
+Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest.
 Before proceeding, you should be able to query the example backend using HTTPS.
 
 ## Redirects
@@ -21,7 +21,7 @@ For example, to issue a permanent redirect (301) from HTTP to HTTPS, configure `
 
 ```shell
 cat <<EOF | kubectl apply -f -
-apiVersion: gateway.networking.k8s.io/v1beta1
+apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: http-to-https-filter-redirect
@@ -72,6 +72,132 @@ $ curl -L -vvv --header "Host: redirect.example" "http://${GATEWAY_HOST}/get"
 If you followed the steps in the [Secure Gateways](secure-gateways.md) guide, you should be able to curl the redirect
 location.
 
+## HTTP --> HTTPS
+
+Listeners expose the TLS setting on a per domain or subdomain basis. TLS settings of a listener are applied to all domains that satisfy the hostname criteria.
+
+Create a root certificate and private key to sign certificates:
+
+```shell
+openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/CN=example.com' -keyout CA.key -out CA.crt
+openssl req -out example.com.csr -newkey rsa:2048 -nodes -keyout tls.key -subj "/CN=example.com"
+```
+
+Generate a self-signed wildcard certificate for `example.com` with `*.example.com` extension
+
+```shell
+cat <<EOF | openssl x509 -req -days 365 -CA CA.crt -CAkey CA.key -set_serial 0 \
+-subj "/CN=example.com" \
+-in example.com.csr -out tls.crt -extensions v3_req  -extfile -
+[v3_req]
+keyUsage = keyEncipherment, dataEncipherment
+extendedKeyUsage = serverAuth
+subjectAltName = @alt_names
+[alt_names]
+DNS.1   = example.com
+DNS.2   = *.example.com
+EOF
+```
+
+Create the kubernetes tls secret
+
+```shell
+kubectl create secret tls example-com --key=tls.key --cert=tls.crt
+```
+
+Define a https listener on the existing gateway
+
+```shell
+cat <<EOF | kubectl apply -n default -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: eg
+spec:
+  gatewayClassName: eg
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+    # hostname: "*.example.com"
+  - name: https
+    port: 443
+    protocol: HTTPS
+    # hostname: "*.example.com"
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - kind: Secret
+        name: example-com
+EOF
+```
+
+Check for any TLS certificate issues on the gateway.
+
+```bash
+kubectl -n default describe gateway eg
+```
+
+Create two HTTPRoutes and attach them to the HTTP and HTTPS listeners using the [sectionName][] field.
+
+
+```shell
+cat <<EOF | kubectl apply -n default -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: tls-redirect
+spec:
+  parentRefs:
+    - name: eg
+      sectionName: http
+  hostnames:
+    # - "*.example.com" # catch all hostnames
+    - "www.example.com"
+  rules:
+    - filters:
+        - type: RequestRedirect
+          requestRedirect:
+            scheme: https
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: backend
+spec:
+  parentRefs:
+    - name: eg
+      sectionName: https
+  hostnames:
+    - "www.example.com"
+  rules:
+    - backendRefs:
+        - group: ""
+          kind: Service
+          name: backend
+          port: 3000
+          weight: 1
+      matches:
+        - path:
+            type: PathPrefix
+            value: /
+EOF
+```
+
+Curl the example app through http listener:
+
+```bash
+curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get
+```
+
+Curl the example app through https listener:
+
+```bash
+curl -v -H 'Host:www.example.com' --resolve "www.example.com:443:$GATEWAY_HOST" \
+--cacert CA.crt https://www.example.com:443/get
+```
+
+
 ## Path Redirects
 
 Path redirects use an HTTP Path Modifier to replace either entire paths or path prefixes. For example, the HTTPRoute
@@ -79,7 +205,7 @@ below will issue a 302 redirect to all `path.redirect.example` requests whose pa
 
 ```shell
 cat <<EOF | kubectl apply -f -
-apiVersion: gateway.networking.k8s.io/v1beta1
+apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: http-filter-path-redirect
@@ -124,6 +250,7 @@ curl -vvv --header "Host: path.redirect.example" "http://${GATEWAY_HOST}/get"
 You should receive a `302` with a redirect location of `http://path.redirect.example/status/200`.
 
 [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/
-[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilter
+[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter
 [Gateway API documentation]: https://gateway-api.sigs.k8s.io/
-[req_filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRequestRedirectFilter
+[req_filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRequestRedirectFilter
+[sectionName]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.CommonRouteSpec
diff --git a/tools/linter/codespell/.codespell.ignorewords b/tools/linter/codespell/.codespell.ignorewords
index 254c60d12..e773f8586 100644
--- a/tools/linter/codespell/.codespell.ignorewords
+++ b/tools/linter/codespell/.codespell.ignorewords
@@ -1,4 +1,4 @@
 keypair
 keypairs
 als
-requestor
\ No newline at end of file
+requestor


Test patch:
diff --git a/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml b/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml
index 06afc6278..83d2c82e6 100644
--- a/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml
+++ b/internal/cmd/egctl/testdata/translate/out/default-resources.all.yaml
@@ -598,7 +598,8 @@ xds:
                     portValue: 8000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: httproute/default/backend/rule/0/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: grpcroute/default/backend/rule/0
@@ -611,7 +612,8 @@ xds:
                     portValue: 9000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: grpcroute/default/backend/rule/0/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: tlsroute/default/backend/rule/-1
@@ -624,7 +626,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: tlsroute/default/backend/rule/-1/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: tcproute/default/backend/rule/-1
@@ -637,7 +640,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: tcproute/default/backend/rule/-1/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: udproute/default/backend/rule/-1
@@ -650,7 +654,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: udproute/default/backend/rule/-1/backend/0
     - '@type': type.googleapis.com/envoy.admin.v3.ClustersConfigDump
       dynamicActiveClusters:
       - cluster:
diff --git a/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.json b/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.json
index 53fc117ef..1e461964b 100644
--- a/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.json
+++ b/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.json
@@ -235,7 +235,9 @@
                       }
                     ],
                     "loadBalancingWeight": 1,
-                    "locality": {}
+                    "locality": {
+                      "region": "httproute/default/backend/rule/0/backend/0"
+                    }
                   }
                 ]
               }
@@ -260,7 +262,9 @@
                       }
                     ],
                     "loadBalancingWeight": 1,
-                    "locality": {}
+                    "locality": {
+                      "region": "grpcroute/default/backend/rule/0/backend/0"
+                    }
                   }
                 ]
               }
@@ -285,7 +289,9 @@
                       }
                     ],
                     "loadBalancingWeight": 1,
-                    "locality": {}
+                    "locality": {
+                      "region": "tlsroute/default/backend/rule/-1/backend/0"
+                    }
                   }
                 ]
               }
@@ -310,7 +316,9 @@
                       }
                     ],
                     "loadBalancingWeight": 1,
-                    "locality": {}
+                    "locality": {
+                      "region": "tcproute/default/backend/rule/-1/backend/0"
+                    }
                   }
                 ]
               }
@@ -335,7 +343,9 @@
                       }
                     ],
                     "loadBalancingWeight": 1,
-                    "locality": {}
+                    "locality": {
+                      "region": "udproute/default/backend/rule/-1/backend/0"
+                    }
                   }
                 ]
               }
diff --git a/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.yaml b/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.yaml
index 40cee9b63..5c5b27e97 100644
--- a/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.yaml
+++ b/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.all.yaml
@@ -131,7 +131,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: httproute/default/backend/rule/0/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: grpcroute/default/backend/rule/0
@@ -144,7 +145,8 @@ xds:
                     portValue: 9000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: grpcroute/default/backend/rule/0/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: tlsroute/default/backend/rule/-1
@@ -157,7 +159,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: tlsroute/default/backend/rule/-1/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: tcproute/default/backend/rule/-1
@@ -170,7 +173,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: tcproute/default/backend/rule/-1/backend/0
       - endpointConfig:
           '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
           clusterName: udproute/default/backend/rule/-1
@@ -183,7 +187,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: udproute/default/backend/rule/-1/backend/0
     - '@type': type.googleapis.com/envoy.admin.v3.ClustersConfigDump
       dynamicActiveClusters:
       - cluster:
diff --git a/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.endpoint.yaml b/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.endpoint.yaml
index 4b510ae5b..76b49b6fa 100644
--- a/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.endpoint.yaml
+++ b/internal/cmd/egctl/testdata/translate/out/from-gateway-api-to-xds.endpoint.yaml
@@ -14,7 +14,8 @@ xds:
                   portValue: 3000
             loadBalancingWeight: 1
           loadBalancingWeight: 1
-          locality: {}
+          locality:
+            region: httproute/default/backend/rule/0/backend/0
     - endpointConfig:
         '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
         clusterName: grpcroute/default/backend/rule/0
@@ -27,7 +28,8 @@ xds:
                   portValue: 9000
             loadBalancingWeight: 1
           loadBalancingWeight: 1
-          locality: {}
+          locality:
+            region: grpcroute/default/backend/rule/0/backend/0
     - endpointConfig:
         '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
         clusterName: tlsroute/default/backend/rule/-1
@@ -40,7 +42,8 @@ xds:
                   portValue: 3000
             loadBalancingWeight: 1
           loadBalancingWeight: 1
-          locality: {}
+          locality:
+            region: tlsroute/default/backend/rule/-1/backend/0
     - endpointConfig:
         '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
         clusterName: tcproute/default/backend/rule/-1
@@ -53,7 +56,8 @@ xds:
                   portValue: 3000
             loadBalancingWeight: 1
           loadBalancingWeight: 1
-          locality: {}
+          locality:
+            region: tcproute/default/backend/rule/-1/backend/0
     - endpointConfig:
         '@type': type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
         clusterName: udproute/default/backend/rule/-1
@@ -66,4 +70,5 @@ xds:
                   portValue: 3000
             loadBalancingWeight: 1
           loadBalancingWeight: 1
-          locality: {}
+          locality:
+            region: udproute/default/backend/rule/-1/backend/0
diff --git a/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.json b/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.json
index 11c9c3261..16c1d3522 100644
--- a/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.json
+++ b/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.json
@@ -235,7 +235,9 @@
                       }
                     ],
                     "loadBalancingWeight": 1,
-                    "locality": {}
+                    "locality": {
+                      "region": "httproute/envoy-gateway-system/backend/rule/0/backend/0"
+                    }
                   }
                 ]
               }
@@ -295,7 +297,9 @@
                         }
                       ],
                       "loadBalancingWeight": 1,
-                      "locality": {}
+                      "locality": {
+                        "region": "raw_githubusercontent_com_443/backend/0"
+                      }
                     }
                   ]
                 },
diff --git a/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.yaml b/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.yaml
index c5470eebb..8b70c138d 100644
--- a/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.yaml
+++ b/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.all.yaml
@@ -131,7 +131,8 @@ xds:
                     portValue: 3000
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: httproute/envoy-gateway-system/backend/rule/0/backend/0
     - '@type': type.googleapis.com/envoy.admin.v3.ClustersConfigDump
       dynamicActiveClusters:
       - cluster:
@@ -169,7 +170,8 @@ xds:
                       portValue: 443
                 loadBalancingWeight: 1
               loadBalancingWeight: 1
-              locality: {}
+              locality:
+                region: raw_githubusercontent_com_443/backend/0
           name: raw_githubusercontent_com_443
           outlierDetection: {}
           perConnectionBufferLimitBytes: 32768
diff --git a/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.cluster.yaml b/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.cluster.yaml
index aa29280bb..47f144cbe 100644
--- a/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.cluster.yaml
+++ b/internal/cmd/egctl/testdata/translate/out/jwt-single-route-single-match-to-xds.cluster.yaml
@@ -37,7 +37,8 @@ xds:
                     portValue: 443
               loadBalancingWeight: 1
             loadBalancingWeight: 1
-            locality: {}
+            locality:
+              region: raw_githubusercontent_com_443/backend/0
         name: raw_githubusercontent_com_443
         outlierDetection: {}
         perConnectionBufferLimitBytes: 32768
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml
index fd6c99deb..3cc8004e6 100644
--- a/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml
+++ b/internal/gatewayapi/testdata/securitypolicy-with-cors.in.yaml
@@ -77,7 +77,7 @@ securityPolicies:
     cors:
       allowOrigins:
       - type: RegularExpression
-        value: "*.example.com"
+        value: "FooBar[0-9]+"
       - type: Exact
         value: foo.bar.com
       allowMethods:
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml
index 9e2fc0e1f..153f26be9 100755
--- a/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml
+++ b/internal/gatewayapi/testdata/securitypolicy-with-cors.out.yaml
@@ -233,7 +233,7 @@ securityPolicies:
       - POST
       allowOrigins:
       - type: RegularExpression
-        value: '*.example.com'
+        value: FooBar[0-9]+
       - type: Exact
         value: foo.bar.com
       exposeHeaders:
@@ -278,7 +278,7 @@ xdsIR:
           allowOrigins:
           - distinct: false
             name: ""
-            safeRegex: '*.example.com'
+            safeRegex: FooBar[0-9]+
           - distinct: false
             exact: foo.bar.com
             name: ""
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.in.yaml
new file mode 100644
index 000000000..833c2b70c
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.in.yaml
@@ -0,0 +1,111 @@
+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:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/bar"
+      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
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      namespace: envoy-gateway
+    jwt:
+      providers:
+      - name: example1
+        issuer: https://one.example.com
+        audiences:
+        - one.foo.com
+        remoteJWKS:
+          uri: https://one.example.com/jwt/public-key/jwks.json
+        claimToHeaders:
+        - header: one-route-example-key
+          claim: claim1
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        name: "client1-secret"
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-for-http-route  # This policy should attach httproute-2
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-2
+      namespace: default
+    jwt:
+      providers:
+      - name: example2
+        issuer: https://two.example.com
+        audiences:
+        - two.foo.com
+        remoteJWKS:
+          uri: https://one.example.com/jwt/public-key/jwks.json
+        claimToHeaders:
+        - header: one-route-example-key
+          claim: claim2
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        name: "client2-secret"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml
new file mode 100755
index 000000000..5ed5a760a
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-jwt-and-invalid-oidc.out.yaml
@@ -0,0 +1,279 @@
+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: 2
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: 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
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /bar
+  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: ""
+        ports:
+        - containerPort: 10080
+          name: http
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-http-route
+    namespace: default
+  spec:
+    jwt:
+      providers:
+      - audiences:
+        - two.foo.com
+        claimToHeaders:
+        - claim: claim2
+          header: one-route-example-key
+        issuer: https://two.example.com
+        name: example2
+        remoteJWKS:
+          uri: https://one.example.com/jwt/public-key/jwks.json
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client2-secret
+      provider:
+        issuer: https://accounts.google.com
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-2
+      namespace: default
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: Secret default/client2-secret does not exist.
+      reason: Invalid
+      status: "False"
+      type: Accepted
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-gateway
+    namespace: envoy-gateway
+  spec:
+    jwt:
+      providers:
+      - audiences:
+        - one.foo.com
+        claimToHeaders:
+        - claim: claim1
+          header: one-route-example-key
+        issuer: https://one.example.com
+        name: example1
+        remoteJWKS:
+          uri: https://one.example.com/jwt/public-key/jwks.json
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client1-secret
+      provider:
+        issuer: https://accounts.google.com
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      namespace: envoy-gateway
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: Secret envoy-gateway/client1-secret does not exist.
+      reason: Invalid
+      status: "False"
+      type: Accepted
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      name: envoy-gateway/gateway-1/http
+      port: 10080
+      routes:
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        jwt:
+          providers:
+          - audiences:
+            - one.foo.com
+            claimToHeaders:
+            - claim: claim1
+              header: one-route-example-key
+            issuer: https://one.example.com
+            name: example1
+            remoteJWKS:
+              uri: https://one.example.com/jwt/public-key/jwks.json
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+          - endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        jwt:
+          providers:
+          - audiences:
+            - two.foo.com
+            claimToHeaders:
+            - claim: claim2
+              header: one-route-example-key
+            issuer: https://two.example.com
+            name: example2
+            remoteJWKS:
+              uri: https://one.example.com/jwt/public-key/jwks.json
+        name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /bar
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-issuer.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-issuer.in.yaml
new file mode 100644
index 000000000..9f49012c5
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-issuer.in.yaml
@@ -0,0 +1,40 @@
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: default
+    name: client1-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: default
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-non-exist-secretRef
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    oidc:
+      provider:
+        issuer: "https://httpbin.org/"
+      clientID: "client1.apps.foo.bar.com"
+      clientSecret:
+        name: "client1-secret"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-issuer.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-issuer.out.yaml
new file mode 100755
index 000000000..8e21d7060
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-issuer.out.yaml
@@ -0,0 +1,96 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: default
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 0
+      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
+infraIR:
+  default/gateway-1:
+    proxy:
+      listeners:
+      - address: ""
+        ports:
+        - containerPort: 10080
+          name: http
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: default
+      name: default/gateway-1
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-non-exist-secretRef
+    namespace: default
+  spec:
+    oidc:
+      clientID: client1.apps.foo.bar.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client1-secret
+      provider:
+        issuer: https://httpbin.org/
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: 'Error fetching endpoints from issuer: invalid character ''<'' looking
+        for beginning of value.'
+      reason: Invalid
+      status: "False"
+      type: Accepted
+xdsIR:
+  default/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      name: default/gateway-1/http
+      port: 10080
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-secretref.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-secretref.in.yaml
new file mode 100644
index 000000000..c3c86142e
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-secretref.in.yaml
@@ -0,0 +1,116 @@
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway
+    name: client2-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: default
+    name: client3-secret
+  data:
+    invalid_client_secret_key: Y2xpZW50MTpzZWNyZXQK
+
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: default
+    name: gateway-1
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: default
+    name: gateway-2
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    namespace: default
+    name: gateway-3
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - name: http
+      protocol: HTTP
+      port: 80
+      allowedRoutes:
+        namespaces:
+          from: All
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-non-exist-secretRef
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+        authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth2.googleapis.com/token"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        name: "client1-secret"
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-no-referenceGrant
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-2
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+        authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth2.googleapis.com/token"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        namespace: envoy-gateway
+        name: "client2-secret"
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-no-client-secret-key
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-3
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+        authorizationEndpoint: "https://accounts.google.com/o/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth2.googleapis.com/token"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        namespace: default
+        name: "client3-secret"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-secretref.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-secretref.out.yaml
new file mode 100755
index 000000000..5a30d9f3c
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc-invalid-secretref.out.yaml
@@ -0,0 +1,285 @@
+gateways:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-1
+    namespace: default
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 0
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-2
+    namespace: default
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 0
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: Gateway
+  metadata:
+    creationTimestamp: null
+    name: gateway-3
+    namespace: default
+  spec:
+    gatewayClassName: envoy-gateway-class
+    listeners:
+    - allowedRoutes:
+        namespaces:
+          from: All
+      name: http
+      port: 80
+      protocol: HTTP
+  status:
+    listeners:
+    - attachedRoutes: 0
+      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
+infraIR:
+  default/gateway-1:
+    proxy:
+      listeners:
+      - address: ""
+        ports:
+        - containerPort: 10080
+          name: http
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: default
+      name: default/gateway-1
+  default/gateway-2:
+    proxy:
+      listeners:
+      - address: ""
+        ports:
+        - containerPort: 10080
+          name: http
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-2
+          gateway.envoyproxy.io/owning-gateway-namespace: default
+      name: default/gateway-2
+  default/gateway-3:
+    proxy:
+      listeners:
+      - address: ""
+        ports:
+        - containerPort: 10080
+          name: http
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-3
+          gateway.envoyproxy.io/owning-gateway-namespace: default
+      name: default/gateway-3
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-non-exist-secretRef
+    namespace: default
+  spec:
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client1-secret
+      provider:
+        authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth
+        issuer: https://accounts.google.com
+        tokenEndpoint: https://oauth2.googleapis.com/token
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: Secret default/client1-secret does not exist.
+      reason: Invalid
+      status: "False"
+      type: Accepted
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-no-referenceGrant
+    namespace: default
+  spec:
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client2-secret
+        namespace: envoy-gateway
+      provider:
+        authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth
+        issuer: https://accounts.google.com
+        tokenEndpoint: https://oauth2.googleapis.com/token
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-2
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: Secret ref namespace must be unspecified/empty or default.
+      reason: Invalid
+      status: "False"
+      type: Accepted
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-no-client-secret-key
+    namespace: default
+  spec:
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client3-secret
+        namespace: default
+      provider:
+        authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth
+        issuer: https://accounts.google.com
+        tokenEndpoint: https://oauth2.googleapis.com/token
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-3
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: Client secret not found in secret default/client3-secret.
+      reason: Invalid
+      status: "False"
+      type: Accepted
+xdsIR:
+  default/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      name: default/gateway-1/http
+      port: 10080
+  default/gateway-2:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      name: default/gateway-2/http
+      port: 10080
+  default/gateway-3:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: false
+      name: default/gateway-3/http
+      port: 10080
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml
new file mode 100644
index 000000000..889fbfbfc
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.in.yaml
@@ -0,0 +1,148 @@
+secrets:
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: envoy-gateway
+    name: client1-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: default
+    name: client2-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+- apiVersion: v1
+  kind: Secret
+  metadata:
+    namespace: default
+    name: client3-secret
+  data:
+    client-secret: Y2xpZW50MTpzZWNyZXQK
+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:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/foo"
+      backendRefs:
+      - name: service-1
+        port: 8080
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    namespace: default
+    name: httproute-2
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - matches:
+      - path:
+          value: "/bar"
+      backendRefs:
+      - name: service-1
+        port: 8080
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    namespace: default
+    name: grpcroute-1
+  spec:
+    parentRefs:
+    - namespace: envoy-gateway
+      name: gateway-1
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: envoy-gateway
+    name: policy-for-gateway-discover-endpoints  # This policy should attach httproute-2
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      namespace: envoy-gateway
+    oidc:
+      provider:
+        issuer: "https://accounts.google.com"
+      clientID: "client1.apps.googleusercontent.com"
+      clientSecret:
+        name: "client1-secret"
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-for-http-route   # This policy should attach httproute-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      namespace: default
+    oidc:
+      provider:
+        issuer: "https://oauth.foo.com"
+        authorizationEndpoint: "https://oauth.foo.com/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth.foo.com/token"
+      clientID: "client2.oauth.foo.com"
+      clientSecret:
+        name: "client2-secret"
+      scopes: ["openid", "email", "profile"]
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    namespace: default
+    name: policy-cross-namespace-secretRef  # This policy should attach grpcroute-1
+  spec:
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-1
+    oidc:
+      provider:
+        issuer: "https://oauth.bar.com"
+        authorizationEndpoint: "https://oauth.bar.com/oauth2/v2/auth"
+        tokenEndpoint: "https://oauth.bar.com/token"
+      clientID: "client3.bar.foo.com"
+      clientSecret:
+        namespace: default
+        name: "client3-secret"
diff --git a/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml
new file mode 100755
index 000000000..a334ddd86
--- /dev/null
+++ b/internal/gatewayapi/testdata/securitypolicy-with-oidc.out.yaml
@@ -0,0 +1,346 @@
+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: 3
+      conditions:
+      - lastTransitionTime: null
+        message: Sending translated listener configuration to the data plane
+        reason: Programmed
+        status: "True"
+        type: Programmed
+      - lastTransitionTime: null
+        message: Listener has been successfully translated
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Listener references have been resolved
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      name: http
+      supportedKinds:
+      - group: gateway.networking.k8s.io
+        kind: HTTPRoute
+      - group: gateway.networking.k8s.io
+        kind: GRPCRoute
+grpcRoutes:
+- apiVersion: gateway.networking.k8s.io/v1alpha2
+  kind: GRPCRoute
+  metadata:
+    creationTimestamp: null
+    name: grpcroute-1
+    namespace: default
+  spec:
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+  status:
+    parents:
+    - conditions:
+      - lastTransitionTime: null
+        message: Route is accepted
+        reason: Accepted
+        status: "True"
+        type: Accepted
+      - lastTransitionTime: null
+        message: Resolved all the Object references for the Route
+        reason: ResolvedRefs
+        status: "True"
+        type: ResolvedRefs
+      controllerName: gateway.envoyproxy.io/gatewayclass-controller
+      parentRef:
+        name: gateway-1
+        namespace: envoy-gateway
+        sectionName: http
+httpRoutes:
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-1
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-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
+- apiVersion: gateway.networking.k8s.io/v1
+  kind: HTTPRoute
+  metadata:
+    creationTimestamp: null
+    name: httproute-2
+    namespace: default
+  spec:
+    hostnames:
+    - gateway.envoyproxy.io
+    parentRefs:
+    - name: gateway-1
+      namespace: envoy-gateway
+      sectionName: http
+    rules:
+    - backendRefs:
+      - name: service-1
+        port: 8080
+      matches:
+      - path:
+          value: /bar
+  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: ""
+        ports:
+        - containerPort: 10080
+          name: http
+          protocol: HTTP
+          servicePort: 80
+      metadata:
+        labels:
+          gateway.envoyproxy.io/owning-gateway-name: gateway-1
+          gateway.envoyproxy.io/owning-gateway-namespace: envoy-gateway
+      name: envoy-gateway/gateway-1
+securityPolicies:
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-http-route
+    namespace: default
+  spec:
+    oidc:
+      clientID: client2.oauth.foo.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client2-secret
+      provider:
+        authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+        issuer: https://oauth.foo.com
+        tokenEndpoint: https://oauth.foo.com/token
+      scopes:
+      - openid
+      - email
+      - profile
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: HTTPRoute
+      name: httproute-1
+      namespace: default
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: SecurityPolicy has been accepted.
+      reason: Accepted
+      status: "True"
+      type: Accepted
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-cross-namespace-secretRef
+    namespace: default
+  spec:
+    oidc:
+      clientID: client3.bar.foo.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client3-secret
+        namespace: default
+      provider:
+        authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth
+        issuer: https://oauth.bar.com
+        tokenEndpoint: https://oauth.bar.com/token
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: GRPCRoute
+      name: grpcroute-1
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: SecurityPolicy has been accepted.
+      reason: Accepted
+      status: "True"
+      type: Accepted
+- apiVersion: gateway.envoyproxy.io/v1alpha1
+  kind: SecurityPolicy
+  metadata:
+    creationTimestamp: null
+    name: policy-for-gateway-discover-endpoints
+    namespace: envoy-gateway
+  spec:
+    oidc:
+      clientID: client1.apps.googleusercontent.com
+      clientSecret:
+        group: null
+        kind: null
+        name: client1-secret
+      provider:
+        issuer: https://accounts.google.com
+    targetRef:
+      group: gateway.networking.k8s.io
+      kind: Gateway
+      name: gateway-1
+      namespace: envoy-gateway
+  status:
+    conditions:
+    - lastTransitionTime: null
+      message: SecurityPolicy has been accepted.
+      reason: Accepted
+      status: "True"
+      type: Accepted
+xdsIR:
+  envoy-gateway/gateway-1:
+    accessLog:
+      text:
+      - path: /dev/stdout
+    http:
+    - address: 0.0.0.0
+      hostnames:
+      - '*'
+      isHTTP2: true
+      name: envoy-gateway/gateway-1/http
+      port: 10080
+      routes:
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-1/rule/0
+          settings:
+          - endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        name: httproute/default/httproute-1/rule/0/match/0/gateway_envoyproxy_io
+        oidc:
+          clientID: client2.oauth.foo.com
+          clientSecret: Y2xpZW50MTpzZWNyZXQK
+          provider:
+            authorizationEndpoint: https://oauth.foo.com/oauth2/v2/auth
+            issuer: https://oauth.foo.com
+            tokenEndpoint: https://oauth.foo.com/token
+          scopes:
+          - openid
+          - email
+          - profile
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /foo
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: httproute/default/httproute-2/rule/0
+          settings:
+          - endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: HTTP
+            weight: 1
+        hostname: gateway.envoyproxy.io
+        name: httproute/default/httproute-2/rule/0/match/0/gateway_envoyproxy_io
+        oidc:
+          clientID: client1.apps.googleusercontent.com
+          clientSecret: Y2xpZW50MTpzZWNyZXQK
+          provider:
+            authorizationEndpoint: https://accounts.google.com/o/oauth2/v2/auth
+            issuer: https://accounts.google.com
+            tokenEndpoint: https://oauth2.googleapis.com/token
+          scopes:
+          - openid
+        pathMatch:
+          distinct: false
+          name: ""
+          prefix: /bar
+      - backendWeights:
+          invalid: 0
+          valid: 0
+        destination:
+          name: grpcroute/default/grpcroute-1/rule/0
+          settings:
+          - endpoints:
+            - host: 7.7.7.7
+              port: 8080
+            protocol: GRPC
+            weight: 1
+        hostname: '*'
+        name: grpcroute/default/grpcroute-1/rule/0/match/-1/*
+        oidc:
+          clientID: client3.bar.foo.com
+          clientSecret: Y2xpZW50MTpzZWNyZXQK
+          provider:
+            authorizationEndpoint: https://oauth.bar.com/oauth2/v2/auth
+            issuer: https://oauth.bar.com
+            tokenEndpoint: https://oauth.bar.com/token
+          scopes:
+          - openid
diff --git a/internal/gatewayapi/validate_test.go b/internal/gatewayapi/validate_test.go
new file mode 100644
index 000000000..86dd93170
--- /dev/null
+++ b/internal/gatewayapi/validate_test.go
@@ -0,0 +1,38 @@
+// 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 gatewayapi
+
+import (
+	"testing"
+)
+
+func Test_validateRegex(t *testing.T) {
+	tests := []struct {
+		name    string
+		regex   string
+		wantErr bool
+	}{
+		{
+			name:    "valid regex",
+			regex:   "^[a-zA-Z0-9-]+$",
+			wantErr: false,
+		},
+		{
+			name:    "invalid regex",
+			regex:   "*.foo.com",
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := validateRegex(tt.regex)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("validateRegex() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+		})
+	}
+}
diff --git a/internal/status/conditions_test.go b/internal/status/conditions_test.go
index 69a67590a..e1a24821f 100644
--- a/internal/status/conditions_test.go
+++ b/internal/status/conditions_test.go
@@ -14,6 +14,7 @@
 package status
 
 import (
+	"errors"
 	"testing"
 	"time"
 
@@ -341,3 +342,27 @@ func TestGatewayReadyCondition(t *testing.T) {
 		})
 	}
 }
+
+func TestError2ConditionMsg(t *testing.T) {
+	testCases := []struct {
+		name   string
+		err    error
+		expect string
+	}{
+		{
+			name:   "nil error",
+			err:    nil,
+			expect: "",
+		},
+		{
+			name:   "error with message",
+			err:    errors.New("something is wrong"),
+			expect: "Something is wrong.",
+		},
+	}
+	for _, tt := range testCases {
+		t.Run(tt.name, func(t *testing.T) {
+			assert.Equalf(t, tt.expect, Error2ConditionMsg(tt.err), "Error2ConditionMsg(%v)", tt.err)
+		})
+	}
+}
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/http-route-extension-filter.endpoints.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/http-route-extension-filter.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/extension-xds-ir/http-route-extension-filter.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/http-route-extension-filter.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/extension-xds-ir/http-route.endpoints.yaml b/internal/xds/translator/testdata/out/extension-xds-ir/http-route.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/extension-xds-ir/http-route.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/extension-xds-ir/http-route.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog.clusters.yaml
index bfce87930..1ba3e1d70 100644
--- a/internal/xds/translator/testdata/out/xds-ir/accesslog.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/accesslog.clusters.yaml
@@ -29,7 +29,8 @@
               portValue: 4317
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: accesslog|otel-collector.default.svc.cluster.local|4317/backend/0
   name: accesslog|otel-collector.default.svc.cluster.local|4317
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/accesslog.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/accesslog.endpoints.yaml
index 7a3b51ea8..20c80b3aa 100644
--- a/internal/xds/translator/testdata/out/xds-ir/accesslog.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/accesslog.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: direct-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/cors.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/cors.endpoints.yaml
index 0d68b430c..3b3f2d090 100755
--- a/internal/xds/translator/testdata/out/xds-ir/cors.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/cors.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.endpoints.yaml
index 7a3b51ea8..20c80b3aa 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-direct-response.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: direct-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-mirror.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-mirror.endpoints.yaml
index df6982a4f..43cba2e79 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-mirror.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-mirror.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-matches.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-matches.endpoints.yaml
index 855f08115..3787880a3 100755
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-matches.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-matches.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,7 +32,8 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
 - clusterName: fourth-route-dest
   endpoints:
   - lbEndpoints:
@@ -41,7 +44,8 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: fourth-route-dest/backend/0
 - clusterName: fifth-route-dest
   endpoints:
   - lbEndpoints:
@@ -52,7 +56,8 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: fifth-route-dest/backend/0
 - clusterName: sixth-route-dest
   endpoints:
   - lbEndpoints:
@@ -63,7 +68,8 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: sixth-route-dest/backend/0
 - clusterName: seventh-route-dest
   endpoints:
   - lbEndpoints:
@@ -74,4 +80,5 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: seventh-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.endpoints.yaml
index a30f56000..f741af39c 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-multiple-mirrors.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: route-dest/backend/0
 - clusterName: mirror-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 0
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: mirror-route-dest/backend/0
 - clusterName: mirror-route-dest1
   endpoints:
   - lbEndpoints:
@@ -30,4 +32,5 @@
             portValue: 0
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: mirror-route-dest1/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.endpoints.yaml
index 9ba005e0e..7543b702f 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-redirect.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: redirect-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-regex.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-regex.endpoints.yaml
index e089bec79..b36ee4500 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-regex.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-regex.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: regex-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.endpoints.yaml
index f41227e38..846a89074 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-request-headers.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: request-header-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-headers.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-headers.endpoints.yaml
index 46cf8b1f9..d21491f62 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-headers.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-headers.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: response-header-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-remove-headers.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-remove-headers.endpoints.yaml
index 46cf8b1f9..d21491f62 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-remove-headers.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-response-add-remove-headers.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: response-header-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-response-remove-headers.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-response-remove-headers.endpoints.yaml
index 46cf8b1f9..d21491f62 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-response-remove-headers.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-response-remove-headers.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: response-header-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.endpoints.yaml
index e28246ea4..256dda090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-root-path-url-prefix.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: rewrite-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.endpoints.yaml
index bc3633dd7..ca1ef21c9 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-fullpath.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: rewrite-route/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.endpoints.yaml
index e28246ea4..256dda090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-host.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: rewrite-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.endpoints.yaml
index e28246ea4..256dda090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-rewrite-url-prefix.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: rewrite-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-timeout.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-timeout.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-timeout.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-timeout.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.endpoints.yaml
index 52ade73ee..f5633bd15 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-backend.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 20
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
   - lbEndpoints:
     - endpoint:
         address:
@@ -17,7 +18,8 @@
             portValue: 50002
       loadBalancingWeight: 1
     loadBalancingWeight: 40
-    locality: {}
+    locality:
+      region: first-route-dest/backend/1
   - lbEndpoints:
     - endpoint:
         address:
@@ -26,7 +28,8 @@
             portValue: 50003
       loadBalancingWeight: 1
     loadBalancingWeight: 20
-    locality: {}
+    locality:
+      region: first-route-dest/backend/2
   - lbEndpoints:
     - endpoint:
         address:
@@ -35,4 +38,5 @@
             portValue: 50004
       loadBalancingWeight: 1
     loadBalancingWeight: 20
-    locality: {}
+    locality:
+      region: first-route-dest/backend/3
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route-weighted-invalid-backend.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http-route.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http-route.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http-route.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http-route.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/http2-route.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/http2-route.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/http2-route.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/http2-route.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-invalid-patch.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-invalid-patch.endpoints.yaml
index 0d68b430c..3b3f2d090 100755
--- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-invalid-patch.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-invalid-patch.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-missing-resource.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-missing-resource.endpoints.yaml
index 0d68b430c..3b3f2d090 100755
--- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch-missing-resource.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch-missing-resource.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jsonpatch.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jsonpatch.endpoints.yaml
index 2441b6fa6..a9249b45d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jsonpatch.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jsonpatch.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 50
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.clusters.yaml
index bf7f01095..fd188612f 100755
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.clusters.yaml
@@ -43,7 +43,8 @@
               portValue: 443
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: localhost_443/backend/0
   name: localhost_443
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.endpoints.yaml
index 7d394e5f4..6731cfd3f 100755
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-multi-provider.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-www.test.com-dest/backend/0
 - clusterName: second-route-www.test.com-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-www.test.com-dest/backend/0
 - clusterName: "192_168_1_250_8080"
   endpoints:
   - lbEndpoints:
@@ -30,4 +32,5 @@
             portValue: 8080
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: 192_168_1_250_8080/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.clusters.yaml
index 35bc85eaf..f18c018dd 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.clusters.yaml
@@ -43,7 +43,8 @@
               portValue: 443
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: localhost_443/backend/0
   name: localhost_443
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.endpoints.yaml
index b321ca1f8..39102da40 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-multi-route-single-provider.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,4 +20,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.clusters.yaml
index 868cdb1b6..e7c4315a9 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.clusters.yaml
@@ -57,7 +57,8 @@
               portValue: 8081
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: ratelimit_cluster/backend/0
   name: ratelimit_cluster
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.endpoints.yaml
index d8d04b171..73130b205 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-ratelimit.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,7 +32,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
 - clusterName: "192_168_1_250_443"
   endpoints:
   - lbEndpoints:
@@ -41,4 +44,5 @@
             portValue: 443
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: 192_168_1_250_443/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.clusters.yaml
index 7a0c93317..7c8285ba0 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.clusters.yaml
@@ -29,7 +29,8 @@
               portValue: 443
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: localhost_443/backend/0
   name: localhost_443
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/jwt-single-route-single-match.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/listener-tcp-keepalive.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/listener-tcp-keepalive.endpoints.yaml
index 1ef9c4df3..5b4fe89e5 100755
--- a/internal/xds/translator/testdata/out/xds-ir/listener-tcp-keepalive.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/listener-tcp-keepalive.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: tls-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,7 +32,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tls-route-dest/backend/0
 - clusterName: tcp-route-dest
   endpoints:
   - lbEndpoints:
@@ -41,4 +44,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml
index ee35a1bf5..f185af17d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/load-balancer.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,7 +32,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
 - clusterName: fourth-route-dest
   endpoints:
   - lbEndpoints:
@@ -41,4 +44,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: fourth-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/metrics-virtual-host.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/metrics-virtual-host.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/metrics-virtual-host.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/metrics-virtual-host.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.endpoints.yaml
index 766e56caf..ceed007af 100644
--- a/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-listeners-same-port.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,7 +32,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
 - clusterName: fourth-route-dest
   endpoints:
   - lbEndpoints:
@@ -41,7 +44,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: fourth-route-dest/backend/0
 - clusterName: tcp-route-dest
   endpoints:
   - lbEndpoints:
@@ -52,7 +56,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-dest/backend/0
 - clusterName: tls-route-dest
   endpoints:
   - lbEndpoints:
@@ -63,4 +68,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tls-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.endpoints.yaml
index f42e3ffc0..8d9b5f227 100644
--- a/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/multiple-simple-tcp-route-same-port.endpoints.yaml
@@ -14,7 +14,8 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-simple-dest/backend/0
 - clusterName: tcp-route-simple-1-dest
   endpoints:
   - lbEndpoints:
@@ -31,7 +32,8 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-simple-1-dest/backend/0
 - clusterName: tcp-route-simple-2-dest
   endpoints:
   - lbEndpoints:
@@ -48,7 +50,8 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-simple-2-dest/backend/0
 - clusterName: tcp-route-simple-3-dest
   endpoints:
   - lbEndpoints:
@@ -65,7 +68,8 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-simple-3-dest/backend/0
 - clusterName: tcp-route-simple-4-dest
   endpoints:
   - lbEndpoints:
@@ -82,4 +86,5 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-simple-4-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.clusters.yaml
index 5dfa69f2a..73b916d5c 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.clusters.yaml
@@ -57,7 +57,8 @@
               portValue: 8081
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: ratelimit_cluster/backend/0
   name: ratelimit_cluster
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.endpoints.yaml
index 40ed98fc3..475b89a08 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-custom-domain.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,4 +32,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.clusters.yaml
index 74d2022fc..abe58dd36 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.clusters.yaml
@@ -71,7 +71,8 @@
               portValue: 8081
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: ratelimit_cluster/backend/0
   name: ratelimit_cluster
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.endpoints.yaml
index ee35a1bf5..f185af17d 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit-sourceip.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,7 +32,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
 - clusterName: fourth-route-dest
   endpoints:
   - lbEndpoints:
@@ -41,4 +44,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: fourth-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml
index aa0ad27be..1ef371007 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.clusters.yaml
@@ -57,7 +57,8 @@
               portValue: 8081
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: ratelimit_cluster/backend/0
   name: ratelimit_cluster
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/ratelimit.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/ratelimit.endpoints.yaml
index 40ed98fc3..475b89a08 100644
--- a/internal/xds/translator/testdata/out/xds-ir/ratelimit.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/ratelimit.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
 - clusterName: second-route-dest
   endpoints:
   - lbEndpoints:
@@ -19,7 +20,8 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: second-route-dest/backend/0
 - clusterName: third-route-dest
   endpoints:
   - lbEndpoints:
@@ -30,4 +32,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: third-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/simple-tls.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/simple-tls.endpoints.yaml
index 0d68b430c..3b3f2d090 100644
--- a/internal/xds/translator/testdata/out/xds-ir/simple-tls.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/simple-tls.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: first-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.endpoints.yaml
index debb1b5ae..f1cd2f6a2 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-complex.endpoints.yaml
@@ -14,4 +14,5 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-complex-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.endpoints.yaml
index c89c9e07e..7eb06a08f 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-simple.endpoints.yaml
@@ -14,4 +14,5 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tcp-route-simple-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-tls-terminate.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-tls-terminate.endpoints.yaml
index 8eeae02c3..164abc9de 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tcp-route-tls-terminate.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-tls-terminate.endpoints.yaml
@@ -14,4 +14,5 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tls-terminate-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.endpoints.yaml
index 0b651a80a..73db3a800 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tcp-route-weighted-backend.endpoints.yaml
@@ -8,7 +8,8 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 20
-    locality: {}
+    locality:
+      region: tcp-route-weighted-backend-dest/backend/0
   - lbEndpoints:
     - endpoint:
         address:
@@ -17,7 +18,8 @@
             portValue: 50002
       loadBalancingWeight: 1
     loadBalancingWeight: 40
-    locality: {}
+    locality:
+      region: tcp-route-weighted-backend-dest/backend/1
   - lbEndpoints:
     - endpoint:
         address:
@@ -26,7 +28,8 @@
             portValue: 50003
       loadBalancingWeight: 1
     loadBalancingWeight: 20
-    locality: {}
+    locality:
+      region: tcp-route-weighted-backend-dest/backend/2
   - lbEndpoints:
     - endpoint:
         address:
@@ -35,4 +38,5 @@
             portValue: 50004
       loadBalancingWeight: 1
     loadBalancingWeight: 20
-    locality: {}
+    locality:
+      region: tcp-route-weighted-backend-dest/backend/3
diff --git a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.endpoints.yaml
index 9c5e334db..ec2795413 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tls-route-passthrough.endpoints.yaml
@@ -14,4 +14,5 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: tls-passthrough-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing.clusters.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing.clusters.yaml
index ebfeca4ad..2053a4bde 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tracing.clusters.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tracing.clusters.yaml
@@ -29,7 +29,8 @@
               portValue: 4317
         loadBalancingWeight: 1
       loadBalancingWeight: 1
-      locality: {}
+      locality:
+        region: tracing|otel-collector.monitoring.svc.cluster.local|4317/backend/0
   name: tracing|otel-collector.monitoring.svc.cluster.local|4317
   outlierDetection: {}
   perConnectionBufferLimitBytes: 32768
diff --git a/internal/xds/translator/testdata/out/xds-ir/tracing.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/tracing.endpoints.yaml
index 7a3b51ea8..20c80b3aa 100644
--- a/internal/xds/translator/testdata/out/xds-ir/tracing.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/tracing.endpoints.yaml
@@ -8,4 +8,5 @@
             portValue: 50000
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: direct-route-dest/backend/0
diff --git a/internal/xds/translator/testdata/out/xds-ir/udp-route.endpoints.yaml b/internal/xds/translator/testdata/out/xds-ir/udp-route.endpoints.yaml
index 3719bc0b1..2e3c84e67 100644
--- a/internal/xds/translator/testdata/out/xds-ir/udp-route.endpoints.yaml
+++ b/internal/xds/translator/testdata/out/xds-ir/udp-route.endpoints.yaml
@@ -14,4 +14,5 @@
             portValue: 50001
       loadBalancingWeight: 1
     loadBalancingWeight: 1
-    locality: {}
+    locality:
+      region: udp-route-dest/backend/0
diff --git a/site/content/en/latest/api/extension_types.md b/site/content/en/latest/api/extension_types.md
index 174ab89e4..33a6a43dc 100644
--- a/site/content/en/latest/api/extension_types.md
+++ b/site/content/en/latest/api/extension_types.md
@@ -363,7 +363,7 @@ _Appears in:_
 | `rateLimitDeployment` _[KubernetesDeploymentSpec](#kubernetesdeploymentspec)_ | RateLimitDeployment defines the desired state of the Envoy ratelimit deployment resource. If unspecified, default settings for the managed Envoy ratelimit deployment resource are applied. |
 | `watch` _[KubernetesWatchMode](#kuberneteswatchmode)_ | Watch holds configuration of which input resources should be watched and reconciled. |
 | `deploy` _[KubernetesDeployMode](#kubernetesdeploymode)_ | Deploy holds configuration of how output managed resources such as the Envoy Proxy data plane should be deployed |
-| `overwrite_control_plane_certs` _boolean_ | OverwriteControlPlaneCerts updates the secrets containing the control plane certs, when set. |
+| `overwriteControlPlaneCerts` _boolean_ | OverwriteControlPlaneCerts updates the secrets containing the control plane certs, when set. |
 
 
 #### EnvoyGatewayLogComponent
@@ -1071,6 +1071,44 @@ _Appears in:_
 
 
 
+#### OIDC
+
+
+
+OIDC defines the configuration for the OpenID Connect (OIDC) authentication.
+
+_Appears in:_
+- [SecurityPolicySpec](#securitypolicyspec)
+
+| Field | Description |
+| --- | --- |
+| `provider` _[OIDCProvider](#oidcprovider)_ | The OIDC Provider configuration. |
+| `clientID` _string_ | The client ID to be used in the OIDC [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). |
+| `clientSecret` _[SecretObjectReference](https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.SecretObjectReference)_ | The Kubernetes secret which contains the OIDC client secret to be used in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). 
+ This is an Opaque secret. The client secret should be stored in the key "client-secret". |
+| `scopes` _string array_ | The OIDC scopes to be used in the [Authentication Request](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest). The "openid" scope is always added to the list of scopes if not already specified. |
+
+
+#### OIDCProvider
+
+
+
+OIDCProvider defines the OIDC Provider configuration. To make the EG OIDC config easy to use, some of the low-level ouath2 filter configuration knobs are hidden from the user, and default values will be provided when translating to XDS. For example: 
+ * redirect_uri: uses a default redirect URI "%REQ(x-forwarded-proto)%://%REQ(:authority)%/oauth2/callback" 
+ * signout_path: uses a default signout path "/signout" 
+ * redirect_path_matcher: uses a default redirect path matcher "/oauth2/callback" 
+ If we get requests to expose these knobs, we can always do so later.
+
+_Appears in:_
+- [OIDC](#oidc)
+
+| Field | Description |
+| --- | --- |
+| `issuer` _string_ | The OIDC Provider's [issuer identifier](https://openid.net/specs/openid-connect-discovery-1_0.html#IssuerDiscovery). Issuer MUST be a URI RFC 3986 [RFC3986] with a scheme component that MUST be https, a host component, and optionally, port and path components and no query or fragment components. |
+| `authorizationEndpoint` _string_ | The OIDC Provider's [authorization endpoint](https://openid.net/specs/openid-connect-core-1_0.html#AuthorizationEndpoint). If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). |
+| `tokenEndpoint` _string_ | The OIDC Provider's [token endpoint](https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint). If not provided, EG will try to discover it from the provider's [Well-Known Configuration Endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationResponse). |
+
+
 #### OpenTelemetryEnvoyProxyAccessLog
 
 
@@ -1160,7 +1198,7 @@ _Appears in:_
 
 
 
-
+ProxyAccessLogSink defines the sink of accesslog.
 
 _Appears in:_
 - [ProxyAccessLogSetting](#proxyaccesslogsetting)
@@ -1227,7 +1265,7 @@ _Appears in:_
 
 
 
-
+ProxyMetricSink defines the sink of metrics. Default metrics sink is OpenTelemetry.
 
 _Appears in:_
 - [ProxyMetrics](#proxymetrics)
@@ -1557,6 +1595,7 @@ _Appears in:_
 | `targetRef` _[PolicyTargetReferenceWithSectionName](#policytargetreferencewithsectionname)_ | TargetRef is the name of the Gateway resource this policy is being attached to. This Policy and the TargetRef MUST be in the same namespace for this Policy to have effect and be applied to the Gateway. TargetRef |
 | `cors` _[CORS](#cors)_ | CORS defines the configuration for Cross-Origin Resource Sharing (CORS). |
 | `jwt` _[JWT](#jwt)_ | JWT defines the configuration for JSON Web Token (JWT) authentication. |
+| `oidc` _[OIDC](#oidc)_ | OIDC defines the configuration for the OpenID Connect (OIDC) authentication. |
 
 
 
diff --git a/site/content/en/latest/install/api.md b/site/content/en/latest/install/api.md
index 253d528bd..9e2d9e91d 100644
--- a/site/content/en/latest/install/api.md
+++ b/site/content/en/latest/install/api.md
@@ -32,7 +32,6 @@ The Helm chart for Envoy Gateway
 | config.envoyGateway.logging.level.default | string | `"info"` |  |
 | config.envoyGateway.provider.type | string | `"Kubernetes"` |  |
 | createNamespace | bool | `false` |  |
-| deployment.envoyGateway.cert.expiryDays | int | `365` |  |
 | deployment.envoyGateway.image.repository | string | `"${ImageRepository}"` |  |
 | deployment.envoyGateway.image.tag | string | `"${ImageTag}"` |  |
 | deployment.envoyGateway.imagePullPolicy | string | `"Always"` |  |
diff --git a/site/content/en/latest/user/http-redirect.md b/site/content/en/latest/user/http-redirect.md
index da61bdaf3..1ef88e53f 100644
--- a/site/content/en/latest/user/http-redirect.md
+++ b/site/content/en/latest/user/http-redirect.md
@@ -9,7 +9,7 @@ learn more about HTTP routing, refer to the [Gateway API documentation][].
 
 ## Prerequisites
 
-Follow the steps from the [Secure Gateways](secure-gateways.md) to install Envoy Gateway and the example manifest.
+Follow the steps from the [Quickstart](../quickstart) to install Envoy Gateway and the example manifest.
 Before proceeding, you should be able to query the example backend using HTTPS.
 
 ## Redirects
@@ -21,7 +21,7 @@ For example, to issue a permanent redirect (301) from HTTP to HTTPS, configure `
 
 ```shell
 cat <<EOF | kubectl apply -f -
-apiVersion: gateway.networking.k8s.io/v1beta1
+apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: http-to-https-filter-redirect
@@ -72,6 +72,132 @@ $ curl -L -vvv --header "Host: redirect.example" "http://${GATEWAY_HOST}/get"
 If you followed the steps in the [Secure Gateways](secure-gateways.md) guide, you should be able to curl the redirect
 location.
 
+## HTTP --> HTTPS
+
+Listeners expose the TLS setting on a per domain or subdomain basis. TLS settings of a listener are applied to all domains that satisfy the hostname criteria.
+
+Create a root certificate and private key to sign certificates:
+
+```shell
+openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/CN=example.com' -keyout CA.key -out CA.crt
+openssl req -out example.com.csr -newkey rsa:2048 -nodes -keyout tls.key -subj "/CN=example.com"
+```
+
+Generate a self-signed wildcard certificate for `example.com` with `*.example.com` extension
+
+```shell
+cat <<EOF | openssl x509 -req -days 365 -CA CA.crt -CAkey CA.key -set_serial 0 \
+-subj "/CN=example.com" \
+-in example.com.csr -out tls.crt -extensions v3_req  -extfile -
+[v3_req]
+keyUsage = keyEncipherment, dataEncipherment
+extendedKeyUsage = serverAuth
+subjectAltName = @alt_names
+[alt_names]
+DNS.1   = example.com
+DNS.2   = *.example.com
+EOF
+```
+
+Create the kubernetes tls secret
+
+```shell
+kubectl create secret tls example-com --key=tls.key --cert=tls.crt
+```
+
+Define a https listener on the existing gateway
+
+```shell
+cat <<EOF | kubectl apply -n default -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: Gateway
+metadata:
+  name: eg
+spec:
+  gatewayClassName: eg
+  listeners:
+  - name: http
+    port: 80
+    protocol: HTTP
+    # hostname: "*.example.com"
+  - name: https
+    port: 443
+    protocol: HTTPS
+    # hostname: "*.example.com"
+    tls:
+      mode: Terminate
+      certificateRefs:
+      - kind: Secret
+        name: example-com
+EOF
+```
+
+Check for any TLS certificate issues on the gateway.
+
+```bash
+kubectl -n default describe gateway eg
+```
+
+Create two HTTPRoutes and attach them to the HTTP and HTTPS listeners using the [sectionName][] field.
+
+
+```shell
+cat <<EOF | kubectl apply -n default -f -
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: tls-redirect
+spec:
+  parentRefs:
+    - name: eg
+      sectionName: http
+  hostnames:
+    # - "*.example.com" # catch all hostnames
+    - "www.example.com"
+  rules:
+    - filters:
+        - type: RequestRedirect
+          requestRedirect:
+            scheme: https
+---
+apiVersion: gateway.networking.k8s.io/v1
+kind: HTTPRoute
+metadata:
+  name: backend
+spec:
+  parentRefs:
+    - name: eg
+      sectionName: https
+  hostnames:
+    - "www.example.com"
+  rules:
+    - backendRefs:
+        - group: ""
+          kind: Service
+          name: backend
+          port: 3000
+          weight: 1
+      matches:
+        - path:
+            type: PathPrefix
+            value: /
+EOF
+```
+
+Curl the example app through http listener:
+
+```bash
+curl --verbose --header "Host: www.example.com" http://$GATEWAY_HOST/get
+```
+
+Curl the example app through https listener:
+
+```bash
+curl -v -H 'Host:www.example.com' --resolve "www.example.com:443:$GATEWAY_HOST" \
+--cacert CA.crt https://www.example.com:443/get
+```
+
+
 ## Path Redirects
 
 Path redirects use an HTTP Path Modifier to replace either entire paths or path prefixes. For example, the HTTPRoute
@@ -79,7 +205,7 @@ below will issue a 302 redirect to all `path.redirect.example` requests whose pa
 
 ```shell
 cat <<EOF | kubectl apply -f -
-apiVersion: gateway.networking.k8s.io/v1beta1
+apiVersion: gateway.networking.k8s.io/v1
 kind: HTTPRoute
 metadata:
   name: http-filter-path-redirect
@@ -124,6 +250,7 @@ curl -vvv --header "Host: path.redirect.example" "http://${GATEWAY_HOST}/get"
 You should receive a `302` with a redirect location of `http://path.redirect.example/status/200`.
 
 [HTTPRoute]: https://gateway-api.sigs.k8s.io/api-types/httproute/
-[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRouteFilter
+[HTTPRoute filters]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRouteFilter
 [Gateway API documentation]: https://gateway-api.sigs.k8s.io/
-[req_filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1beta1.HTTPRequestRedirectFilter
+[req_filter]: https://gateway-api.sigs.k8s.io/references/spec/#gateway.networking.k8s.io/v1.HTTPRequestRedirectFilter
+[sectionName]: https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io/v1.CommonRouteSpec
diff --git a/test/cel-validation/envoyproxy_test.go b/test/cel-validation/envoyproxy_test.go
index 1d029655f..ac949ff97 100644
--- a/test/cel-validation/envoyproxy_test.go
+++ b/test/cel-validation/envoyproxy_test.go
@@ -38,10 +38,8 @@ func TestEnvoyProxyProvider(t *testing.T) {
 		wantErrors   []string
 	}{
 		{
-			desc: "nil provider",
-			mutate: func(envoy *egv1a1.EnvoyProxy) {
-
-			},
+			desc:       "nil provider",
+			mutate:     func(envoy *egv1a1.EnvoyProxy) {},
 			wantErrors: []string{},
 		},
 		{
@@ -204,6 +202,223 @@ func TestEnvoyProxyProvider(t *testing.T) {
 			},
 			wantErrors: []string{"loadBalancerIP can only be set for LoadBalancer type"},
 		},
+		{
+			desc: "invalid-ProxyAccessLogFormat",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: "foo",
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"Unsupported value: \"foo\": supported values: \"Text\", \"JSON\""},
+		},
+		{
+			desc: "ProxyAccessLogFormat-with-TypeText-but-no-text",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: egv1a1.ProxyAccessLogFormatTypeText,
+									},
+									Sinks: []egv1a1.ProxyAccessLogSink{
+										{
+											Type: egv1a1.ProxyAccessLogSinkTypeFile,
+											File: &egv1a1.FileEnvoyProxyAccessLog{
+												Path: "foo/bar",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"If AccessLogFormat type is Text, text field needs to be set"},
+		},
+		{
+			desc: "ProxyAccessLogFormat-with-TypeJSON-but-no-json",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: egv1a1.ProxyAccessLogFormatTypeJSON,
+									},
+									Sinks: []egv1a1.ProxyAccessLogSink{
+										{
+											Type: egv1a1.ProxyAccessLogSinkTypeFile,
+											File: &egv1a1.FileEnvoyProxyAccessLog{
+												Path: "foo/bar",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"If AccessLogFormat type is JSON, json field needs to be set"},
+		},
+		{
+			desc: "ProxyAccessLogFormat-with-TypeJSON-but-got-text",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: egv1a1.ProxyAccessLogFormatTypeJSON,
+										Text: ptr.To("[%START_TIME%]"),
+									},
+									Sinks: []egv1a1.ProxyAccessLogSink{
+										{
+											Type: egv1a1.ProxyAccessLogSinkTypeFile,
+											File: &egv1a1.FileEnvoyProxyAccessLog{
+												Path: "foo/bar",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"If AccessLogFormat type is JSON, json field needs to be set"},
+		},
+		{
+			desc: "ProxyAccessLogSink-with-TypeFile-but-no-file",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: egv1a1.ProxyAccessLogFormatTypeText,
+										Text: ptr.To("[%START_TIME%]"),
+									},
+									Sinks: []egv1a1.ProxyAccessLogSink{
+										{
+											Type: egv1a1.ProxyAccessLogSinkTypeFile,
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"If AccessLogSink type is File, file field needs to be set"},
+		},
+		{
+			desc: "ProxyAccessLogSink-with-TypeOpenTelemetry-but-no-openTelemetry",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: egv1a1.ProxyAccessLogFormatTypeText,
+										Text: ptr.To("[%START_TIME%]"),
+									},
+									Sinks: []egv1a1.ProxyAccessLogSink{
+										{
+											Type: egv1a1.ProxyAccessLogSinkTypeOpenTelemetry,
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"If AccessLogSink type is OpenTelemetry, openTelemetry field needs to be set"},
+		},
+		{
+			desc: "ProxyAccessLog-settings-pass",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						AccessLog: &egv1a1.ProxyAccessLog{
+							Settings: []egv1a1.ProxyAccessLogSetting{
+								{
+									Format: egv1a1.ProxyAccessLogFormat{
+										Type: egv1a1.ProxyAccessLogFormatTypeText,
+										Text: ptr.To("[%START_TIME%]"),
+									},
+									Sinks: []egv1a1.ProxyAccessLogSink{
+										{
+											Type: egv1a1.ProxyAccessLogSinkTypeFile,
+											File: &egv1a1.FileEnvoyProxyAccessLog{
+												Path: "foo/bar",
+											},
+										},
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{},
+		},
+		{
+			desc: "ProxyMetricSink-with-TypeOpenTelemetry-but-no-openTelemetry",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						Metrics: &egv1a1.ProxyMetrics{
+							Sinks: []egv1a1.ProxyMetricSink{
+								{
+									Type: egv1a1.MetricSinkTypeOpenTelemetry,
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{"If MetricSink type is OpenTelemetry, openTelemetry field needs to be set"},
+		},
+		{
+			desc: "ProxyMetrics-sinks-pass",
+			mutate: func(envoy *egv1a1.EnvoyProxy) {
+				envoy.Spec = egv1a1.EnvoyProxySpec{
+					Telemetry: &egv1a1.ProxyTelemetry{
+						Metrics: &egv1a1.ProxyMetrics{
+							Sinks: []egv1a1.ProxyMetricSink{
+								{
+									Type: egv1a1.MetricSinkTypeOpenTelemetry,
+									OpenTelemetry: &egv1a1.ProxyOpenTelemetrySink{
+										Host: "0.0.0.0",
+										Port: 3217,
+									},
+								},
+							},
+						},
+					},
+				}
+			},
+			wantErrors: []string{},
+		},
 	}
 
 	for _, tc := range cases {
diff --git a/test/e2e/base/manifests.yaml b/test/e2e/base/manifests.yaml
index a4f39429e..552e0eeb4 100644
--- a/test/e2e/base/manifests.yaml
+++ b/test/e2e/base/manifests.yaml
@@ -414,3 +414,89 @@ spec:
         resources:
           requests:
             cpu: 10m
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: gateway-conformance-udp
+  labels:
+    gateway-conformance: udp
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coredns
+  namespace: gateway-conformance-udp
+  labels:
+    app: udp
+spec:
+  ports:
+  - name: udp-dns
+    port: 53
+    protocol: UDP
+    targetPort: 53
+  selector:
+    app: udp
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coredns
+  namespace: gateway-conformance-udp
+  labels:
+    app: udp
+spec:
+  selector:
+    matchLabels:
+      app: udp
+  template:
+    metadata:
+      labels:
+        app: udp
+    spec:
+      containers:
+      - args:
+        - -conf
+        - /root/Corefile
+        image: coredns/coredns
+        name: coredns
+        volumeMounts:
+        - mountPath: /root
+          name: conf
+      volumes:
+      - configMap:
+          defaultMode: 420
+          name: coredns
+        name: conf
+---
+apiVersion: v1
+kind: ConfigMap
+metadata:
+  name: coredns
+  namespace: gateway-conformance-udp
+data:
+  Corefile: |
+    .:53 {
+        forward . 8.8.8.8 9.9.9.9
+        log
+        errors
+    }
+
+    foo.bar.com:53 {
+      whoami
+    }
+---
+apiVersion: gateway.networking.k8s.io/v1beta1
+kind: Gateway
+metadata:
+  name: udp-gateway
+  namespace: gateway-conformance-udp
+spec:
+  gatewayClassName: "{GATEWAY_CLASS_NAME}"
+  listeners:
+  - name: coredns
+    protocol: UDP
+    port: 5300
+    allowedRoutes:
+      kinds:
+      - kind: UDPRoute
diff --git a/test/e2e/testdata/udproute.yaml b/test/e2e/testdata/udproute.yaml
new file mode 100644
index 000000000..e5ea8d124
--- /dev/null
+++ b/test/e2e/testdata/udproute.yaml
@@ -0,0 +1,13 @@
+apiVersion: gateway.networking.k8s.io/v1alpha2
+kind: UDPRoute
+metadata:
+  name: udp-coredns
+  namespace: gateway-conformance-udp
+spec:
+  parentRefs:
+  - name: udp-gateway
+    sectionName: coredns
+  rules:
+  - backendRefs:
+    - name: coredns
+      port: 53
diff --git a/test/e2e/tests/udproute.go b/test/e2e/tests/udproute.go
new file mode 100644
index 000000000..31b0dbf44
--- /dev/null
+++ b/test/e2e/tests/udproute.go
@@ -0,0 +1,235 @@
+// 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.
+
+// This file contains code derived from upstream gateway-api, it will be moved to upstream.
+
+//go:build e2e
+// +build e2e
+
+package tests
+
+import (
+	"context"
+	"fmt"
+	"reflect"
+	"testing"
+	"time"
+
+	"github.com/miekg/dns"
+	"github.com/stretchr/testify/require"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/wait"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	gatewayv1 "sigs.k8s.io/gateway-api/apis/v1"
+	"sigs.k8s.io/gateway-api/apis/v1alpha2"
+	"sigs.k8s.io/gateway-api/conformance/utils/config"
+	"sigs.k8s.io/gateway-api/conformance/utils/kubernetes"
+	"sigs.k8s.io/gateway-api/conformance/utils/suite"
+)
+
+func init() {
+	ConformanceTests = append(ConformanceTests, UDPRouteTest)
+}
+
+var UDPRouteTest = suite.ConformanceTest{
+	ShortName:   "UDPRoute",
+	Description: "Make sure UDPRoute is working",
+	Manifests:   []string{"testdata/udproute.yaml"},
+	Test: func(t *testing.T, suite *suite.ConformanceTestSuite) {
+		t.Run("Simple UDP request matching UDPRoute should reach coredns backend", func(t *testing.T) {
+			namespace := "gateway-conformance-udp"
+			domain := "foo.bar.com."
+			routeNN := types.NamespacedName{Name: "udp-coredns", Namespace: namespace}
+			gwNN := types.NamespacedName{Name: "udp-gateway", Namespace: namespace}
+			gwAddr := GatewayAndUDPRoutesMustBeAccepted(t, suite.Client, suite.TimeoutConfig, suite.ControllerName, NewGatewayRef(gwNN), routeNN)
+
+			msg := new(dns.Msg)
+			msg.SetQuestion(domain, dns.TypeA)
+
+			if err := wait.PollUntilContextTimeout(context.TODO(), time.Second, time.Minute, true,
+				func(_ context.Context) (done bool, err error) {
+					t.Logf("performing DNS query %s on %s", domain, gwAddr)
+					_, err = dns.Exchange(msg, gwAddr)
+					if err != nil {
+						return false, err
+					}
+					return true, nil
+				}); err != nil {
+				t.Errorf("failed to perform DNS query: %v", err)
+			}
+		})
+	},
+}
+
+// GatewayRef is a tiny type for specifying an UDP Route ParentRef without
+// relying on a specific api version.
+type GatewayRef struct {
+	types.NamespacedName
+	listenerNames []*gatewayv1.SectionName
+}
+
+// NewGatewayRef creates a GatewayRef resource.  ListenerNames are optional.
+func NewGatewayRef(nn types.NamespacedName, listenerNames ...string) GatewayRef {
+	var listeners []*gatewayv1.SectionName
+
+	if len(listenerNames) == 0 {
+		listenerNames = append(listenerNames, "")
+	}
+
+	for _, listener := range listenerNames {
+		sectionName := gatewayv1.SectionName(listener)
+		listeners = append(listeners, &sectionName)
+	}
+	return GatewayRef{
+		NamespacedName: nn,
+		listenerNames:  listeners,
+	}
+}
+
+// GatewayAndUDPRoutesMustBeAccepted waits until the specified Gateway has an IP
+// address assigned to it and the UDPRoute has a ParentRef referring to the
+// Gateway. The test will fail if these conditions are not met before the
+// timeouts.
+func GatewayAndUDPRoutesMustBeAccepted(t *testing.T, c client.Client, timeoutConfig config.TimeoutConfig, controllerName string, gw GatewayRef, routeNNs ...types.NamespacedName) string {
+	t.Helper()
+
+	gwAddr, err := kubernetes.WaitForGatewayAddress(t, c, timeoutConfig, gw.NamespacedName)
+	require.NoErrorf(t, err, "timed out waiting for Gateway address to be assigned")
+
+	ns := gatewayv1.Namespace(gw.Namespace)
+	kind := gatewayv1.Kind("Gateway")
+
+	for _, routeNN := range routeNNs {
+		namespaceRequired := true
+		if routeNN.Namespace == gw.Namespace {
+			namespaceRequired = false
+		}
+
+		var parents []gatewayv1.RouteParentStatus
+		for _, listener := range gw.listenerNames {
+			parents = append(parents, gatewayv1.RouteParentStatus{
+				ParentRef: gatewayv1.ParentReference{
+					Group:       (*gatewayv1.Group)(&gatewayv1.GroupVersion.Group),
+					Kind:        &kind,
+					Name:        gatewayv1.ObjectName(gw.Name),
+					Namespace:   &ns,
+					SectionName: listener,
+				},
+				ControllerName: gatewayv1.GatewayController(controllerName),
+				Conditions: []metav1.Condition{
+					{
+						Type:   string(gatewayv1.RouteConditionAccepted),
+						Status: metav1.ConditionTrue,
+						Reason: string(gatewayv1.RouteReasonAccepted),
+					},
+				},
+			})
+		}
+		UDPRouteMustHaveParents(t, c, timeoutConfig, routeNN, parents, namespaceRequired)
+	}
+
+	return gwAddr
+}
+
+func UDPRouteMustHaveParents(t *testing.T, client client.Client, timeoutConfig config.TimeoutConfig, routeName types.NamespacedName, parents []gatewayv1.RouteParentStatus, namespaceRequired bool) {
+	t.Helper()
+
+	var actual []gatewayv1.RouteParentStatus
+	waitErr := wait.PollUntilContextTimeout(context.Background(), 1*time.Second, timeoutConfig.RouteMustHaveParents, true, func(ctx context.Context) (bool, error) {
+		route := &v1alpha2.UDPRoute{}
+		err := client.Get(ctx, routeName, route)
+		if err != nil {
+			return false, fmt.Errorf("error fetching UDPRoute: %w", err)
+		}
+
+		actual = route.Status.Parents
+		return parentsForRouteMatch(t, routeName, parents, actual, namespaceRequired), nil
+	})
+	require.NoErrorf(t, waitErr, "error waiting for UDPRoute to have parents matching expectations")
+}
+
+func parentsForRouteMatch(t *testing.T, routeName types.NamespacedName, expected, actual []gatewayv1.RouteParentStatus, namespaceRequired bool) bool {
+	t.Helper()
+
+	if len(expected) != len(actual) {
+		t.Logf("Route %s/%s expected %d Parents got %d", routeName.Namespace, routeName.Name, len(expected), len(actual))
+		return false
+	}
+
+	for i, expectedParent := range expected {
+		actualParent := actual[i]
+		if actualParent.ControllerName != expectedParent.ControllerName {
+			t.Logf("Route %s/%s ControllerName doesn't match", routeName.Namespace, routeName.Name)
+			return false
+		}
+		if !reflect.DeepEqual(actualParent.ParentRef.Group, expectedParent.ParentRef.Group) {
+			t.Logf("Route %s/%s expected ParentReference.Group to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Group, actualParent.ParentRef.Group)
+			return false
+		}
+		if !reflect.DeepEqual(actualParent.ParentRef.Kind, expectedParent.ParentRef.Kind) {
+			t.Logf("Route %s/%s expected ParentReference.Kind to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Kind, actualParent.ParentRef.Kind)
+			return false
+		}
+		if actualParent.ParentRef.Name != expectedParent.ParentRef.Name {
+			t.Logf("Route %s/%s ParentReference.Name doesn't match", routeName.Namespace, routeName.Name)
+			return false
+		}
+		if !reflect.DeepEqual(actualParent.ParentRef.Namespace, expectedParent.ParentRef.Namespace) {
+			if namespaceRequired || actualParent.ParentRef.Namespace != nil {
+				t.Logf("Route %s/%s expected ParentReference.Namespace to be %v, got %v", routeName.Namespace, routeName.Name, expectedParent.ParentRef.Namespace, actualParent.ParentRef.Namespace)
+				return false
+			}
+		}
+		if !conditionsMatch(t, expectedParent.Conditions, actualParent.Conditions) {
+			return false
+		}
+	}
+
+	t.Logf("Route %s/%s Parents matched expectations", routeName.Namespace, routeName.Name)
+	return true
+}
+
+func conditionsMatch(t *testing.T, expected, actual []metav1.Condition) bool {
+	t.Helper()
+
+	if len(actual) < len(expected) {
+		t.Logf("Expected more conditions to be present")
+		return false
+	}
+	for _, condition := range expected {
+		if !findConditionInList(t, actual, condition.Type, string(condition.Status), condition.Reason) {
+			return false
+		}
+	}
+
+	t.Logf("Conditions matched expectations")
+	return true
+}
+
+// findConditionInList finds a condition in a list of Conditions, checking
+// the Name, Value, and Reason. If an empty reason is passed, any Reason will match.
+// If an empty status is passed, any Status will match.
+func findConditionInList(t *testing.T, conditions []metav1.Condition, condName, expectedStatus, expectedReason string) bool {
+	t.Helper()
+
+	for _, cond := range conditions {
+		if cond.Type == condName {
+			// an empty Status string means "Match any status".
+			if expectedStatus == "" || cond.Status == metav1.ConditionStatus(expectedStatus) {
+				// an empty Reason string means "Match any reason".
+				if expectedReason == "" || cond.Reason == expectedReason {
+					return true
+				}
+				t.Logf("%s condition Reason set to %s, expected %s", condName, cond.Reason, expectedReason)
+			}
+
+			t.Logf("%s condition set to Status %s with Reason %v, expected Status %s", condName, cond.Status, cond.Reason, expectedStatus)
+		}
+	}
+
+	t.Logf("%s was not in conditions list [%v]", condName, conditions)
+	return false
+}