Repository: nginx/kubernetes-ingress

Issue:
Set `sync` setting for `limit_req_zone` when `zone_sync` is enabled
### Overview
The goal of this story is to ensure that the `sync` directive is appended to the end of any `limit_req_zone` in the NGINX configuration so that rate limit policies deployed can take advantage of `zone_sync`

### Acceptance Criteria
- [ ]  When `zone_sync` is enabled, the `sync` directive must be appended to the end of any `limit_req_zone`
- [ ] When `zone_sync` is disabled, the `sync` directive must be removed from the end of any `limit_req_zone`


Hints from issue comments:
@github-actions: Hi @shaun-nx thanks for reporting! 

 Be sure to check out the [docs](https://docs.nginx.com/nginx-ingress-controller) and the [Contributing Guidelines](https://github.com/nginx/kubernetes-ingress/blob/main/CONTRIBUTING.md) while you wait for a human to take a look at this :slightly_smiling_face:

 Cheers!

Solution patch:
diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go
index 0b185a781..456c77be3 100644
--- a/internal/configs/ingress.go
+++ b/internal/configs/ingress.go
@@ -46,6 +46,7 @@ type IngressEx struct {
 	AppProtectLogs   []AppProtectLog
 	DosEx            *DosEx
 	SecretRefs       map[string]*secrets.SecretReference
+	ZoneSync         bool
 }
 
 // DosEx holds a DosProtectedResource and the dos policy and log confs it references.
@@ -290,6 +291,7 @@ func generateNginxCfg(p NginxCfgParams) (version1.IngressNginxConfig, Warnings)
 						Key:  cfgParams.LimitReqKey,
 						Size: cfgParams.LimitReqZoneSize,
 						Rate: rate,
+						Sync: p.ingEx.ZoneSync,
 					})
 				}
 			}
diff --git a/internal/configs/version1/__snapshots__/template_test.snap b/internal/configs/version1/__snapshots__/template_test.snap
index 8d72ead22..202e4dfb0 100644
--- a/internal/configs/version1/__snapshots__/template_test.snap
+++ b/internal/configs/version1/__snapshots__/template_test.snap
@@ -1209,6 +1209,32 @@ server {
         
     }
     
+}
+
+---
+
+[TestExecuteTemplate_ForIngressForNGINXPlusWithRequestRateLimitZoneSync - 1]
+# configuration for default/myingress
+
+
+limit_req_zone ${binary_remote_addr} zone=default/zone1:10m rate=200r/s sync;
+
+
+
+server {
+
+    server_tokens "";
+
+    server_name test.example.com;
+
+    status_zone ;
+    set $resource_type "ingress";
+    set $resource_name "myingress";
+    set $resource_namespace "default";
+
+    
+
+    
 }
 
 ---
diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go
index 8b30146fa..5ff1ee1d3 100644
--- a/internal/configs/version1/config.go
+++ b/internal/configs/version1/config.go
@@ -73,6 +73,7 @@ type LimitReqZone struct {
 	Key  string
 	Size string
 	Rate string
+	Sync bool
 }
 
 // Server describes an NGINX server.
diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl
index d887810d6..c31517cc0 100644
--- a/internal/configs/version1/nginx-plus.ingress.tmpl
+++ b/internal/configs/version1/nginx-plus.ingress.tmpl
@@ -22,7 +22,7 @@ upstream {{$upstream.Name}} {
 {{- end}}
 
 {{range $limitReqZone := .LimitReqZones}}
-limit_req_zone {{ $limitReqZone.Key }} zone={{ $limitReqZone.Name }}:{{$limitReqZone.Size}} rate={{$limitReqZone.Rate}};
+limit_req_zone {{ $limitReqZone.Key }} zone={{ $limitReqZone.Name }}:{{$limitReqZone.Size}} rate={{$limitReqZone.Rate}}{{- if $limitReqZone.Sync }} sync{{- end }};
 {{end}}
 
 {{range $server := .Servers}}
diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap
index 2dfe09753..78bcf86e5 100644
--- a/internal/configs/version2/__snapshots__/templates_test.snap
+++ b/internal/configs/version2/__snapshots__/templates_test.snap
@@ -2913,7 +2913,7 @@ map $http_x_version $match_0_0 {
     default 0;
 }
 # HTTP snippet
-limit_req_zone $url zone=pol_rl_test_test_test:10m rate=10r/s;
+limit_req_zone $url zone=pol_rl_test_test_test:10m rate=10r/s sync;
 
 server {
     listen 80 proxy_protocol;
diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go
index 8a2a84fde..818546b55 100644
--- a/internal/configs/version2/http.go
+++ b/internal/configs/version2/http.go
@@ -383,10 +383,11 @@ type LimitReqZone struct {
 	PolicyResult  string
 	GroupDefault  bool
 	GroupSource   string
+	Sync          bool
 }
 
 func (rlz LimitReqZone) String() string {
-	return fmt.Sprintf("{Key %q, ZoneName %q, ZoneSize %v, Rate %q, GroupValue %q, PolicyValue %q, GroupVariable %q, PolicyResult %q, GroupDefault %t, GroupSource %q}",
+	return fmt.Sprintf("{Key %q, ZoneName %q, ZoneSize %v, Rate %q, GroupValue %q, PolicyValue %q, GroupVariable %q, PolicyResult %q, GroupDefault %t, GroupSource %q, Sync %t}",
 		rlz.Key,
 		rlz.ZoneName,
 		rlz.ZoneSize,
@@ -397,6 +398,7 @@ func (rlz LimitReqZone) String() string {
 		rlz.PolicyResult,
 		rlz.GroupDefault,
 		rlz.GroupSource,
+		rlz.Sync,
 	)
 }
 
diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl
index 49bd62712..ee4085171 100644
--- a/internal/configs/version2/nginx-plus.virtualserver.tmpl
+++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl
@@ -67,7 +67,7 @@ map {{ $m.Source }} {{ $m.Variable }} {
 {{- end }}
 
 {{- range $z := .LimitReqZones }}
-limit_req_zone {{ $z.Key }} zone={{ $z.ZoneName }}:{{ $z.ZoneSize }} rate={{ $z.Rate }};
+limit_req_zone {{ $z.Key }} zone={{ $z.ZoneName }}:{{ $z.ZoneSize }} rate={{ $z.Rate }}{{- if $z.Sync }} sync{{- end }};
 {{- end }}
 
 {{- range $m := .StatusMatches }}
diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go
index 5adfe3546..afdcc4a00 100644
--- a/internal/configs/virtualserver.go
+++ b/internal/configs/virtualserver.go
@@ -102,6 +102,7 @@ type VirtualServerEx struct {
 	LogConfRefs         map[string]*unstructured.Unstructured
 	DosProtectedRefs    map[string]*unstructured.Unstructured
 	DosProtectedEx      map[string]*DosEx
+	ZoneSync            bool
 }
 
 func (vsx *VirtualServerEx) String() string {
@@ -412,6 +413,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(
 
 	policyOpts := policyOptions{
 		tls:         sslConfig != nil,
+		zoneSync:    vsEx.ZoneSync,
 		secretRefs:  vsEx.SecretRefs,
 		apResources: apResources,
 	}
@@ -1009,6 +1011,7 @@ type policyOwnerDetails struct {
 
 type policyOptions struct {
 	tls         bool
+	zoneSync    bool
 	secretRefs  map[string]*secrets.SecretReference
 	apResources *appProtectResourcesForVS
 }
@@ -1045,17 +1048,18 @@ func (p *policiesCfg) addRateLimitConfig(
 	polName string,
 	ownerDetails policyOwnerDetails,
 	podReplicas int,
+	zoneSync bool,
 ) *validationResults {
 	res := newValidationResults()
 
 	rlZoneName := rfc1123ToSnake(fmt.Sprintf("pol_rl_%v_%v_%v_%v", polNamespace, polName, ownerDetails.vsNamespace, ownerDetails.vsName))
 	if rateLimit.Condition != nil && rateLimit.Condition.JWT.Claim != "" && rateLimit.Condition.JWT.Match != "" {
-		lrz := generateGroupedLimitReqZone(rlZoneName, rateLimit, podReplicas, ownerDetails)
+		lrz := generateGroupedLimitReqZone(rlZoneName, rateLimit, podReplicas, ownerDetails, zoneSync)
 		p.RateLimit.PolicyGroupMaps = append(p.RateLimit.PolicyGroupMaps, *generateLRZPolicyGroupMap(lrz))
 		p.RateLimit.AuthJWTClaimSets = append(p.RateLimit.AuthJWTClaimSets, generateAuthJwtClaimSet(*rateLimit.Condition.JWT, ownerDetails))
 		p.RateLimit.Zones = append(p.RateLimit.Zones, lrz)
 	} else {
-		p.RateLimit.Zones = append(p.RateLimit.Zones, generateLimitReqZone(rlZoneName, rateLimit, podReplicas))
+		p.RateLimit.Zones = append(p.RateLimit.Zones, generateLimitReqZone(rlZoneName, rateLimit, podReplicas, zoneSync))
 	}
 
 	p.RateLimit.Reqs = append(p.RateLimit.Reqs, generateLimitReq(rlZoneName, rateLimit))
@@ -1667,6 +1671,7 @@ func (vsc *virtualServerConfigurator) generatePolicies(
 					p.Name,
 					ownerDetails,
 					vsc.IngressControllerReplicas,
+					policyOpts.zoneSync,
 				)
 			case pol.Spec.JWTAuth != nil:
 				res = config.addJWTAuthConfig(pol.Spec.JWTAuth, key, polNamespace, policyOpts.secretRefs)
@@ -1742,7 +1747,7 @@ func generateLimitReq(zoneName string, rateLimitPol *conf_v1.RateLimit) version2
 	return limitReq
 }
 
-func generateLimitReqZone(zoneName string, rateLimitPol *conf_v1.RateLimit, podReplicas int) version2.LimitReqZone {
+func generateLimitReqZone(zoneName string, rateLimitPol *conf_v1.RateLimit, podReplicas int, zoneSync bool) version2.LimitReqZone {
 	rate := rateLimitPol.Rate
 	if rateLimitPol.Scale {
 		rate = scaleRatelimit(rateLimitPol.Rate, podReplicas)
@@ -1752,6 +1757,7 @@ func generateLimitReqZone(zoneName string, rateLimitPol *conf_v1.RateLimit, podR
 		Key:      rateLimitPol.Key,
 		ZoneSize: rateLimitPol.ZoneSize,
 		Rate:     rate,
+		Sync:     zoneSync,
 	}
 }
 
@@ -1759,6 +1765,7 @@ func generateGroupedLimitReqZone(zoneName string,
 	rateLimitPol *conf_v1.RateLimit,
 	podReplicas int,
 	ownerDetails policyOwnerDetails,
+	zoneSync bool,
 ) version2.LimitReqZone {
 	rate := rateLimitPol.Rate
 	if rateLimitPol.Scale {
@@ -1769,6 +1776,7 @@ func generateGroupedLimitReqZone(zoneName string,
 		Key:      rateLimitPol.Key,
 		ZoneSize: rateLimitPol.ZoneSize,
 		Rate:     rate,
+		Sync:     zoneSync,
 	}
 	if rateLimitPol.Condition != nil && rateLimitPol.Condition.JWT != nil {
 		lrz.GroupValue = rateLimitPol.Condition.JWT.Match
diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go
index f00d4f060..beca13b33 100644
--- a/internal/k8s/controller.go
+++ b/internal/k8s/controller.go
@@ -2216,6 +2216,10 @@ func (lbc *LoadBalancerController) createIngressEx(ing *networking.Ingress, vali
 				}
 			}
 		}
+
+		if lbc.configurator != nil && lbc.configurator.CfgParams != nil {
+			ingEx.ZoneSync = lbc.configurator.CfgParams.ZoneSync.Enable
+		}
 	}
 
 	ingEx.Endpoints = make(map[string][]string)
@@ -2356,6 +2360,9 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.
 		LogConfRefs:    make(map[string]*unstructured.Unstructured),
 		DosProtectedEx: make(map[string]*configs.DosEx),
 	}
+	if lbc.configurator != nil && lbc.configurator.CfgParams != nil {
+		virtualServerEx.ZoneSync = lbc.configurator.CfgParams.ZoneSync.Enable
+	}
 
 	resource := lbc.configuration.hosts[virtualServer.Spec.Host]
 	if vsc, ok := resource.(*VirtualServerConfiguration); ok {
diff --git a/pyproject.toml b/pyproject.toml
index d61b8baee..be6af3c37 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -22,7 +22,7 @@ pythonpath = [
 ]
 addopts = "--tb=native -ra --disable-warnings -x -l -v --strict-markers"
 log_cli = true
-markers =[
+markers = [
     "agent",
     "annotations",
     "appprotect",
@@ -92,7 +92,6 @@ markers =[
     "vsr_upstream",
     "watch_namespace",
     "wildcard_tls",
-    "zonesync",
 ]
 testpaths = [
     "tests",



Test patch:
diff --git a/internal/configs/ingress_test.go b/internal/configs/ingress_test.go
index 9d8e1750a..d5576cc65 100644
--- a/internal/configs/ingress_test.go
+++ b/internal/configs/ingress_test.go
@@ -1159,6 +1159,47 @@ func TestGenerateNginxCfgForLimitReqDefaults(t *testing.T) {
 	}
 }
 
+func TestGenerateNginxCfgForLimitReqZoneSync(t *testing.T) {
+	t.Parallel()
+	cafeIngressEx := createCafeIngressEx()
+	cafeIngressEx.Ingress.Annotations["nginx.org/limit-req-rate"] = "200r/s"
+	cafeIngressEx.Ingress.Annotations["nginx.org/limit-req-key"] = "${request_uri}"
+	cafeIngressEx.Ingress.Annotations["nginx.org/limit-req-zone-size"] = "11m"
+
+	cafeIngressEx.ZoneSync = true
+	isPlus := true
+
+	configParams := NewDefaultConfigParams(context.Background(), isPlus)
+
+	expectedZones := []version1.LimitReqZone{
+		{
+			Name: "default/cafe-ingress",
+			Key:  "${request_uri}",
+			Size: "11m",
+			Rate: "200r/s",
+			Sync: true,
+		},
+	}
+
+	result, warnings := generateNginxCfg(NginxCfgParams{
+		ingEx:         &cafeIngressEx,
+		BaseCfgParams: configParams,
+		staticParams:  &StaticConfigParams{},
+		isPlus:        isPlus,
+	})
+
+	if !reflect.DeepEqual(result.LimitReqZones, expectedZones) {
+		t.Errorf("generateNginxCfg returned \n%v,  but expected \n%v", result.LimitReqZones, expectedZones)
+	}
+
+	if !reflect.DeepEqual(result.LimitReqZones, expectedZones) {
+		t.Errorf("generateNginxCfg returned \n%v,  but expected \n%v", result.LimitReqZones, expectedZones)
+	}
+	if len(warnings) != 0 {
+		t.Errorf("generateNginxCfg returned warnings: %v", warnings)
+	}
+}
+
 func TestGenerateNginxCfgForMergeableIngressesForLimitReq(t *testing.T) {
 	t.Parallel()
 	mergeableIngresses := createMergeableCafeIngress()
diff --git a/internal/configs/version1/template_test.go b/internal/configs/version1/template_test.go
index ee5cdae1d..31df58882 100644
--- a/internal/configs/version1/template_test.go
+++ b/internal/configs/version1/template_test.go
@@ -1815,6 +1815,52 @@ func TestExecuteTemplate_ForIngressForNGINXPlusWithRequestRateLimitMinions(t *te
 	snaps.MatchSnapshot(t, buf.String())
 }
 
+func TestExecuteTemplate_ForIngressForNGINXPlusWithRequestRateLimitZoneSync(t *testing.T) {
+	t.Parallel()
+
+	tmpl := newNGINXPlusIngressTmpl(t)
+	buf := &bytes.Buffer{}
+
+	ingressCfg := IngressNginxConfig{
+		Ingress: Ingress{
+			Name:      "myingress",
+			Namespace: "default",
+		},
+		Servers: []Server{
+			{
+				Name: "test.example.com",
+			},
+		},
+		LimitReqZones: []LimitReqZone{
+			{
+				Name: "default/zone1",
+				Key:  "${binary_remote_addr}",
+				Size: "10m",
+				Rate: "200r/s",
+				Sync: true,
+			},
+		},
+	}
+
+	err := tmpl.Execute(buf, ingressCfg)
+	t.Log(buf.String())
+	if err != nil {
+		t.Fatal(err)
+	}
+	ingConf := buf.String()
+
+	wantDirectives := []string{
+		"limit_req_zone ${binary_remote_addr} zone=default/zone1:10m rate=200r/s sync;",
+	}
+
+	for _, want := range wantDirectives {
+		if !strings.Contains(ingConf, want) {
+			t.Errorf("want %q in generated config", want)
+		}
+	}
+	snaps.MatchSnapshot(t, buf.String())
+}
+
 func newNGINXPlusIngressTmpl(t *testing.T) *template.Template {
 	t.Helper()
 	tmpl, err := template.New("nginx-plus.ingress.tmpl").Funcs(helperFunctions).ParseFiles("nginx-plus.ingress.tmpl")
diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go
index 5aa546fc7..8a8afecf6 100644
--- a/internal/configs/version2/templates_test.go
+++ b/internal/configs/version2/templates_test.go
@@ -1147,7 +1147,7 @@ var (
 	virtualServerCfg = VirtualServerConfig{
 		LimitReqZones: []LimitReqZone{
 			{
-				ZoneName: "pol_rl_test_test_test", Rate: "10r/s", ZoneSize: "10m", Key: "$url",
+				ZoneName: "pol_rl_test_test_test", Rate: "10r/s", ZoneSize: "10m", Key: "$url", Sync: true,
 			},
 		},
 		Upstreams: []Upstream{
diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go
index d8f9caf6d..1bd79225f 100644
--- a/internal/configs/virtualserver_test.go
+++ b/internal/configs/virtualserver_test.go
@@ -6395,6 +6395,343 @@ func TestGenerateVirtualServerConfigAPIKeyClientMaps(t *testing.T) {
 	}
 }
 
+func TestGenerateVirtualServerConfigRateLimit(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		msg             string
+		virtualServerEx VirtualServerEx
+		expected        version2.VirtualServerConfig
+	}{
+		{
+			msg: "rate limits at vs spec level with zone sync enabled",
+			virtualServerEx: VirtualServerEx{
+				VirtualServer: &conf_v1.VirtualServer{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "cafe",
+						Namespace: "default",
+					},
+					Spec: conf_v1.VirtualServerSpec{
+						Host: "cafe.example.com",
+						Policies: []conf_v1.PolicyReference{
+							{
+								Name: "rate-limit-policy",
+							},
+						},
+						Upstreams: []conf_v1.Upstream{
+							{
+								Name:    "tea",
+								Service: "tea-svc",
+								Port:    80,
+							},
+							{
+								Name:    "coffee",
+								Service: "coffee-svc",
+								Port:    80,
+							},
+						},
+						Routes: []conf_v1.Route{
+							{
+								Path: "/tea",
+								Action: &conf_v1.Action{
+									Pass: "tea",
+								},
+							},
+							{
+								Path: "/coffee",
+								Action: &conf_v1.Action{
+									Pass: "coffee",
+								},
+							},
+						},
+					},
+				},
+				Policies: map[string]*conf_v1.Policy{
+					"default/rate-limit-policy": {
+						Spec: conf_v1.PolicySpec{
+							RateLimit: &conf_v1.RateLimit{
+								Key:      "$binary_remote_addr",
+								ZoneSize: "10M",
+								Rate:     "10r/s",
+							},
+						},
+					},
+				},
+				Endpoints: map[string][]string{
+					"default/tea-svc:80": {
+						"10.0.0.20:80",
+					},
+					"default/coffee-svc:80": {
+						"10.0.0.30:80",
+					},
+				},
+				ZoneSync: true,
+			},
+			expected: version2.VirtualServerConfig{
+				Upstreams: []version2.Upstream{
+					{
+						UpstreamLabels: version2.UpstreamLabels{
+							Service:           "coffee-svc",
+							ResourceType:      "virtualserver",
+							ResourceName:      "cafe",
+							ResourceNamespace: "default",
+						},
+						Name: "vs_default_cafe_coffee",
+						Servers: []version2.UpstreamServer{
+							{
+								Address: "10.0.0.30:80",
+							},
+						},
+					},
+					{
+						UpstreamLabels: version2.UpstreamLabels{
+							Service:           "tea-svc",
+							ResourceType:      "virtualserver",
+							ResourceName:      "cafe",
+							ResourceNamespace: "default",
+						},
+						Name: "vs_default_cafe_tea",
+						Servers: []version2.UpstreamServer{
+							{
+								Address: "10.0.0.20:80",
+							},
+						},
+					},
+				},
+				HTTPSnippets: []string{},
+				LimitReqZones: []version2.LimitReqZone{
+					{
+						Key:      "$binary_remote_addr",
+						ZoneName: "pol_rl_default_rate_limit_policy_default_cafe",
+						ZoneSize: "10M",
+						Rate:     "10r/s",
+						Sync:     true,
+					},
+				},
+				Server: version2.Server{
+					ServerName:   "cafe.example.com",
+					StatusZone:   "cafe.example.com",
+					ServerTokens: "off",
+					VSNamespace:  "default",
+					VSName:       "cafe",
+					LimitReqs: []version2.LimitReq{
+						{ZoneName: "pol_rl_default_rate_limit_policy_default_cafe", Burst: 0, NoDelay: false, Delay: 0},
+					},
+					LimitReqOptions: version2.LimitReqOptions{
+						DryRun:     false,
+						LogLevel:   "error",
+						RejectCode: 503,
+					},
+					Locations: []version2.Location{
+						{
+							Path:                     "/tea",
+							ProxyPass:                "http://vs_default_cafe_tea",
+							ProxyNextUpstream:        "error timeout",
+							ProxyNextUpstreamTimeout: "0s",
+							ProxyNextUpstreamTries:   0,
+							ProxySSLName:             "tea-svc.default.svc",
+							ProxyPassRequestHeaders:  true,
+							ProxySetHeaders:          []version2.Header{{Name: "Host", Value: "$host"}},
+							ServiceName:              "tea-svc",
+						},
+						{
+							Path:                     "/coffee",
+							ProxyPass:                "http://vs_default_cafe_coffee",
+							ProxyNextUpstream:        "error timeout",
+							ProxyNextUpstreamTimeout: "0s",
+							ProxyNextUpstreamTries:   0,
+							ProxySSLName:             "coffee-svc.default.svc",
+							ProxyPassRequestHeaders:  true,
+							ProxySetHeaders:          []version2.Header{{Name: "Host", Value: "$host"}},
+							ServiceName:              "coffee-svc",
+						},
+					},
+				},
+			},
+		},
+		{
+			msg: "rate limits at vs spec level without zone sync",
+			virtualServerEx: VirtualServerEx{
+				VirtualServer: &conf_v1.VirtualServer{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "cafe",
+						Namespace: "default",
+					},
+					Spec: conf_v1.VirtualServerSpec{
+						Host: "cafe.example.com",
+						Policies: []conf_v1.PolicyReference{
+							{
+								Name: "rate-limit-policy",
+							},
+						},
+						Upstreams: []conf_v1.Upstream{
+							{
+								Name:    "tea",
+								Service: "tea-svc",
+								Port:    80,
+							},
+							{
+								Name:    "coffee",
+								Service: "coffee-svc",
+								Port:    80,
+							},
+						},
+						Routes: []conf_v1.Route{
+							{
+								Path: "/tea",
+								Action: &conf_v1.Action{
+									Pass: "tea",
+								},
+							},
+							{
+								Path: "/coffee",
+								Action: &conf_v1.Action{
+									Pass: "coffee",
+								},
+							},
+						},
+					},
+				},
+				Policies: map[string]*conf_v1.Policy{
+					"default/rate-limit-policy": {
+						Spec: conf_v1.PolicySpec{
+							RateLimit: &conf_v1.RateLimit{
+								Key:      "$binary_remote_addr",
+								ZoneSize: "10M",
+								Rate:     "10r/s",
+							},
+						},
+					},
+				},
+				Endpoints: map[string][]string{
+					"default/tea-svc:80": {
+						"10.0.0.20:80",
+					},
+					"default/coffee-svc:80": {
+						"10.0.0.30:80",
+					},
+				},
+			},
+			expected: version2.VirtualServerConfig{
+				Upstreams: []version2.Upstream{
+					{
+						UpstreamLabels: version2.UpstreamLabels{
+							Service:           "coffee-svc",
+							ResourceType:      "virtualserver",
+							ResourceName:      "cafe",
+							ResourceNamespace: "default",
+						},
+						Name: "vs_default_cafe_coffee",
+						Servers: []version2.UpstreamServer{
+							{
+								Address: "10.0.0.30:80",
+							},
+						},
+					},
+					{
+						UpstreamLabels: version2.UpstreamLabels{
+							Service:           "tea-svc",
+							ResourceType:      "virtualserver",
+							ResourceName:      "cafe",
+							ResourceNamespace: "default",
+						},
+						Name: "vs_default_cafe_tea",
+						Servers: []version2.UpstreamServer{
+							{
+								Address: "10.0.0.20:80",
+							},
+						},
+					},
+				},
+				HTTPSnippets: []string{},
+				LimitReqZones: []version2.LimitReqZone{
+					{
+						Key:      "$binary_remote_addr",
+						ZoneName: "pol_rl_default_rate_limit_policy_default_cafe",
+						ZoneSize: "10M",
+						Rate:     "10r/s",
+						Sync:     false,
+					},
+				},
+				Server: version2.Server{
+					ServerName:   "cafe.example.com",
+					StatusZone:   "cafe.example.com",
+					ServerTokens: "off",
+					VSNamespace:  "default",
+					VSName:       "cafe",
+					LimitReqs: []version2.LimitReq{
+						{ZoneName: "pol_rl_default_rate_limit_policy_default_cafe", Burst: 0, NoDelay: false, Delay: 0},
+					},
+					LimitReqOptions: version2.LimitReqOptions{
+						DryRun:     false,
+						LogLevel:   "error",
+						RejectCode: 503,
+					},
+					Locations: []version2.Location{
+						{
+							Path:                     "/tea",
+							ProxyPass:                "http://vs_default_cafe_tea",
+							ProxyNextUpstream:        "error timeout",
+							ProxyNextUpstreamTimeout: "0s",
+							ProxyNextUpstreamTries:   0,
+							ProxySSLName:             "tea-svc.default.svc",
+							ProxyPassRequestHeaders:  true,
+							ProxySetHeaders:          []version2.Header{{Name: "Host", Value: "$host"}},
+							ServiceName:              "tea-svc",
+						},
+						{
+							Path:                     "/coffee",
+							ProxyPass:                "http://vs_default_cafe_coffee",
+							ProxyNextUpstream:        "error timeout",
+							ProxyNextUpstreamTimeout: "0s",
+							ProxyNextUpstreamTries:   0,
+							ProxySSLName:             "coffee-svc.default.svc",
+							ProxyPassRequestHeaders:  true,
+							ProxySetHeaders:          []version2.Header{{Name: "Host", Value: "$host"}},
+							ServiceName:              "coffee-svc",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	baseCfgParams := ConfigParams{
+		Context:      context.Background(),
+		ServerTokens: "off",
+	}
+
+	vsc := newVirtualServerConfigurator(
+		&baseCfgParams,
+		false,
+		false,
+		&StaticConfigParams{},
+		false,
+		&fakeBV,
+	)
+
+	for _, test := range tests {
+		result, warnings := vsc.GenerateVirtualServerConfig(&test.virtualServerEx, nil, nil)
+
+		sort.Slice(result.Maps, func(i, j int) bool {
+			return result.Maps[i].Variable < result.Maps[j].Variable
+		})
+
+		sort.Slice(test.expected.Maps, func(i, j int) bool {
+			return test.expected.Maps[i].Variable < test.expected.Maps[j].Variable
+		})
+
+		if diff := cmp.Diff(test.expected, result); diff != "" {
+			t.Errorf("GenerateVirtualServerConfig() mismatch (-want +got):\n%s", diff)
+			t.Error(test.msg)
+		}
+
+		if len(warnings) != 0 {
+			t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings)
+		}
+	}
+}
+
 func TestGenerateVirtualServerConfigRateLimitGroups(t *testing.T) {
 	t.Parallel()
 
@@ -6403,6 +6740,239 @@ func TestGenerateVirtualServerConfigRateLimitGroups(t *testing.T) {
 		virtualServerEx VirtualServerEx
 		expected        version2.VirtualServerConfig
 	}{
+		{
+			msg: "rate limits at vs spec level, no default with zonesync enabled",
+			virtualServerEx: VirtualServerEx{
+				VirtualServer: &conf_v1.VirtualServer{
+					ObjectMeta: meta_v1.ObjectMeta{
+						Name:      "cafe",
+						Namespace: "default",
+					},
+					Spec: conf_v1.VirtualServerSpec{
+						Host: "cafe.example.com",
+						Policies: []conf_v1.PolicyReference{
+							{
+								Name: "premium-rate-limit-policy",
+							},
+							{
+								Name: "basic-rate-limit-policy",
+							},
+						},
+						Upstreams: []conf_v1.Upstream{
+							{
+								Name:    "tea",
+								Service: "tea-svc",
+								Port:    80,
+							},
+							{
+								Name:    "coffee",
+								Service: "coffee-svc",
+								Port:    80,
+							},
+						},
+						Routes: []conf_v1.Route{
+							{
+								Path: "/tea",
+								Action: &conf_v1.Action{
+									Pass: "tea",
+								},
+							},
+							{
+								Path: "/coffee",
+								Action: &conf_v1.Action{
+									Pass: "coffee",
+								},
+							},
+						},
+					},
+				},
+				Policies: map[string]*conf_v1.Policy{
+					"default/premium-rate-limit-policy": {
+						Spec: conf_v1.PolicySpec{
+							RateLimit: &conf_v1.RateLimit{
+								Key:      "$jwt_claim_sub",
+								ZoneSize: "10M",
+								Rate:     "10r/s",
+								Condition: &conf_v1.RateLimitCondition{
+									JWT: &conf_v1.JWTCondition{
+										Claim: "user_type.tier",
+										Match: "premium",
+									},
+								},
+							},
+						},
+					},
+					"default/basic-rate-limit-policy": {
+						Spec: conf_v1.PolicySpec{
+							RateLimit: &conf_v1.RateLimit{
+								Key:      "$jwt_claim_sub",
+								ZoneSize: "20M",
+								Rate:     "20r/s",
+								Condition: &conf_v1.RateLimitCondition{
+									JWT: &conf_v1.JWTCondition{
+										Claim: "user_type.tier",
+										Match: "basic",
+									},
+								},
+							},
+						},
+					},
+				},
+				Endpoints: map[string][]string{
+					"default/tea-svc:80": {
+						"10.0.0.20:80",
+					},
+					"default/coffee-svc:80": {
+						"10.0.0.30:80",
+					},
+				},
+				ZoneSync: true,
+			},
+			expected: version2.VirtualServerConfig{
+				Maps: []version2.Map{
+					{
+						Source:   "$rl_default_cafe_group_user_type_tier",
+						Variable: "$pol_rl_default_basic_rate_limit_policy_default_cafe",
+						Parameters: []version2.Parameter{
+							{
+								Value:  "default",
+								Result: "''",
+							},
+							{
+								Value:  "rl_default_cafe_match_basic",
+								Result: "Val$jwt_claim_sub",
+							},
+						},
+					},
+					{
+						Source:   "$rl_default_cafe_group_user_type_tier",
+						Variable: "$pol_rl_default_premium_rate_limit_policy_default_cafe",
+						Parameters: []version2.Parameter{
+							{
+								Value:  "default",
+								Result: "''",
+							},
+							{
+								Value:  "rl_default_cafe_match_premium",
+								Result: "Val$jwt_claim_sub",
+							},
+						},
+					},
+					{
+						Source:   "$jwt_default_cafe_user_type_tier",
+						Variable: "$rl_default_cafe_group_user_type_tier",
+						Parameters: []version2.Parameter{
+							{
+								Value:  "basic",
+								Result: "rl_default_cafe_match_basic",
+							},
+							{
+								Value:  "premium",
+								Result: "rl_default_cafe_match_premium",
+							},
+						},
+					},
+				},
+				AuthJWTClaimSets: []version2.AuthJWTClaimSet{{Variable: "$jwt_default_cafe_user_type_tier", Claim: "user_type tier"}},
+				Upstreams: []version2.Upstream{
+					{
+						UpstreamLabels: version2.UpstreamLabels{
+							Service:           "coffee-svc",
+							ResourceType:      "virtualserver",
+							ResourceName:      "cafe",
+							ResourceNamespace: "default",
+						},
+						Name: "vs_default_cafe_coffee",
+						Servers: []version2.UpstreamServer{
+							{
+								Address: "10.0.0.30:80",
+							},
+						},
+					},
+					{
+						UpstreamLabels: version2.UpstreamLabels{
+							Service:           "tea-svc",
+							ResourceType:      "virtualserver",
+							ResourceName:      "cafe",
+							ResourceNamespace: "default",
+						},
+						Name: "vs_default_cafe_tea",
+						Servers: []version2.UpstreamServer{
+							{
+								Address: "10.0.0.20:80",
+							},
+						},
+					},
+				},
+				HTTPSnippets: []string{},
+				LimitReqZones: []version2.LimitReqZone{
+					{
+						Key:           "$pol_rl_default_premium_rate_limit_policy_default_cafe",
+						ZoneName:      "pol_rl_default_premium_rate_limit_policy_default_cafe",
+						ZoneSize:      "10M",
+						Rate:          "10r/s",
+						PolicyResult:  "$jwt_claim_sub",
+						GroupVariable: "$rl_default_cafe_group_user_type_tier",
+						PolicyValue:   "rl_default_cafe_match_premium",
+						GroupValue:    "premium",
+						GroupSource:   "$jwt_default_cafe_user_type_tier",
+						Sync:          true,
+					},
+					{
+						Key:           "$pol_rl_default_basic_rate_limit_policy_default_cafe",
+						ZoneName:      "pol_rl_default_basic_rate_limit_policy_default_cafe",
+						ZoneSize:      "20M",
+						Rate:          "20r/s",
+						PolicyResult:  "$jwt_claim_sub",
+						GroupVariable: "$rl_default_cafe_group_user_type_tier",
+						PolicyValue:   "rl_default_cafe_match_basic",
+						GroupValue:    "basic",
+						GroupSource:   "$jwt_default_cafe_user_type_tier",
+						Sync:          true,
+					},
+				},
+				Server: version2.Server{
+					ServerName:   "cafe.example.com",
+					StatusZone:   "cafe.example.com",
+					ServerTokens: "off",
+					VSNamespace:  "default",
+					VSName:       "cafe",
+					LimitReqs: []version2.LimitReq{
+						{ZoneName: "pol_rl_default_premium_rate_limit_policy_default_cafe", Burst: 0, NoDelay: false, Delay: 0},
+						{ZoneName: "pol_rl_default_basic_rate_limit_policy_default_cafe", Burst: 0, NoDelay: false, Delay: 0},
+					},
+					LimitReqOptions: version2.LimitReqOptions{
+						DryRun:     false,
+						LogLevel:   "error",
+						RejectCode: 503,
+					},
+					Locations: []version2.Location{
+						{
+							Path:                     "/tea",
+							ProxyPass:                "http://vs_default_cafe_tea",
+							ProxyNextUpstream:        "error timeout",
+							ProxyNextUpstreamTimeout: "0s",
+							ProxyNextUpstreamTries:   0,
+							ProxySSLName:             "tea-svc.default.svc",
+							ProxyPassRequestHeaders:  true,
+							ProxySetHeaders:          []version2.Header{{Name: "Host", Value: "$host"}},
+							ServiceName:              "tea-svc",
+						},
+						{
+							Path:                     "/coffee",
+							ProxyPass:                "http://vs_default_cafe_coffee",
+							ProxyNextUpstream:        "error timeout",
+							ProxyNextUpstreamTimeout: "0s",
+							ProxyNextUpstreamTries:   0,
+							ProxySSLName:             "coffee-svc.default.svc",
+							ProxyPassRequestHeaders:  true,
+							ProxySetHeaders:          []version2.Header{{Name: "Host", Value: "$host"}},
+							ServiceName:              "coffee-svc",
+						},
+					},
+				},
+			},
+		},
 		{
 			msg: "rate limits at vs spec level, no default",
 			virtualServerEx: VirtualServerEx{
@@ -7948,7 +8518,8 @@ func TestGeneratePolicies(t *testing.T) {
 	mTLSCrlPath := "/etc/nginx/secrets/default-ingress-mtls-secret-ca.crl"
 	mTLSCertAndCrlPath := fmt.Sprintf("%s %s", mTLSCertPath, mTLSCrlPath)
 	policyOpts := policyOptions{
-		tls: true,
+		tls:      true,
+		zoneSync: true,
 		secretRefs: map[string]*secrets.SecretReference{
 			"default/ingress-mtls-secret": {
 				Secret: &api_v1.Secret{
@@ -8140,6 +8711,7 @@ func TestGeneratePolicies(t *testing.T) {
 							ZoneSize: "10M",
 							Rate:     "10r/s",
 							ZoneName: "pol_rl_default_rateLimit_policy_default_test",
+							Sync:     true,
 						},
 					},
 					Options: version2.LimitReqOptions{
@@ -8189,12 +8761,14 @@ func TestGeneratePolicies(t *testing.T) {
 							ZoneSize: "10M",
 							Rate:     "10r/s",
 							ZoneName: "pol_rl_default_rateLimit_policy_default_test",
+							Sync:     true,
 						},
 						{
 							Key:      "test2",
 							ZoneSize: "20M",
 							Rate:     "20r/s",
 							ZoneName: "pol_rl_default_rateLimit_policy2_default_test",
+							Sync:     true,
 						},
 					},
 					Options: version2.LimitReqOptions{
@@ -8241,6 +8815,7 @@ func TestGeneratePolicies(t *testing.T) {
 							ZoneSize: "10M",
 							Rate:     "5r/s",
 							ZoneName: "pol_rl_default_rateLimitScale_policy_default_test",
+							Sync:     true,
 						},
 					},
 					Options: version2.LimitReqOptions{
@@ -8256,7 +8831,6 @@ func TestGeneratePolicies(t *testing.T) {
 			},
 			msg: "rate limit reference with scale",
 		},
-
 		{
 			policyRefs: []conf_v1.PolicyReference{
 				{
diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go
index 31458504f..84efe85a7 100644
--- a/internal/k8s/controller_test.go
+++ b/internal/k8s/controller_test.go
@@ -3534,3 +3534,108 @@ func TestGenerateSecretNSName(t *testing.T) {
 		})
 	}
 }
+
+func TestCreateVirtualServerExWithZoneSync(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		testCase string
+		input    NewLoadBalancerControllerInput
+		vs       conf_v1.VirtualServer
+		vsr      []*conf_v1.VirtualServerRoute
+		expected configs.VirtualServerEx
+	}{
+		{
+			testCase: "VirtualServerEx without Zone sync",
+			input: NewLoadBalancerControllerInput{
+				KubeClient:               fake.NewSimpleClientset(),
+				EnableTelemetryReporting: false,
+				LoggerContext:            context.Background(),
+			},
+			vs:  conf_v1.VirtualServer{},
+			vsr: []*conf_v1.VirtualServerRoute{{}},
+			expected: configs.VirtualServerEx{
+				VirtualServer: &conf_v1.VirtualServer{},
+				VirtualServerRoutes: []*conf_v1.VirtualServerRoute{
+					{},
+				},
+			},
+		},
+		{
+			testCase: "VirtualServerEx with Zone sync",
+			input: NewLoadBalancerControllerInput{
+				KubeClient:               fake.NewSimpleClientset(),
+				EnableTelemetryReporting: false,
+				LoggerContext:            context.Background(),
+				NginxConfigurator: &configs.Configurator{
+					CfgParams: &configs.ConfigParams{
+						ZoneSync: configs.ZoneSync{
+							Enable: true,
+						},
+					},
+				},
+			},
+			vs:  conf_v1.VirtualServer{},
+			vsr: []*conf_v1.VirtualServerRoute{{}},
+			expected: configs.VirtualServerEx{
+				VirtualServer: &conf_v1.VirtualServer{},
+				VirtualServerRoutes: []*conf_v1.VirtualServerRoute{
+					{},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		lbc := NewLoadBalancerController(tc.input)
+		vsEx := lbc.createVirtualServerEx(&tc.vs, tc.vsr)
+		if reflect.DeepEqual(vsEx, tc.expected) {
+			t.Fatalf("Expected %v, but got %v", tc.expected, vsEx)
+		}
+	}
+}
+
+func TestCreateIngressExWithZoneSync(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		testCase string
+		input    NewLoadBalancerControllerInput
+		ingress  *networking.Ingress
+		expected configs.IngressEx
+	}{
+		{
+			testCase: "IngressEx without Zone sync",
+			input: NewLoadBalancerControllerInput{
+				KubeClient:               fake.NewSimpleClientset(),
+				EnableTelemetryReporting: false,
+				LoggerContext:            context.Background(),
+			},
+			ingress: &networking.Ingress{},
+			expected: configs.IngressEx{
+				Ingress: &networking.Ingress{},
+			},
+		},
+		{
+			testCase: "IngressEx with Zone sync",
+			input: NewLoadBalancerControllerInput{
+				KubeClient:               fake.NewSimpleClientset(),
+				EnableTelemetryReporting: false,
+				LoggerContext:            context.Background(),
+			},
+			ingress: &networking.Ingress{},
+			expected: configs.IngressEx{
+				Ingress:  &networking.Ingress{},
+				ZoneSync: true,
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		lbc := NewLoadBalancerController(tc.input)
+		ingressEx := lbc.createIngressEx(tc.ingress, nil, nil)
+		if reflect.DeepEqual(ingressEx, tc.expected) {
+			t.Fatalf("Expected %v, but got %v", tc.expected, ingressEx)
+		}
+	}
+}
diff --git a/tests/suite/test_rl_ingress.py b/tests/suite/test_rl_ingress.py
index fe9fddecf..04cf9ade0 100644
--- a/tests/suite/test_rl_ingress.py
+++ b/tests/suite/test_rl_ingress.py
@@ -4,6 +4,11 @@ import pytest
 import requests
 from settings import TEST_DATA
 from suite.fixtures.fixtures import PublicEndpoint
+from suite.utils.nginx_api_utils import (
+    check_synced_zone_exists,
+    wait_for_zone_sync_enabled,
+    wait_for_zone_sync_nodes_online,
+)
 from suite.utils.resources_utils import (
     are_all_pods_in_ready_state,
     create_example_app,
@@ -15,6 +20,7 @@ from suite.utils.resources_utils import (
     get_first_pod_name,
     get_ingress_nginx_template_conf,
     get_pod_list,
+    replace_configmap_from_yaml,
     scale_deployment,
     wait_before_test,
     wait_until_all_pods_are_ready,
@@ -116,6 +122,89 @@ class TestRateLimitIngress:
         assert (counter.count(200)) <= 2 and (429 in counter)  # check for only 2 200s in the list
 
 
+@pytest.mark.skip_for_nginx_oss
+@pytest.mark.annotations
+@pytest.mark.parametrize(
+    "ingress_controller",
+    [
+        pytest.param(
+            {"extra_args": ["-nginx-status-allow-cidrs=0.0.0.0/0,::/0"]},
+        )
+    ],
+    indirect=["ingress_controller"],
+)
+class TestRateLimitIngressZoneSync:
+    def test_ingress_rate_limit_with_zone_sync(
+        self,
+        kube_apis,
+        ingress_controller,
+        ingress_controller_endpoint,
+        ingress_controller_prerequisites,
+        test_namespace,
+    ):
+        """
+        Test pods are scaled to 3, ZoneSync is enabled & annotated ratelimit zone is synced
+        """
+        src = f"{TEST_DATA}/rate-limit/ingress/standard/annotations-rl-ingress.yaml"
+        NGINX_API_VERSION = 9
+        replica_count = 3
+        configmap_name = "nginx-config"
+
+        print("Step 1: apply minimal zone_sync nginx-config map")
+        replace_configmap_from_yaml(
+            kube_apis.v1,
+            configmap_name,
+            ingress_controller_prerequisites.namespace,
+            f"{TEST_DATA}/zone-sync/configmap-with-zonesync-minimal.yaml",
+        )
+
+        ingress_name = get_name_from_yaml(src)
+
+        create_items_from_yaml(kube_apis, src, test_namespace)
+
+        print(f"Step 3: scale deployments to {replica_count}")
+        scale_deployment(
+            kube_apis.v1,
+            kube_apis.apps_v1_api,
+            "nginx-ingress",
+            ingress_controller_prerequisites.namespace,
+            replica_count,
+        )
+
+        wait_before_test()
+
+        print("Step 4: check if pods are ready")
+        wait_until_all_pods_are_ready(kube_apis.v1, ingress_controller_prerequisites.namespace)
+
+        print("Step 5: check plus api for zone sync")
+        api_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.api_port}"
+
+        stream_url = f"{api_url}/api/{NGINX_API_VERSION}/stream"
+        assert wait_for_zone_sync_enabled(stream_url)
+
+        zone_sync_url = f"{stream_url}/zone_sync"
+        assert wait_for_zone_sync_nodes_online(zone_sync_url, replica_count)
+
+        print("Step 6: check plus api if zone is synced")
+        assert check_synced_zone_exists(zone_sync_url, ingress_name)
+
+        # revert changes
+        scale_deployment(
+            kube_apis.v1,
+            kube_apis.apps_v1_api,
+            "nginx-ingress",
+            ingress_controller_prerequisites.namespace,
+            1,
+        )
+        delete_items_from_yaml(kube_apis, src, test_namespace)
+        replace_configmap_from_yaml(
+            kube_apis.v1,
+            configmap_name,
+            ingress_controller_prerequisites.namespace,
+            f"{TEST_DATA}/zone-sync/default-configmap.yaml",
+        )
+
+
 @pytest.mark.annotations
 @pytest.mark.parametrize("annotations_setup", ["standard-scaled", "mergeable-scaled"], indirect=True)
 class TestRateLimitIngressScaled:
diff --git a/tests/suite/test_rl_policies.py b/tests/suite/test_rl_policies.py
index c02d8e2e6..7d39ad402 100644
--- a/tests/suite/test_rl_policies.py
+++ b/tests/suite/test_rl_policies.py
@@ -5,13 +5,20 @@ import pytest
 import requests
 from settings import TEST_DATA
 from suite.utils.custom_resources_utils import read_custom_resource
+from suite.utils.nginx_api_utils import (
+    check_synced_zone_exists,
+    wait_for_zone_sync_enabled,
+    wait_for_zone_sync_nodes_online,
+)
 from suite.utils.policy_resources_utils import apply_and_assert_valid_policy, create_policy_from_yaml, delete_policy
 from suite.utils.resources_utils import (
     get_pod_list,
     get_vs_nginx_template_conf,
+    replace_configmap_from_yaml,
     scale_deployment,
     wait_before_test,
     wait_for_event,
+    wait_until_all_pods_are_ready,
 )
 from suite.utils.vs_vsr_resources_utils import (
     apply_and_assert_valid_vs,
@@ -21,6 +28,8 @@ from suite.utils.vs_vsr_resources_utils import (
     patch_virtual_server_from_yaml,
 )
 
+NGINX_API_VERSION = 9
+
 std_vs_src = f"{TEST_DATA}/rate-limit/standard/virtual-server.yaml"
 rl_pol_pri_src = f"{TEST_DATA}/rate-limit/policies/rate-limit-primary.yaml"
 rl_pol_pri_sca_src = f"{TEST_DATA}/rate-limit/policies/rate-limit-primary-scaled.yaml"
@@ -66,6 +75,7 @@ rl_pol_premium_with_default_jwt_claim_sub = (
                 "extra_args": [
                     f"-enable-custom-resources",
                     f"-enable-leader-election=false",
+                    "-nginx-status-allow-cidrs=0.0.0.0/0,::/0",
                 ],
             },
             {
@@ -367,6 +377,86 @@ class TestRateLimitingPolicies:
         delete_policy(kube_apis.custom_objects, pol_name, test_namespace)
         self.restore_default_vs(kube_apis, virtual_server_setup)
 
+    @pytest.mark.skip_for_nginx_oss
+    @pytest.mark.parametrize("src", [rl_vs_sec_src])
+    def test_rl_policy_5rs_with_zone_sync(
+        self,
+        kube_apis,
+        crd_ingress_controller,
+        ingress_controller_prerequisites,
+        ingress_controller_endpoint,
+        virtual_server_setup,
+        test_namespace,
+        src,
+    ):
+        """
+        Test pods are scaled to 3, ZoneSync is enabled & Policy zone is synced
+        """
+        replica_count = 3
+        pol_name = apply_and_assert_valid_policy(kube_apis, test_namespace, rl_pol_sec_src)
+
+        configmap_name = "nginx-config"
+
+        print("Step 1: apply minimal zone_sync nginx-config map")
+        replace_configmap_from_yaml(
+            kube_apis.v1,
+            configmap_name,
+            ingress_controller_prerequisites.namespace,
+            f"{TEST_DATA}/zone-sync/configmap-with-zonesync-minimal.yaml",
+        )
+
+        print("Step 2: apply the policy to the virtual server")
+        # Patch VirtualServer
+        apply_and_assert_valid_vs(
+            kube_apis,
+            virtual_server_setup.namespace,
+            virtual_server_setup.vs_name,
+            src,
+        )
+
+        print(f"Step 3: scale deployments to {replica_count}")
+        scale_deployment(
+            kube_apis.v1,
+            kube_apis.apps_v1_api,
+            "nginx-ingress",
+            ingress_controller_prerequisites.namespace,
+            replica_count,
+        )
+
+        wait_before_test()
+
+        print("Step 4: check if pods are ready")
+        wait_until_all_pods_are_ready(kube_apis.v1, ingress_controller_prerequisites.namespace)
+
+        print("Step 5: check plus api for zone sync")
+        api_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.api_port}"
+
+        stream_url = f"{api_url}/api/{NGINX_API_VERSION}/stream"
+        assert wait_for_zone_sync_enabled(stream_url)
+
+        zone_sync_url = f"{stream_url}/zone_sync"
+        assert wait_for_zone_sync_nodes_online(zone_sync_url, replica_count)
+
+        print("Step 6: check plus api if zone is synced")
+        assert check_synced_zone_exists(zone_sync_url, pol_name.replace("-", "_", -1))
+
+        # revert changes
+        scale_deployment(
+            kube_apis.v1,
+            kube_apis.apps_v1_api,
+            "nginx-ingress",
+            ingress_controller_prerequisites.namespace,
+            1,
+        )
+        self.restore_default_vs(kube_apis, virtual_server_setup)
+        replace_configmap_from_yaml(
+            kube_apis.v1,
+            configmap_name,
+            ingress_controller_prerequisites.namespace,
+            f"{TEST_DATA}/zone-sync/default-configmap.yaml",
+        )
+        delete_policy(kube_apis.custom_objects, pol_name, test_namespace)
+
     @pytest.mark.skip_for_nginx_oss
     @pytest.mark.parametrize("src", [rl_vs_jwt_claim_sub])
     def test_rl_policy_jwt_claim_sub(
diff --git a/tests/suite/test_rl_policies_vsr.py b/tests/suite/test_rl_policies_vsr.py
index 3aabb9f1b..50e1753fd 100644
--- a/tests/suite/test_rl_policies_vsr.py
+++ b/tests/suite/test_rl_policies_vsr.py
@@ -5,8 +5,20 @@ import pytest
 import requests
 from settings import TEST_DATA
 from suite.utils.custom_resources_utils import read_custom_resource
+from suite.utils.nginx_api_utils import (
+    check_synced_zone_exists,
+    wait_for_zone_sync_enabled,
+    wait_for_zone_sync_nodes_online,
+)
 from suite.utils.policy_resources_utils import apply_and_assert_valid_policy, create_policy_from_yaml, delete_policy
-from suite.utils.resources_utils import get_pod_list, scale_deployment, wait_before_test, wait_for_event
+from suite.utils.resources_utils import (
+    get_pod_list,
+    replace_configmap_from_yaml,
+    scale_deployment,
+    wait_before_test,
+    wait_for_event,
+    wait_until_all_pods_are_ready,
+)
 from suite.utils.vs_vsr_resources_utils import (
     apply_and_assert_valid_vs,
     apply_and_assert_valid_vsr,
@@ -79,6 +91,7 @@ rl_pol_gold_no_default_jwt_claim_sub = (
                 "extra_args": [
                     f"-enable-custom-resources",
                     f"-enable-leader-election=false",
+                    "-nginx-status-allow-cidrs=0.0.0.0/0,::/0",
                 ],
             },
             {"example": "virtual-server-route"},
@@ -193,6 +206,7 @@ class TestRateLimitingPoliciesVsr:
             headers={"host": v_s_route_setup.vs_host},
         )
 
+        self.restore_default_vsr(kube_apis, v_s_route_setup)
         delete_policy(kube_apis.custom_objects, pol_name, v_s_route_setup.route_m.namespace)
 
     @pytest.mark.parametrize("src", [rl_vsr_override_src])
@@ -409,6 +423,87 @@ class TestRateLimitingPoliciesVsr:
         delete_policy(kube_apis.custom_objects, pol_name, v_s_route_setup.route_m.namespace)
         self.restore_default_vsr(kube_apis, v_s_route_setup)
 
+    @pytest.mark.skip_for_nginx_oss
+    @pytest.mark.parametrize("src", [rl_vsr_sec_src])
+    def test_rl_policy_5rs_with_zone_sync_vsr(
+        self,
+        kube_apis,
+        ingress_controller_prerequisites,
+        ingress_controller_endpoint,
+        crd_ingress_controller,
+        v_s_route_app_setup,
+        v_s_route_setup,
+        test_namespace,
+        src,
+    ):
+        """
+        Test pods are scaled to 3, ZoneSync is enabled & Policy zone is synced
+        """
+        replica_count = 3
+        NGINX_API_VERSION = 9
+        pol_name = apply_and_assert_valid_policy(kube_apis, v_s_route_setup.route_m.namespace, rl_pol_sec_src)
+
+        configmap_name = "nginx-config"
+
+        print("Step 1: apply minimal zone_sync nginx-config map")
+        replace_configmap_from_yaml(
+            kube_apis.v1,
+            configmap_name,
+            ingress_controller_prerequisites.namespace,
+            f"{TEST_DATA}/zone-sync/configmap-with-zonesync-minimal.yaml",
+        )
+
+        print("Step 2: apply the policy to the virtual server route")
+        apply_and_assert_valid_vsr(
+            kube_apis,
+            v_s_route_setup.route_m.namespace,
+            v_s_route_setup.route_m.name,
+            src,
+        )
+
+        print(f"Step 3: scale deployments to {replica_count}")
+        scale_deployment(
+            kube_apis.v1,
+            kube_apis.apps_v1_api,
+            "nginx-ingress",
+            ingress_controller_prerequisites.namespace,
+            replica_count,
+        )
+
+        wait_before_test()
+
+        print("Step 4: check if pods are ready")
+        wait_until_all_pods_are_ready(kube_apis.v1, ingress_controller_prerequisites.namespace)
+
+        print("Step 5: check plus api for zone sync")
+        api_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.api_port}"
+
+        stream_url = f"{api_url}/api/{NGINX_API_VERSION}/stream"
+        assert wait_for_zone_sync_enabled(stream_url)
+
+        zone_sync_url = f"{stream_url}/zone_sync"
+        assert wait_for_zone_sync_nodes_online(zone_sync_url, replica_count)
+
+        print("Step 6: check plus api if zone is synced")
+        assert check_synced_zone_exists(zone_sync_url, pol_name.replace("-", "_", -1))
+
+        # revert changes
+        scale_deployment(
+            kube_apis.v1,
+            kube_apis.apps_v1_api,
+            "nginx-ingress",
+            ingress_controller_prerequisites.namespace,
+            1,
+        )
+        self.restore_default_vsr(kube_apis, v_s_route_setup)
+        replace_configmap_from_yaml(
+            kube_apis.v1,
+            configmap_name,
+            ingress_controller_prerequisites.namespace,
+            f"{TEST_DATA}/zone-sync/default-configmap.yaml",
+        )
+        delete_policy(kube_apis.custom_objects, pol_name, v_s_route_setup.route_m.namespace)
+
     @pytest.mark.skip_for_nginx_oss
     @pytest.mark.parametrize("src", [rl_vsr_jwt_claim_sub_src])
     def test_rl_policy_jwt_claim_sub_vsr(
diff --git a/tests/suite/test_zone_sync.py b/tests/suite/test_zone_sync.py
index 043a398ed..8c1ef359b 100644
--- a/tests/suite/test_zone_sync.py
+++ b/tests/suite/test_zone_sync.py
@@ -1,4 +1,5 @@
 import pytest
+import requests
 from kubernetes.client.exceptions import ApiException
 from settings import TEST_DATA
 from suite.utils.resources_utils import (
@@ -10,6 +11,35 @@ from suite.utils.resources_utils import (
 
 WAIT_TIME = 1
 DEPLOYMENT_NAME = "nginx-ingress"
+NGINX_API_VERSION = 9
+
+
+def assert_zonesync_in_plus_api(ingress_controller_endpoint):
+    """
+    Assert that zone_sync is present in the NGINX Plus API response.
+
+    :param ingress_controller_endpoint: Ingress Controller Endpoint
+    """
+    api_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.api_port}"
+    stream_url = f"{api_url}/api/{NGINX_API_VERSION}/stream"
+
+    resp = requests.get(stream_url).json()
+    print(f"response: {resp}")
+    assert "zone_sync" in resp, f"got {resp}"
+
+
+def assert_zonesync_not_in_plus_api(ingress_controller_endpoint):
+    """
+    Assert that zone_sync is not present in the NGINX Plus API response.
+
+    :param ingress_controller_endpoint: Ingress Controller Endpoint
+    """
+    api_url = f"http://{ingress_controller_endpoint.public_ip}:{ingress_controller_endpoint.api_port}"
+    stream_url = f"{api_url}/api/{NGINX_API_VERSION}/stream"
+
+    resp = requests.get(stream_url).json()
+    print(f"response: {resp}")
+    assert "zone_sync" not in resp, f"got {resp}"
 
 
 def assert_event(event_list, event_type, reason, message_substring):
@@ -87,10 +117,18 @@ def assert_headless_service_doesnt_exist(v1, cli_arguments, namespace):
     assert service_exists(v1, cli_arguments, namespace) is False
 
 
-@pytest.mark.zonesync
 @pytest.mark.skip_for_nginx_oss
 @pytest.mark.ingresses
 @pytest.mark.smoke
+@pytest.mark.parametrize(
+    "ingress_controller",
+    [
+        pytest.param(
+            {"extra_args": ["-nginx-status-allow-cidrs=0.0.0.0/0,::/0"]},
+        )
+    ],
+    indirect=["ingress_controller"],
+)
 class TestZoneSyncLifecycle:
     def test_nic_starts_without_zonesync(
         self,
@@ -130,14 +168,17 @@ class TestZoneSyncLifecycle:
 
         wait_before_test(WAIT_TIME)
 
-        print("Step 3: verify zone_sync not present in nginx.conf")
+        print("Step 2: verify zone_sync not present in nginx.conf")
         nginx_config = get_nginx_template_conf(kube_apis.v1, ingress_controller_prerequisites.namespace)
         assert_zonesync_disabled(nginx_config)
 
-        print("Step 4: verify headless service doesn't exist")
+        print("Step 3: verify headless service doesn't exist")
         assert_headless_service_doesnt_exist(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
-        print("Step 4: cleanup: apply default nginx-config map")
+        print("Step 4: check plus api for no zone sync")
+        assert_zonesync_not_in_plus_api(ingress_controller_endpoint)
+
+        print("Step 5: cleanup: apply default nginx-config map")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
@@ -188,12 +229,15 @@ class TestZoneSyncLifecycle:
         nginx_config = get_nginx_template_conf(kube_apis.v1, ingress_controller_prerequisites.namespace)
         assert_zonesync_enabled(nginx_config)
 
-        wait_before_test(5)
+        wait_before_test(WAIT_TIME)
 
         print("Step 3: verify headless service exists")
         assert_headless_service_exists(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
-        print("Step 4: cleanup:  apply default nginx-config map")
+        print("Step 4: check plus api for zone sync")
+        assert_zonesync_in_plus_api(ingress_controller_endpoint)
+
+        print("Step 5: cleanup:  apply default nginx-config map")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
@@ -247,7 +291,10 @@ class TestZoneSyncLifecycle:
         print("Step 4: verify headless service exists")
         assert_headless_service_exists(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
-        print("Step 4: cleanup: apply default nginx-config map")
+        print("Step 5: check plus api for zone sync")
+        assert_zonesync_in_plus_api(ingress_controller_endpoint)
+
+        print("Step 6: cleanup: apply default nginx-config map")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
@@ -288,9 +335,12 @@ class TestZoneSyncLifecycle:
         print("Step 2: verify headless service exists")
         assert_headless_service_exists(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
+        print("Step 3: check plus api for zone sync")
+        assert_zonesync_in_plus_api(ingress_controller_endpoint)
+
         wait_before_test(WAIT_TIME)
 
-        print("Step 3: update the ConfigMap nginx-config - set zone-sync-port to custom value")
+        print("Step 4: update the ConfigMap nginx-config - set zone-sync-port to custom value")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
@@ -300,14 +350,17 @@ class TestZoneSyncLifecycle:
 
         wait_before_test(WAIT_TIME)
 
-        print("Step 4: check if zone_syns port is updated")
+        print("Step 5: check if zone_syns port is updated")
         nginx_config = get_nginx_template_conf(kube_apis.v1, ingress_controller_prerequisites.namespace)
         assert_zonesync_enabled(nginx_config, port="34100")
 
-        print("Step 5: verify headless service exists")
+        print("Step 6: verify headless service exists")
         assert_headless_service_exists(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
-        print("Step 6: cleanup:  apply default nginx-config map")
+        print("Step 7: check plus api for zone sync")
+        assert_zonesync_in_plus_api(ingress_controller_endpoint)
+
+        print("Step 8: cleanup:  apply default nginx-config map")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
@@ -346,9 +399,12 @@ class TestZoneSyncLifecycle:
         print("Step 2: verify headless service exists")
         assert_headless_service_exists(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
+        print("Step 3: check plus api for zone sync")
+        assert_zonesync_in_plus_api(ingress_controller_endpoint)
+
         wait_before_test(WAIT_TIME)
 
-        print("Step 3: update the ConfigMap nginx-config - set zone-sync disabled")
+        print("Step 4: update the ConfigMap nginx-config - set zone-sync disabled")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
@@ -356,14 +412,17 @@ class TestZoneSyncLifecycle:
             f"{TEST_DATA}/zone-sync/configmap-with-zonesync-disabled.yaml",
         )
 
-        print("Step 4: verify zone_sync disabled - not present in nginx.conf")
+        print("Step 5: verify zone_sync disabled - not present in nginx.conf")
         nginx_config = get_nginx_template_conf(kube_apis.v1, ingress_controller_prerequisites.namespace)
         assert_zonesync_disabled(nginx_config)
 
-        print("Step 5: verify headless service doesn't exist")
+        print("Step 6: verify headless service doesn't exist")
         assert_headless_service_doesnt_exist(kube_apis.v1, cli_arguments, ingress_controller_prerequisites.namespace)
 
-        print("Step 6: cleanup:  apply default nginx-config map")
+        print("Step 7: check plus api for zone sync")
+        assert_zonesync_not_in_plus_api(ingress_controller_endpoint)
+
+        print("Step 8: cleanup:  apply default nginx-config map")
         replace_configmap_from_yaml(
             kube_apis.v1,
             configmap_name,
diff --git a/tests/suite/utils/nginx_api_utils.py b/tests/suite/utils/nginx_api_utils.py
index d249b7aae..04fa5b534 100644
--- a/tests/suite/utils/nginx_api_utils.py
+++ b/tests/suite/utils/nginx_api_utils.py
@@ -51,3 +51,69 @@ def wait_for_non_empty_array(request_url) -> None:
         if counter == 10:
             pytest.fail(f"After 10 seconds array is empty, request_url: {request_url}")
         counter = counter + 1
+
+
+def wait_for_zone_sync_enabled(request_url) -> bool:
+    """
+    Wait while the response from the API contains zone_sync.
+
+    :param request_url:
+    :return: bool
+    """
+    interval = 1
+    retry = 120
+    count = 1
+    while count <= retry:
+        print(f"{count}: Calling get on {request_url}")
+        resp = requests.get(request_url)
+        if resp.status_code != 200:
+            return False
+        if "zone_sync" in resp.json():
+            return True
+        count += 1
+        wait_before_test(interval)
+    return False
+
+
+def wait_for_zone_sync_nodes_online(request_url, node_count) -> bool:
+    """
+    Wait while the response from the API contains the correct number of online zone sync nodes.
+
+    :param request_url:
+    :param node_count
+    :return: bool
+    """
+    interval = 1
+    retry = 120
+    count = 1
+    while count <= retry:
+        print(f"{count}: Calling get on {request_url}")
+        resp = requests.get(request_url)
+        if resp.status_code != 200:
+            return False
+        body = resp.json()
+        nodes = body["status"]["nodes_online"]
+        online = node_count - 1  # NGINX only shows remote peers in the `nodes_online` field
+        if nodes == online:
+            return True
+        count += 1
+        wait_before_test(interval)
+    return False
+
+
+def check_synced_zone_exists(request_url, zone_name) -> bool:
+    """
+    Check the response from the API contains the requested zone_name.
+
+    :param request_url:
+    :param zone_name
+    :return: bool
+    """
+    resp = requests.get(request_url)
+    if resp.status_code != 200:
+        return False
+    body = resp.json()
+    for zone in body["zones"].keys():
+        if zone_name in zone:
+            return True
+    return False