Repository: nginx/kubernetes-ingress

Issue:
VirtualServer support for services/upstreams in foreign namespace
VS mandates that upstreams must reside in the same namespace as the VS resource itself, creating complexity for multi-tenant environments where namespaces are isolated per tenant.

NIC users operating at scale (e.g., VS and VSRs sitting across multiple namespaces) are experiencing challenges due to the current coupling of VS and VSR CRD's. 

Some users want to simplify this by containing all updates within one VS itself, as an option to reducing maintenance overhead.


**Requirements and Acceptance Criteria**

- Modify VirtualServer to support routing to upstreams/services in different namespaces.
- Ensure backward compatibility for existing VS/VSR configurations.
- Update documentation
- Existing VS/VSR setups remain unaffected.


Tasks:

- [x] Design 
- [x] POC 

Discussion:
@github-actions: Hi @mkingst 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/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml
index d92b2cde2..544718e87 100644
--- a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml
+++ b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml
@@ -1095,12 +1095,13 @@ spec:
                         ConfigMap key.
                       type: string
                     service:
-                      description: The name of a service. The service must belong
-                        to the same namespace as the resource. If the service doesn’t
-                        exist, NGINX will assume the service has zero endpoints and
-                        return a 502 response for requests for this upstream. For
-                        NGINX Plus only, services of type ExternalName are also supported
-                        .
+                      description: The name of a service. If the Service belongs to
+                        a different namespace than the VirtualServer or VirtualServerRoute,
+                        you need to include the namespace. For example, tea-namespace/tea.
+                        If the service doesn’t exist, NGINX will assume the service
+                        has zero endpoints and return a 502 response for requests
+                        for this upstream. For NGINX Plus only, services of type ExternalName
+                        are also supported in the same namespace.
                       type: string
                     sessionCookie:
                       description: The SessionCookie field configures session persistence
diff --git a/config/crd/bases/k8s.nginx.org_virtualservers.yaml b/config/crd/bases/k8s.nginx.org_virtualservers.yaml
index a8bf0a26a..ac5bdd456 100644
--- a/config/crd/bases/k8s.nginx.org_virtualservers.yaml
+++ b/config/crd/bases/k8s.nginx.org_virtualservers.yaml
@@ -1284,12 +1284,13 @@ spec:
                         ConfigMap key.
                       type: string
                     service:
-                      description: The name of a service. The service must belong
-                        to the same namespace as the resource. If the service doesn’t
-                        exist, NGINX will assume the service has zero endpoints and
-                        return a 502 response for requests for this upstream. For
-                        NGINX Plus only, services of type ExternalName are also supported
-                        .
+                      description: The name of a service. If the Service belongs to
+                        a different namespace than the VirtualServer or VirtualServerRoute,
+                        you need to include the namespace. For example, tea-namespace/tea.
+                        If the service doesn’t exist, NGINX will assume the service
+                        has zero endpoints and return a 502 response for requests
+                        for this upstream. For NGINX Plus only, services of type ExternalName
+                        are also supported in the same namespace.
                       type: string
                     sessionCookie:
                       description: The SessionCookie field configures session persistence
diff --git a/deploy/crds.yaml b/deploy/crds.yaml
index c39c2a0ab..fd080f2d5 100644
--- a/deploy/crds.yaml
+++ b/deploy/crds.yaml
@@ -2133,12 +2133,13 @@ spec:
                         ConfigMap key.
                       type: string
                     service:
-                      description: The name of a service. The service must belong
-                        to the same namespace as the resource. If the service doesn’t
-                        exist, NGINX will assume the service has zero endpoints and
-                        return a 502 response for requests for this upstream. For
-                        NGINX Plus only, services of type ExternalName are also supported
-                        .
+                      description: The name of a service. If the Service belongs to
+                        a different namespace than the VirtualServer or VirtualServerRoute,
+                        you need to include the namespace. For example, tea-namespace/tea.
+                        If the service doesn’t exist, NGINX will assume the service
+                        has zero endpoints and return a 502 response for requests
+                        for this upstream. For NGINX Plus only, services of type ExternalName
+                        are also supported in the same namespace.
                       type: string
                     sessionCookie:
                       description: The SessionCookie field configures session persistence
@@ -3551,12 +3552,13 @@ spec:
                         ConfigMap key.
                       type: string
                     service:
-                      description: The name of a service. The service must belong
-                        to the same namespace as the resource. If the service doesn’t
-                        exist, NGINX will assume the service has zero endpoints and
-                        return a 502 response for requests for this upstream. For
-                        NGINX Plus only, services of type ExternalName are also supported
-                        .
+                      description: The name of a service. If the Service belongs to
+                        a different namespace than the VirtualServer or VirtualServerRoute,
+                        you need to include the namespace. For example, tea-namespace/tea.
+                        If the service doesn’t exist, NGINX will assume the service
+                        has zero endpoints and return a 502 response for requests
+                        for this upstream. For NGINX Plus only, services of type ExternalName
+                        are also supported in the same namespace.
                       type: string
                     sessionCookie:
                       description: The SessionCookie field configures session persistence
diff --git a/docs/crd/k8s.nginx.org_virtualserverroutes.md b/docs/crd/k8s.nginx.org_virtualserverroutes.md
index bcf4c2a92..52426dace 100644
--- a/docs/crd/k8s.nginx.org_virtualserverroutes.md
+++ b/docs/crd/k8s.nginx.org_virtualserverroutes.md
@@ -209,7 +209,7 @@ The `.spec` object supports the following fields:
 | `upstreams[].queue.timeout` | `string` | The timeout of the queue. A request cannot be queued for a period longer than the timeout. The default is 60s. |
 | `upstreams[].read-timeout` | `string` | The timeout for reading a response from an upstream server. The default is specified in the proxy-read-timeout ConfigMap key. |
 | `upstreams[].send-timeout` | `string` | The timeout for transmitting a request to an upstream server. The default is specified in the proxy-send-timeout ConfigMap key. |
-| `upstreams[].service` | `string` | The name of a service. The service must belong to the same namespace as the resource. If the service doesn’t exist, NGINX will assume the service has zero endpoints and return a 502 response for requests for this upstream. For NGINX Plus only, services of type ExternalName are also supported . |
+| `upstreams[].service` | `string` | The name of a service. If the Service belongs to a different namespace than the VirtualServer or VirtualServerRoute, you need to include the namespace. For example, tea-namespace/tea. If the service doesn’t exist, NGINX will assume the service has zero endpoints and return a 502 response for requests for this upstream. For NGINX Plus only, services of type ExternalName are also supported in the same namespace. |
 | `upstreams[].sessionCookie` | `object` | The SessionCookie field configures session persistence which allows requests from the same client to be passed to the same upstream server. The information about the designated upstream server is passed in a session cookie generated by NGINX Plus. |
 | `upstreams[].sessionCookie.domain` | `string` | The domain for which the cookie is set. |
 | `upstreams[].sessionCookie.enable` | `boolean` | Enables session persistence with a session cookie for an upstream server. The default is false. |
diff --git a/docs/crd/k8s.nginx.org_virtualservers.md b/docs/crd/k8s.nginx.org_virtualservers.md
index cc0268c77..280e1bb8b 100644
--- a/docs/crd/k8s.nginx.org_virtualservers.md
+++ b/docs/crd/k8s.nginx.org_virtualservers.md
@@ -244,7 +244,7 @@ The `.spec` object supports the following fields:
 | `upstreams[].queue.timeout` | `string` | The timeout of the queue. A request cannot be queued for a period longer than the timeout. The default is 60s. |
 | `upstreams[].read-timeout` | `string` | The timeout for reading a response from an upstream server. The default is specified in the proxy-read-timeout ConfigMap key. |
 | `upstreams[].send-timeout` | `string` | The timeout for transmitting a request to an upstream server. The default is specified in the proxy-send-timeout ConfigMap key. |
-| `upstreams[].service` | `string` | The name of a service. The service must belong to the same namespace as the resource. If the service doesn’t exist, NGINX will assume the service has zero endpoints and return a 502 response for requests for this upstream. For NGINX Plus only, services of type ExternalName are also supported . |
+| `upstreams[].service` | `string` | The name of a service. If the Service belongs to a different namespace than the VirtualServer or VirtualServerRoute, you need to include the namespace. For example, tea-namespace/tea. If the service doesn’t exist, NGINX will assume the service has zero endpoints and return a 502 response for requests for this upstream. For NGINX Plus only, services of type ExternalName are also supported in the same namespace. |
 | `upstreams[].sessionCookie` | `object` | The SessionCookie field configures session persistence which allows requests from the same client to be passed to the same upstream server. The information about the designated upstream server is passed in a session cookie generated by NGINX Plus. |
 | `upstreams[].sessionCookie.domain` | `string` | The domain for which the cookie is set. |
 | `upstreams[].sessionCookie.enable` | `boolean` | Enables session persistence with a session cookie for an upstream server. The default is false. |
diff --git a/examples/custom-resources/foreign-namespace-upstreams/README.md b/examples/custom-resources/foreign-namespace-upstreams/README.md
new file mode 100644
index 000000000..d46b59e2b
--- /dev/null
+++ b/examples/custom-resources/foreign-namespace-upstreams/README.md
@@ -0,0 +1,109 @@
+# Upstreams in foreign namespaces
+
+In this example we use the [VirtualServer and
+VirtualServerRoute](https://docs.nginx.com/nginx-ingress-controller/configuration/virtualserver-and-virtualserverroute-resources/)
+resources to configure load balancing for the modified cafe application from the [Basic
+Configuration](../basic-configuration/) example. We have put the load balancing configuration as well as the deployments
+and services into multiple namespaces. Instead of one namespace, we now use three: `tea`, `coffee`, and `cafe`.
+
+- In the tea namespace, we create the tea deployment and service.
+- In the coffee namespace, we create the coffee deployment and service.
+- In the cafe namespace, we create the cafe secret with the TLS certificate and key and the load-balancing configuration
+  for the cafe application. That configuration references the coffee and tea configurations.
+
+**Note:** When using upstreams in foreign namespaces, ensure that the NGINX Ingress Controller is configured to watch all the relevant namespaces. If you are using the `-watch-namespace` flag, make sure to include all namespaces that contain services referenced by your VirtualServer resources (in this case: `tea`, `coffee`, and `cafe`).
+
+## Prerequisites
+
+1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/)
+   instructions to deploy the Ingress Controller with custom resources enabled.
+1. Save the public IP address of the Ingress Controller into a shell variable:
+
+    ```console
+    IC_IP=XXX.YYY.ZZZ.III
+    ```
+
+1. Save the HTTPS port of the Ingress Controller into a shell variable:
+
+    ```console
+    IC_HTTPS_PORT=<port number>
+    ```
+
+## Step 1 - Create Namespaces
+
+Create the required tea, coffee, and cafe namespaces:
+
+```console
+kubectl create -f namespaces.yaml
+```
+
+## Step 2 - Deploy the Cafe Application
+
+1. Create the tea deployment and service in the tea namespace:
+
+    ```console
+    kubectl create -f tea.yaml
+    ```
+
+1. Create the coffee deployment and service in the coffee namespace:
+
+    ```console
+    kubectl create -f coffee.yaml
+    ```
+
+## Step 3 - Configure Load Balancing and TLS Termination
+
+1. Create the secret with the TLS certificate and key in the cafe namespace:
+
+    ```console
+    kubectl create -f cafe-secret.yaml
+    ```
+
+1. Create the VirtualServer resource for the cafe app in the cafe namespace:
+
+    ```console
+    kubectl create -f cafe-virtual-server.yaml
+    ```
+
+## Step 4 - Test the Configuration
+
+1. Check that the configuration has been successfully applied by inspecting the events of the VirtualServer:
+
+    ```console
+    kubectl describe virtualserver cafe -n cafe
+    ```
+
+    ```text
+    . . .
+    Events:
+      Type    Reason          Age   From                      Message
+      ----    ------          ----  ----                      -------
+      Normal  AddedOrUpdated  1m    nginx-ingress-controller  Configuration for cafe/cafe was added or updated
+    ```
+
+1. Access the application using curl. We'll use curl's `--insecure` option to turn off certificate verification of our
+   self-signed certificate and `--resolve` option to set the IP address and HTTPS port of the Ingress Controller to the
+   domain name of the cafe application:
+
+    To get coffee:
+
+    ```console
+    curl --resolve cafe.example.com:$IC_HTTPS_PORT:$IC_IP https://cafe.example.com:$IC_HTTPS_PORT/coffee --insecure
+    ```
+
+    ```text
+    Server address: 10.16.1.193:80
+    Server name: coffee-7dbb5795f6-mltpf
+    ...
+    ```
+
+    If you prefer tea:
+
+    ```console
+    curl --resolve cafe.example.com:$IC_HTTPS_PORT:$IC_IP https://cafe.example.com:$IC_HTTPS_PORT/tea --insecure
+    ```
+
+    ```text
+    Server address: 10.16.0.157:80
+    Server name: tea-7d57856c44-674b8
+    ...
diff --git a/examples/custom-resources/foreign-namespace-upstreams/cafe-secret.yaml b/examples/custom-resources/foreign-namespace-upstreams/cafe-secret.yaml
new file mode 120000
index 000000000..6d8cd13e7
--- /dev/null
+++ b/examples/custom-resources/foreign-namespace-upstreams/cafe-secret.yaml
@@ -0,0 +1 @@
+../../common-secrets/cafe-secret-cafe-ns.example.com.yaml
\ No newline at end of file
diff --git a/examples/custom-resources/foreign-namespace-upstreams/cafe-virtual-server.yaml b/examples/custom-resources/foreign-namespace-upstreams/cafe-virtual-server.yaml
new file mode 100644
index 000000000..48c1d7611
--- /dev/null
+++ b/examples/custom-resources/foreign-namespace-upstreams/cafe-virtual-server.yaml
@@ -0,0 +1,23 @@
+apiVersion: k8s.nginx.org/v1
+kind: VirtualServer
+metadata:
+  name: cafe
+  namespace: cafe
+spec:
+  host: cafe.example.com
+  tls:
+    secret: cafe-secret
+  routes:
+  - path: /coffee
+    action:
+      pass: coffee
+  - path: /tea
+    action:
+      pass: tea
+  upstreams:
+  - name: coffee
+    service: coffee/coffee-svc
+    port: 80
+  - name: tea
+    service: tea/tea-svc
+    port: 80
diff --git a/examples/custom-resources/foreign-namespace-upstreams/coffee.yaml b/examples/custom-resources/foreign-namespace-upstreams/coffee.yaml
new file mode 100644
index 000000000..df52a9504
--- /dev/null
+++ b/examples/custom-resources/foreign-namespace-upstreams/coffee.yaml
@@ -0,0 +1,70 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coffee
+  namespace: coffee
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: coffee
+  template:
+    metadata:
+      labels:
+        app: coffee
+    spec:
+      containers:
+      - name: coffee
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coffee-svc
+  namespace: coffee
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: coffee
+
+---
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: coffee
+  namespace: coffee2
+spec:
+  replicas: 3
+  selector:
+    matchLabels:
+      app: coffee
+  template:
+    metadata:
+      labels:
+        app: coffee
+    spec:
+      containers:
+      - name: coffee
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: coffee-svc
+  namespace: coffee2
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: coffee
diff --git a/examples/custom-resources/foreign-namespace-upstreams/namespaces.yaml b/examples/custom-resources/foreign-namespace-upstreams/namespaces.yaml
new file mode 100644
index 000000000..6c47c83f3
--- /dev/null
+++ b/examples/custom-resources/foreign-namespace-upstreams/namespaces.yaml
@@ -0,0 +1,14 @@
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: cafe
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: tea
+---
+apiVersion: v1
+kind: Namespace
+metadata:
+  name: coffee
diff --git a/examples/custom-resources/foreign-namespace-upstreams/tea.yaml b/examples/custom-resources/foreign-namespace-upstreams/tea.yaml
new file mode 100644
index 000000000..615e2fd43
--- /dev/null
+++ b/examples/custom-resources/foreign-namespace-upstreams/tea.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: tea
+  namespace: tea
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: tea
+  template:
+    metadata:
+      labels:
+        app: tea
+    spec:
+      containers:
+      - name: tea
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: tea-svc
+  namespace: tea
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: tea
diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go
index eedc6a48e..8dc42d007 100644
--- a/internal/configs/virtualserver.go
+++ b/internal/configs/virtualserver.go
@@ -145,6 +145,17 @@ func GenerateEndpointsKey(
 	return fmt.Sprintf("%s/%s:%d", serviceNamespace, serviceName, port)
 }
 
+// ParseServiceReference returns the namespace and name from a service reference.
+func ParseServiceReference(serviceRef, defaultNamespace string) (namespace, serviceName string) {
+	if strings.Contains(serviceRef, "/") {
+		parts := strings.Split(serviceRef, "/")
+		if len(parts) == 2 {
+			return parts[0], parts[1]
+		}
+	}
+	return defaultNamespace, serviceRef
+}
+
 type upstreamNamer struct {
 	prefix    string
 	namespace string
@@ -353,7 +364,8 @@ func (vsc *virtualServerConfigurator) generateEndpointsForUpstream(
 	upstream conf_v1.Upstream,
 	virtualServerEx *VirtualServerEx,
 ) []string {
-	endpointsKey := GenerateEndpointsKey(namespace, upstream.Service, upstream.Subselector, upstream.Port)
+	serviceNamespace, serviceName := ParseServiceReference(upstream.Service, namespace)
+	endpointsKey := GenerateEndpointsKey(serviceNamespace, serviceName, upstream.Subselector, upstream.Port)
 	externalNameSvcKey := GenerateExternalNameSvcKey(namespace, upstream.Service)
 	endpoints := virtualServerEx.Endpoints[endpointsKey]
 	if !vsc.isPlus && len(endpoints) == 0 {
@@ -659,7 +671,8 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(
 			upstreamName := virtualServerUpstreamNamer.GetNameForUpstreamFromAction(r.Action)
 			upstream := crUpstreams[upstreamName]
 
-			proxySSLName := generateProxySSLName(upstream.Service, vsEx.VirtualServer.Namespace)
+			serviceNamespace, serviceName := ParseServiceReference(upstream.Service, vsEx.VirtualServer.Namespace)
+			proxySSLName := generateProxySSLName(serviceName, serviceNamespace)
 
 			loc, returnLoc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, errorPages, false,
 				proxySSLName, r.Path, vsLocSnippets, vsc.enableSnippets, len(returnLocations), isVSR, "", "", vsc.warnings)
@@ -812,7 +825,8 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(
 			} else {
 				upstreamName := upstreamNamer.GetNameForUpstreamFromAction(r.Action)
 				upstream := crUpstreams[upstreamName]
-				proxySSLName := generateProxySSLName(upstream.Service, vsr.Namespace)
+				serviceNamespace, serviceName := ParseServiceReference(upstream.Service, vsr.Namespace)
+				proxySSLName := generateProxySSLName(serviceName, serviceNamespace)
 
 				loc, returnLoc := generateLocation(r.Path, upstreamName, upstream, r.Action, vsc.cfgParams, errorPages, false,
 					proxySSLName, r.Path, locSnippets, vsc.enableSnippets, len(returnLocations), isVSR, vsr.Name, vsr.Namespace, vsc.warnings)
@@ -2535,8 +2549,10 @@ func generateLocation(path string, upstreamName string, upstream conf_v1.Upstrea
 
 	checkGrpcErrorPageCodes(errorPages, isGRPC(upstream.Type), upstream.Name, vscWarnings)
 
+	_, serviceName := ParseServiceReference(upstream.Service, "")
+
 	return generateLocationForProxying(path, upstreamName, upstream, cfgParams, errorPages.pages, internal,
-		errorPages.index, proxySSLName, action.Proxy, originalPath, locationSnippets, isVSR, vsrName, vsrNamespace), nil
+		errorPages.index, proxySSLName, action.Proxy, originalPath, locationSnippets, isVSR, vsrName, vsrNamespace, serviceName), nil
 }
 
 func generateProxySetHeaders(proxy *conf_v1.ActionProxy) []version2.Header {
@@ -2621,7 +2637,7 @@ func generateProxyAddHeaders(proxy *conf_v1.ActionProxy) []version2.AddHeader {
 
 func generateLocationForProxying(path string, upstreamName string, upstream conf_v1.Upstream,
 	cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int,
-	proxySSLName string, proxy *conf_v1.ActionProxy, originalPath string, locationSnippets []string, isVSR bool, vsrName string, vsrNamespace string,
+	proxySSLName string, proxy *conf_v1.ActionProxy, originalPath string, locationSnippets []string, isVSR bool, vsrName string, vsrNamespace string, serviceName string,
 ) version2.Location {
 	return version2.Location{
 		Path:                     generatePath(path),
@@ -2652,7 +2668,7 @@ func generateLocationForProxying(path string, upstreamName string, upstream conf
 		HasKeepalive:             upstreamHasKeepalive(upstream, cfgParams),
 		ErrorPages:               generateErrorPages(errPageIndex, errorPages),
 		ProxySSLName:             proxySSLName,
-		ServiceName:              upstream.Service,
+		ServiceName:              serviceName,
 		IsVSR:                    isVSR,
 		VSRName:                  vsrName,
 		VSRNamespace:             vsrNamespace,
@@ -2823,7 +2839,8 @@ func generateSplits(
 		path := fmt.Sprintf("/%vsplits_%d_split_%d", internalLocationPrefix, scIndex, i)
 		upstreamName := upstreamNamer.GetNameForUpstreamFromAction(s.Action)
 		upstream := crUpstreams[upstreamName]
-		proxySSLName := generateProxySSLName(upstream.Service, upstreamNamer.namespace)
+		serviceNamespace, serviceName := ParseServiceReference(upstream.Service, upstreamNamer.namespace)
+		proxySSLName := generateProxySSLName(serviceName, serviceNamespace)
 		newRetLocIndex := retLocIndex + len(returnLocations)
 		loc, returnLoc := generateLocation(path, upstreamName, upstream, s.Action, cfgParams, errorPages, true,
 			proxySSLName, originalPath, locSnippets, enableSnippets, newRetLocIndex, isVSR, vsrName, vsrNamespace, vscWarnings)
@@ -3056,7 +3073,8 @@ func generateMatchesConfig(route conf_v1.Route, upstreamNamer *upstreamNamer, cr
 			path := fmt.Sprintf("/%vmatches_%d_match_%d", internalLocationPrefix, index, i)
 			upstreamName := upstreamNamer.GetNameForUpstreamFromAction(m.Action)
 			upstream := crUpstreams[upstreamName]
-			proxySSLName := generateProxySSLName(upstream.Service, upstreamNamer.namespace)
+			serviceNamespace, serviceName := ParseServiceReference(upstream.Service, upstreamNamer.namespace)
+			proxySSLName := generateProxySSLName(serviceName, serviceNamespace)
 			newRetLocIndex := retLocIndex + len(returnLocations)
 			loc, returnLoc := generateLocation(path, upstreamName, upstream, m.Action, cfgParams, errorPages, true,
 				proxySSLName, route.Path, locSnippets, enableSnippets, newRetLocIndex, isVSR, vsrName, vsrNamespace, vscWarnings)
@@ -3099,7 +3117,8 @@ func generateMatchesConfig(route conf_v1.Route, upstreamNamer *upstreamNamer, cr
 		path := fmt.Sprintf("/%vmatches_%d_default", internalLocationPrefix, index)
 		upstreamName := upstreamNamer.GetNameForUpstreamFromAction(route.Action)
 		upstream := crUpstreams[upstreamName]
-		proxySSLName := generateProxySSLName(upstream.Service, upstreamNamer.namespace)
+		serviceNamespace, serviceName := ParseServiceReference(upstream.Service, upstreamNamer.namespace)
+		proxySSLName := generateProxySSLName(serviceName, serviceNamespace)
 		newRetLocIndex := retLocIndex + len(returnLocations)
 		loc, returnLoc := generateLocation(path, upstreamName, upstream, route.Action, cfgParams, errorPages, true,
 			proxySSLName, route.Path, locSnippets, enableSnippets, newRetLocIndex, isVSR, vsrName, vsrNamespace, vscWarnings)
@@ -3288,9 +3307,9 @@ func createUpstreamsForPlus(
 		}
 
 		upstreamName := upstreamNamer.GetNameForUpstream(u.Name)
-		upstreamNamespace := virtualServerEx.VirtualServer.Namespace
+		upstreamNamespace, upstreamServiceName := ParseServiceReference(u.Service, virtualServerEx.VirtualServer.Namespace)
 
-		endpointsKey := GenerateEndpointsKey(upstreamNamespace, u.Service, u.Subselector, u.Port)
+		endpointsKey := GenerateEndpointsKey(upstreamNamespace, upstreamServiceName, u.Subselector, u.Port)
 		endpoints := virtualServerEx.Endpoints[endpointsKey]
 
 		backupEndpoints := []string{}
@@ -3312,15 +3331,15 @@ func createUpstreamsForPlus(
 			}
 
 			upstreamName := upstreamNamer.GetNameForUpstream(u.Name)
-			upstreamNamespace := vsr.Namespace
+			serviceNamespace, serviceName := ParseServiceReference(u.Service, vsr.Namespace)
 
-			endpointsKey := GenerateEndpointsKey(upstreamNamespace, u.Service, u.Subselector, u.Port)
+			endpointsKey := GenerateEndpointsKey(serviceNamespace, serviceName, u.Subselector, u.Port)
 			endpoints := virtualServerEx.Endpoints[endpointsKey]
 
 			// BackupService
 			backupEndpoints := []string{}
 			if u.Backup != "" {
-				backupEndpointsKey := GenerateEndpointsKey(upstreamNamespace, u.Backup, u.Subselector, *u.BackupPort)
+				backupEndpointsKey := GenerateEndpointsKey(vsr.Namespace, u.Backup, u.Subselector, *u.BackupPort)
 				backupEndpoints = virtualServerEx.Endpoints[backupEndpointsKey]
 			}
 			ups := vsc.generateUpstream(vsr, upstreamName, u, isExternalNameSvc, endpoints, backupEndpoints)
diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go
index c3ac93467..58ad41d19 100644
--- a/internal/k8s/controller.go
+++ b/internal/k8s/controller.go
@@ -2467,11 +2467,12 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.
 	}
 
 	for _, u := range virtualServer.Spec.Upstreams {
-		endpointsKey := configs.GenerateEndpointsKey(virtualServer.Namespace, u.Service, u.Subselector, u.Port)
+		serviceNamespace, serviceName := configs.ParseServiceReference(u.Service, virtualServer.Namespace)
+		endpointsKey := configs.GenerateEndpointsKey(serviceNamespace, serviceName, u.Subselector, u.Port)
 
 		var endps []string
 		if u.UseClusterIP {
-			s, err := lbc.getServiceForUpstream(virtualServer.Namespace, u.Service, u.Port)
+			s, err := lbc.getServiceForUpstream(serviceNamespace, serviceName, u.Port)
 			if err != nil {
 				nl.Warnf(lbc.Logger, "Error getting Service for Upstream %v: %v", u.Service, err)
 			} else {
@@ -2483,13 +2484,13 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.
 			var err error
 
 			if len(u.Subselector) > 0 {
-				podEndps, err = lbc.getEndpointsForSubselector(virtualServer.Namespace, u)
+				podEndps, err = lbc.getEndpointsForSubselector(serviceNamespace, serviceName, u.Port, u.Subselector)
 			} else {
 				var external bool
-				podEndps, external, err = lbc.getEndpointsForUpstream(virtualServer.Namespace, u.Service, u.Port)
+				podEndps, external, err = lbc.getEndpointsForUpstream(serviceNamespace, serviceName, u.Port)
 
 				if err == nil && external && lbc.isNginxPlus {
-					externalNameSvcs[configs.GenerateExternalNameSvcKey(virtualServer.Namespace, u.Service)] = true
+					externalNameSvcs[configs.GenerateExternalNameSvcKey(serviceNamespace, serviceName)] = true
 				}
 			}
 
@@ -2614,11 +2615,12 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.
 		}
 
 		for _, u := range vsr.Spec.Upstreams {
-			endpointsKey := configs.GenerateEndpointsKey(vsr.Namespace, u.Service, u.Subselector, u.Port)
+			serviceNamespace, serviceName := configs.ParseServiceReference(u.Service, vsr.Namespace)
+			endpointsKey := configs.GenerateEndpointsKey(serviceNamespace, serviceName, u.Subselector, u.Port)
 
 			var endps []string
 			if u.UseClusterIP {
-				s, err := lbc.getServiceForUpstream(vsr.Namespace, u.Service, u.Port)
+				s, err := lbc.getServiceForUpstream(serviceNamespace, serviceName, u.Port)
 				if err != nil {
 					nl.Warnf(lbc.Logger, "Error getting Service for Upstream %v: %v", u.Service, err)
 				} else {
@@ -2629,13 +2631,13 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1.
 				var podEndps []podEndpoint
 				var err error
 				if len(u.Subselector) > 0 {
-					podEndps, err = lbc.getEndpointsForSubselector(vsr.Namespace, u)
+					podEndps, err = lbc.getEndpointsForSubselector(serviceNamespace, serviceName, u.Port, u.Subselector)
 				} else {
 					var external bool
-					podEndps, external, err = lbc.getEndpointsForUpstream(vsr.Namespace, u.Service, u.Port)
+					podEndps, external, err = lbc.getEndpointsForUpstream(serviceNamespace, serviceName, u.Port)
 
 					if err == nil && external && lbc.isNginxPlus {
-						externalNameSvcs[configs.GenerateExternalNameSvcKey(vsr.Namespace, u.Service)] = true
+						externalNameSvcs[configs.GenerateExternalNameSvcKey(serviceNamespace, serviceName)] = true
 					}
 				}
 				if err != nil {
@@ -2967,31 +2969,31 @@ func (lbc *LoadBalancerController) getEndpointsForUpstream(namespace string, ups
 	return endps, isExternal, err
 }
 
-func (lbc *LoadBalancerController) getEndpointsForSubselector(namespace string, upstream conf_v1.Upstream) (endps []podEndpoint, err error) {
-	svc, err := lbc.getServiceForUpstream(namespace, upstream.Service, upstream.Port)
+func (lbc *LoadBalancerController) getEndpointsForSubselector(namespace string, serviceName string, servicePort uint16, subselector map[string]string) (endps []podEndpoint, err error) {
+	svc, err := lbc.getServiceForUpstream(namespace, serviceName, servicePort)
 	if err != nil {
-		return nil, fmt.Errorf("error getting service %v: %w", upstream.Service, err)
+		return nil, fmt.Errorf("error getting service %v: %w", serviceName, err)
 	}
 
 	var targetPort int32
 
 	for _, port := range svc.Spec.Ports {
-		if port.Port == int32(upstream.Port) {
+		if port.Port == int32(servicePort) {
 			targetPort, err = lbc.getTargetPort(port, svc)
 			if err != nil {
-				return nil, fmt.Errorf("error determining target port for port %v in service %v: %w", upstream.Port, svc.Name, err)
+				return nil, fmt.Errorf("error determining target port for port %v in service %v: %w", servicePort, svc.Name, err)
 			}
 			break
 		}
 	}
 
 	if targetPort == 0 {
-		return nil, fmt.Errorf("no port %v in service %s", upstream.Port, svc.Name)
+		return nil, fmt.Errorf("no port %v in service %s", servicePort, svc.Name)
 	}
 
-	endps, err = lbc.getEndpointsForServiceWithSubselector(targetPort, upstream.Subselector, svc)
+	endps, err = lbc.getEndpointsForServiceWithSubselector(targetPort, subselector, svc)
 	if err != nil {
-		return nil, fmt.Errorf("error retrieving endpoints for the service %v: %w", upstream.Service, err)
+		return nil, fmt.Errorf("error retrieving endpoints for the service %v: %w", serviceName, err)
 	}
 
 	return endps, err
diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go
index 5c0c38719..1f3dd4a6b 100644
--- a/pkg/apis/configuration/v1/types.go
+++ b/pkg/apis/configuration/v1/types.go
@@ -119,7 +119,7 @@ type PolicyReference struct {
 type Upstream struct {
 	// The name of the upstream. Must be a valid DNS label as defined in RFC 1035. For example, hello and upstream-123 are valid. The name must be unique among all upstreams of the resource.
 	Name string `json:"name"`
-	// The name of a service. The service must belong to the same namespace as the resource. If the service doesn’t exist, NGINX will assume the service has zero endpoints and return a 502 response for requests for this upstream. For NGINX Plus only, services of type ExternalName are also supported .
+	// The name of a service. If the Service belongs to a different namespace than the VirtualServer or VirtualServerRoute, you need to include the namespace. For example, tea-namespace/tea. If the service doesn’t exist, NGINX will assume the service has zero endpoints and return a 502 response for requests for this upstream. For NGINX Plus only, services of type ExternalName are also supported in the same namespace.
 	Service string `json:"service"`
 	// Selects the pods within the service using label keys and values. By default, all pods of the service are selected. Note: the specified labels are expected to be present in the pods when they are created. If the pod labels are updated, NGINX Ingress Controller will not see that change until the number of the pods is changed.
 	Subselector map[string]string `json:"subselector"`
diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go
index 136966ec5..e5edfff29 100644
--- a/pkg/apis/configuration/validation/virtualserver.go
+++ b/pkg/apis/configuration/validation/virtualserver.go
@@ -621,7 +621,7 @@ func (vsv *VirtualServerValidator) validateUpstreams(upstreams []v1.Upstream, fi
 			allErrs = append(allErrs, validateLabels(u.Subselector, idxPath.Child("subselector"))...)
 		}
 
-		allErrs = append(allErrs, validateServiceName(u.Service, idxPath.Child("service"))...)
+		allErrs = append(allErrs, validateVirtualServerServiceName(u.Service, idxPath.Child("service"))...)
 		allErrs = append(allErrs, validateTime(u.ProxyConnectTimeout, idxPath.Child("connect-timeout"))...)
 		allErrs = append(allErrs, validateTime(u.ProxyReadTimeout, idxPath.Child("read-timeout"))...)
 		allErrs = append(allErrs, validateTime(u.ProxySendTimeout, idxPath.Child("send-timeout"))...)
@@ -735,6 +735,30 @@ func validateServiceName(name string, fieldPath *field.Path) field.ErrorList {
 	return validateDNS1035Label(name, fieldPath)
 }
 
+// validateVirtualServerServiceName checks if a namespaced service name is valid for VirtualServer upstreams.
+func validateVirtualServerServiceName(name string, fieldPath *field.Path) field.ErrorList {
+	if strings.Contains(name, "/") {
+		parts := strings.Split(name, "/")
+		if len(parts) != 2 {
+			return field.ErrorList{field.Invalid(fieldPath, name, " service reference must be in the format namespace/service-name")}
+		}
+
+		namespaceErrs := validateDNS1123Label(parts[0], fieldPath)
+		if len(namespaceErrs) > 0 {
+			return field.ErrorList{field.Invalid(fieldPath, name, "invalid namespace in service reference")}
+		}
+
+		serviceErrs := validateServiceName(parts[1], fieldPath)
+		if len(serviceErrs) > 0 {
+			return field.ErrorList{field.Invalid(fieldPath, name, "invalid service name in service reference")}
+		}
+
+		return field.ErrorList{}
+	}
+
+	return validateServiceName(name, fieldPath)
+}
+
 func validateDNS1035Label(name string, fieldPath *field.Path) field.ErrorList {
 	if name == "" {
 		return field.ErrorList{field.Required(fieldPath, "")}
@@ -747,6 +771,18 @@ func validateDNS1035Label(name string, fieldPath *field.Path) field.ErrorList {
 	return allErrs
 }
 
+func validateDNS1123Label(name string, fieldPath *field.Path) field.ErrorList {
+	if name == "" {
+		return field.ErrorList{field.Required(fieldPath, "")}
+	}
+
+	allErrs := field.ErrorList{}
+	for _, msg := range validation.IsDNS1123Label(name) {
+		allErrs = append(allErrs, field.Invalid(fieldPath, name, msg))
+	}
+	return allErrs
+}
+
 func (vsv *VirtualServerValidator) validateVirtualServerRoutes(routes []v1.Route, fieldPath *field.Path, upstreamNames sets.Set[string], namespace string) field.ErrorList {
 	allErrs := field.ErrorList{}
 



Test patch:
diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go
index d3d0f6702..bf5f8d78d 100644
--- a/internal/configs/virtualserver_test.go
+++ b/internal/configs/virtualserver_test.go
@@ -100,6 +100,13 @@ func TestGenerateEndpointsKey(t *testing.T) {
 			subselector:      nil,
 			expected:         "default/backup-svc:8090",
 		},
+		{
+			serviceNamespace: "tea",
+			serviceName:      "tea-svc",
+			port:             8080,
+			subselector:      nil,
+			expected:         "tea/tea-svc:8080",
+		},
 	}
 
 	for _, test := range tests {
@@ -110,6 +117,38 @@ func TestGenerateEndpointsKey(t *testing.T) {
 	}
 }
 
+func TestParseServiceReference(t *testing.T) {
+	t.Parallel()
+
+	tests := []struct {
+		serviceRef       string
+		defaultNamespace string
+		expectedNS       string
+		expectedSvc      string
+	}{
+		{
+			serviceRef:       "coffee-svc",
+			defaultNamespace: "coffee",
+			expectedNS:       "coffee",
+			expectedSvc:      "coffee-svc",
+		},
+		{
+			serviceRef:       "tea/tea-svc",
+			defaultNamespace: "cafe",
+			expectedNS:       "tea",
+			expectedSvc:      "tea-svc",
+		},
+	}
+
+	for _, test := range tests {
+		namespace, serviceName := ParseServiceReference(test.serviceRef, test.defaultNamespace)
+		if namespace != test.expectedNS || serviceName != test.expectedSvc {
+			t.Errorf("parseServiceReference(%q, %q) returned (%q, %q) but expected (%q, %q)",
+				test.serviceRef, test.defaultNamespace, namespace, serviceName, test.expectedNS, test.expectedSvc)
+		}
+	}
+}
+
 func TestUpstreamNamerForVirtualServer(t *testing.T) {
 	t.Parallel()
 	virtualServer := conf_v1.VirtualServer{
@@ -16216,7 +16255,7 @@ func TestGenerateLocationForProxying(t *testing.T) {
 		VSRNamespace:             "",
 	}
 
-	result := generateLocationForProxying(path, upstreamName, conf_v1.Upstream{}, &cfgParams, nil, false, 0, "", nil, "", vsLocSnippets, false, "", "")
+	result := generateLocationForProxying(path, upstreamName, conf_v1.Upstream{}, &cfgParams, nil, false, 0, "", nil, "", vsLocSnippets, false, "", "", "")
 	if diff := cmp.Diff(expected, result); diff != "" {
 		t.Errorf("generateLocationForProxying() mismatch (-want +got):\n%s", diff)
 	}
@@ -16263,7 +16302,7 @@ func TestGenerateLocationForGrpcProxying(t *testing.T) {
 		GRPCPass:                 "grpc://test-upstream",
 	}
 
-	result := generateLocationForProxying(path, upstreamName, conf_v1.Upstream{Type: "grpc"}, &cfgParams, nil, false, 0, "", nil, "", vsLocSnippets, false, "", "")
+	result := generateLocationForProxying(path, upstreamName, conf_v1.Upstream{Type: "grpc"}, &cfgParams, nil, false, 0, "", nil, "", vsLocSnippets, false, "", "", "")
 	if diff := cmp.Diff(expected, result); diff != "" {
 		t.Errorf("generateLocationForForGrpcProxying() mismatch (-want +got):\n%s", diff)
 	}
@@ -22708,3 +22747,220 @@ func TestRFC1123ToSnake(t *testing.T) {
 		})
 	}
 }
+
+func TestGenerateVirtualServerConfigWithForeignNamespaceService(t *testing.T) {
+	t.Parallel()
+	virtualServerEx := VirtualServerEx{
+		VirtualServer: &conf_v1.VirtualServer{
+			ObjectMeta: meta_v1.ObjectMeta{
+				Name:      "cafe",
+				Namespace: "default",
+			},
+			Spec: conf_v1.VirtualServerSpec{
+				Host: "cafe.example.com",
+				Upstreams: []conf_v1.Upstream{
+					{
+						Name:    "coffee",
+						Service: "coffee/coffee-svc",
+						Port:    80,
+					},
+				},
+				Routes: []conf_v1.Route{
+					{
+						Path: "/coffee",
+						Action: &conf_v1.Action{
+							Pass: "coffee",
+						},
+					},
+				},
+			},
+		},
+		Endpoints: map[string][]string{
+			"coffee/coffee-svc:80": {
+				"10.0.0.20:80",
+			},
+		},
+		VirtualServerRoutes: []*conf_v1.VirtualServerRoute{},
+	}
+
+	vsc := newVirtualServerConfigurator(&baseCfgParams, false, false, &StaticConfigParams{}, false, nil)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil)
+	if len(warnings) != 0 {
+		t.Errorf("GenerateVirtualServerConfig returned warnings: %v", warnings)
+	}
+
+	expected := version2.VirtualServerConfig{
+		Upstreams: []version2.Upstream{
+			{
+				UpstreamLabels: version2.UpstreamLabels{
+					Service:           "coffee/coffee-svc",
+					ResourceType:      "virtualserver",
+					ResourceName:      "cafe",
+					ResourceNamespace: "default",
+				},
+				Name: "vs_default_cafe_coffee",
+				Servers: []version2.UpstreamServer{
+					{
+						Address: "10.0.0.20:80",
+					},
+				},
+				Keepalive: 16,
+			},
+		},
+		HTTPSnippets:  []string{},
+		LimitReqZones: []version2.LimitReqZone{},
+		Server: version2.Server{
+			ServerName:      "cafe.example.com",
+			StatusZone:      "cafe.example.com",
+			VSNamespace:     "default",
+			VSName:          "cafe",
+			ProxyProtocol:   true,
+			ServerTokens:    "off",
+			SetRealIPFrom:   []string{"0.0.0.0/0"},
+			RealIPHeader:    "X-Real-IP",
+			RealIPRecursive: true,
+			Snippets:        []string{"# server snippet"},
+			Locations: []version2.Location{
+				{
+					Path:                     "/coffee",
+					ProxyPass:                "http://vs_default_cafe_coffee",
+					ProxyNextUpstream:        "error timeout",
+					ProxyNextUpstreamTimeout: "0s",
+					ProxyPassRequestHeaders:  true,
+					ProxySetHeaders: []version2.Header{
+						{
+							Name:  "Host",
+							Value: "$host",
+						},
+					},
+					HasKeepalive: true,
+					ProxySSLName: "coffee-svc.coffee.svc",
+					ServiceName:  "coffee-svc",
+				},
+			},
+		},
+		SpiffeClientCerts: false,
+	}
+
+	if !cmp.Equal(expected, result) {
+		t.Error(cmp.Diff(expected, result))
+	}
+}
+
+func TestGenerateVirtualServerConfigWithForeignNamespaceServiceInVSR(t *testing.T) {
+	t.Parallel()
+	virtualServerEx := VirtualServerEx{
+		VirtualServer: &conf_v1.VirtualServer{
+			ObjectMeta: meta_v1.ObjectMeta{
+				Name:      "cafe",
+				Namespace: "default",
+			},
+			Spec: conf_v1.VirtualServerSpec{
+				Host: "cafe.example.com",
+				Routes: []conf_v1.Route{
+					{
+						Path:  "/tea",
+						Route: "default/tea",
+					},
+				},
+			},
+		},
+		Endpoints: map[string][]string{
+			"tea/tea-svc:80": {
+				"10.0.0.30:80",
+			},
+		},
+		VirtualServerRoutes: []*conf_v1.VirtualServerRoute{
+			{
+				ObjectMeta: meta_v1.ObjectMeta{
+					Name:      "tea",
+					Namespace: "default",
+				},
+				Spec: conf_v1.VirtualServerRouteSpec{
+					Host: "cafe.example.com",
+					Upstreams: []conf_v1.Upstream{
+						{
+							Name:    "tea",
+							Service: "tea/tea-svc",
+							Port:    80,
+						},
+					},
+					Subroutes: []conf_v1.Route{
+						{
+							Path: "/tea",
+							Action: &conf_v1.Action{
+								Pass: "tea",
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	vsc := newVirtualServerConfigurator(&baseCfgParams, false, false, &StaticConfigParams{}, false, nil)
+	result, warnings := vsc.GenerateVirtualServerConfig(&virtualServerEx, nil, nil)
+	if len(warnings) != 0 {
+		t.Errorf("GenerateVirtualServerConfig returned warnings: %v", warnings)
+	}
+
+	expected := version2.VirtualServerConfig{
+		Upstreams: []version2.Upstream{
+			{
+				UpstreamLabels: version2.UpstreamLabels{
+					Service:           "tea/tea-svc",
+					ResourceType:      "virtualserverroute",
+					ResourceName:      "tea",
+					ResourceNamespace: "default",
+				},
+				Name: "vs_default_cafe_vsr_default_tea_tea",
+				Servers: []version2.UpstreamServer{
+					{
+						Address: "10.0.0.30:80",
+					},
+				},
+				Keepalive: 16,
+			},
+		},
+		HTTPSnippets:  []string{},
+		LimitReqZones: []version2.LimitReqZone{},
+		Server: version2.Server{
+			ServerName:      "cafe.example.com",
+			StatusZone:      "cafe.example.com",
+			VSNamespace:     "default",
+			VSName:          "cafe",
+			ProxyProtocol:   true,
+			ServerTokens:    "off",
+			SetRealIPFrom:   []string{"0.0.0.0/0"},
+			RealIPHeader:    "X-Real-IP",
+			RealIPRecursive: true,
+			Snippets:        []string{"# server snippet"},
+			Locations: []version2.Location{
+				{
+					Path:                     "/tea",
+					ProxyPass:                "http://vs_default_cafe_vsr_default_tea_tea",
+					ProxyNextUpstream:        "error timeout",
+					ProxyNextUpstreamTimeout: "0s",
+					ProxyPassRequestHeaders:  true,
+					ProxySetHeaders: []version2.Header{
+						{
+							Name:  "Host",
+							Value: "$host",
+						},
+					},
+					HasKeepalive: true,
+					ProxySSLName: "tea-svc.tea.svc",
+					ServiceName:  "tea-svc",
+					IsVSR:        true,
+					VSRName:      "tea",
+					VSRNamespace: "default",
+				},
+			},
+		},
+		SpiffeClientCerts: false,
+	}
+
+	if !cmp.Equal(expected, result) {
+		t.Error(cmp.Diff(expected, result))
+	}
+}
diff --git a/pkg/apis/configuration/validation/virtualserver_test.go b/pkg/apis/configuration/validation/virtualserver_test.go
index 381ccad42..3a2c2edcd 100644
--- a/pkg/apis/configuration/validation/virtualserver_test.go
+++ b/pkg/apis/configuration/validation/virtualserver_test.go
@@ -190,6 +190,20 @@ func TestValidateBackup(t *testing.T) {
 	}
 }
 
+func TestValidateBackupRejectsCrossNamespace(t *testing.T) {
+	t.Parallel()
+
+	vs := makeVirtualServer()
+	vs.Spec.Upstreams[1].Backup = "external-ns/backup-service"
+	vs.Spec.Upstreams[1].BackupPort = createPointerFromUInt16(8080)
+
+	vsv := &VirtualServerValidator{isPlus: true, isDosEnabled: true}
+	err := vsv.ValidateVirtualServer(&vs)
+	if err == nil {
+		t.Error("ValidateVirtualServer() returned no error for cross-namespace backup service, expected error")
+	}
+}
+
 func TestValidateHost(t *testing.T) {
 	t.Parallel()
 	validHosts := []string{
@@ -901,6 +915,36 @@ func TestValidateDNS1035Label(t *testing.T) {
 	}
 }
 
+func TestValidateDNS1123Label(t *testing.T) {
+	t.Parallel()
+	validNames := []string{
+		"my-namespace",
+		"namespace-123",
+		"123namespace",
+	}
+
+	for _, name := range validNames {
+		allErrs := validateDNS1123Label(name, field.NewPath("namespace"))
+		if len(allErrs) != 0 {
+			t.Errorf("validateDNS1123Label(%v) returned errors %v for valid input", name, allErrs)
+		}
+	}
+
+	invalidNames := []string{
+		"",
+		"UPPERCASE",
+		"Test",
+		"very-long-label-name-that-exceeds-the-maximum-allowed-length-of-63-characters",
+	}
+
+	for _, name := range invalidNames {
+		allErrs := validateDNS1123Label(name, field.NewPath("namespace"))
+		if len(allErrs) == 0 {
+			t.Errorf("validateDNS1123Label(%v) returned no errors for invalid input", name)
+		}
+	}
+}
+
 func TestValidateVirtualServerRoutes(t *testing.T) {
 	t.Parallel()
 	tests := []struct {
@@ -4563,3 +4607,62 @@ func TestValidateErrorPageHeaderFails(t *testing.T) {
 		}
 	}
 }
+
+func TestValidateVirtualServerServiceName(t *testing.T) {
+	t.Parallel()
+
+	validTests := []string{
+		"coffee-svc",
+		"coffee/coffee-svc",
+	}
+
+	for _, test := range validTests {
+		allErrs := validateVirtualServerServiceName(test, field.NewPath("service"))
+		if len(allErrs) != 0 {
+			t.Errorf("validateVirtualServerServiceName(%v) returned errors %v for valid input", test, allErrs)
+		}
+	}
+
+	invalidTests := []string{
+		"",
+		"namespace/service/extra",
+		"namespace//service",
+		"/service",
+	}
+
+	for _, test := range invalidTests {
+		allErrs := validateVirtualServerServiceName(test, field.NewPath("service"))
+		if len(allErrs) == 0 {
+			t.Errorf("validateVirtualServerServiceName(%v) returned no errors for invalid input", test)
+		}
+	}
+}
+
+func TestValidateServiceName(t *testing.T) {
+	t.Parallel()
+
+	validTests := []string{
+		"my-service",
+		"service-name",
+		"service123",
+	}
+
+	for _, test := range validTests {
+		allErrs := validateServiceName(test, field.NewPath("service"))
+		if len(allErrs) != 0 {
+			t.Errorf("validateServiceName(%v) returned errors %v for valid input", test, allErrs)
+		}
+	}
+
+	invalidTests := []string{
+		"",
+		"123service",
+	}
+
+	for _, test := range invalidTests {
+		allErrs := validateServiceName(test, field.NewPath("service"))
+		if len(allErrs) == 0 {
+			t.Errorf("validateServiceName(%v) returned no errors for invalid input", test)
+		}
+	}
+}
diff --git a/tests/data/common/app/simple-namespaced-upstream/backend1.yaml b/tests/data/common/app/simple-namespaced-upstream/backend1.yaml
new file mode 100644
index 000000000..2c8a6ade7
--- /dev/null
+++ b/tests/data/common/app/simple-namespaced-upstream/backend1.yaml
@@ -0,0 +1,32 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend1
+spec:
+  replicas: 2
+  selector:
+    matchLabels:
+      app: backend1
+  template:
+    metadata:
+      labels:
+        app: backend1
+    spec:
+      containers:
+      - name: backend1
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: backend1-svc
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: backend1
diff --git a/tests/data/common/app/simple-namespaced-upstream/backend2.yaml b/tests/data/common/app/simple-namespaced-upstream/backend2.yaml
new file mode 100644
index 000000000..0319d4b48
--- /dev/null
+++ b/tests/data/common/app/simple-namespaced-upstream/backend2.yaml
@@ -0,0 +1,34 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+  name: backend2
+  namespace: backend2-namespace
+spec:
+  replicas: 1
+  selector:
+    matchLabels:
+      app: backend2
+  template:
+    metadata:
+      labels:
+        app: backend2
+    spec:
+      containers:
+      - name: backend2
+        image: nginxdemos/nginx-hello:plain-text
+        ports:
+        - containerPort: 8080
+---
+apiVersion: v1
+kind: Service
+metadata:
+  name: backend2-svc
+  namespace: backend2-namespace
+spec:
+  ports:
+  - port: 80
+    targetPort: 8080
+    protocol: TCP
+    name: http
+  selector:
+    app: backend2
diff --git a/tests/data/virtual-server-foreign-upstream/route-backend2.yaml b/tests/data/virtual-server-foreign-upstream/route-backend2.yaml
new file mode 100644
index 000000000..fa093f760
--- /dev/null
+++ b/tests/data/virtual-server-foreign-upstream/route-backend2.yaml
@@ -0,0 +1,14 @@
+apiVersion: k8s.nginx.org/v1
+kind: VirtualServerRoute
+metadata:
+  name: backend2
+spec:
+  host: virtual-server-vsr.example.com
+  upstreams:
+  - name: backend2
+    service: backend2-namespace/backend2-svc
+    port: 80
+  subroutes:
+  - path: "/backend2"
+    action:
+      pass: backend2
diff --git a/tests/data/virtual-server-foreign-upstream/standard/virtual-server-regex.yaml b/tests/data/virtual-server-foreign-upstream/standard/virtual-server-regex.yaml
new file mode 100644
index 000000000..c6731faf7
--- /dev/null
+++ b/tests/data/virtual-server-foreign-upstream/standard/virtual-server-regex.yaml
@@ -0,0 +1,20 @@
+apiVersion: k8s.nginx.org/v1
+kind: VirtualServer
+metadata:
+  name: virtual-server
+spec:
+  host: virtual-server-regex.example.com
+  upstreams:
+  - name: backend1
+    service: backend1-svc
+    port: 80
+  - name: backend2
+    service: backend2-namespace/backend2-svc
+    port: 80
+  routes:
+  - path: "~ ^/[a-z]+1"
+    action:
+      pass: backend1
+  - path: "~ ^/[a-z]+2"
+    action:
+      pass: backend2
diff --git a/tests/data/virtual-server-foreign-upstream/standard/virtual-server-vsr.yaml b/tests/data/virtual-server-foreign-upstream/standard/virtual-server-vsr.yaml
new file mode 100644
index 000000000..87baaa9f7
--- /dev/null
+++ b/tests/data/virtual-server-foreign-upstream/standard/virtual-server-vsr.yaml
@@ -0,0 +1,16 @@
+apiVersion: k8s.nginx.org/v1
+kind: VirtualServer
+metadata:
+  name: virtual-server
+spec:
+  host: virtual-server-vsr.example.com
+  upstreams:
+  - name: backend1
+    service: backend1-svc
+    port: 80
+  routes:
+  - path: "/backend1"
+    action:
+      pass: backend1
+  - path: "/backend2"
+    route: backend2
diff --git a/tests/data/virtual-server-foreign-upstream/standard/virtual-server.yaml b/tests/data/virtual-server-foreign-upstream/standard/virtual-server.yaml
new file mode 100644
index 000000000..48228bafc
--- /dev/null
+++ b/tests/data/virtual-server-foreign-upstream/standard/virtual-server.yaml
@@ -0,0 +1,20 @@
+apiVersion: k8s.nginx.org/v1
+kind: VirtualServer
+metadata:
+  name: virtual-server
+spec:
+  host: virtual-server.example.com
+  upstreams:
+  - name: backend1
+    service: backend1-svc
+    port: 80
+  - name: backend2
+    service: backend2-namespace/backend2-svc
+    port: 80
+  routes:
+  - path: "/backend1"
+    action:
+      pass: backend1
+  - path: "/backend2"
+    action:
+      pass: backend2
diff --git a/tests/suite/test_virtual_server_foreign_upstream.py b/tests/suite/test_virtual_server_foreign_upstream.py
new file mode 100644
index 000000000..11ac55eca
--- /dev/null
+++ b/tests/suite/test_virtual_server_foreign_upstream.py
@@ -0,0 +1,236 @@
+import pytest
+from settings import TEST_DATA
+from suite.fixtures.custom_resource_fixtures import VirtualServerSetup
+from suite.utils.custom_assertions import wait_and_assert_status_code
+from suite.utils.resources_utils import (
+    create_items_from_yaml,
+    create_namespace_with_name_from_yaml,
+    delete_items_from_yaml,
+    delete_namespace,
+    wait_before_test,
+    wait_until_all_pods_are_ready,
+)
+from suite.utils.vs_vsr_resources_utils import (
+    create_v_s_route_from_yaml,
+    create_virtual_server_from_yaml,
+    delete_v_s_route,
+    delete_virtual_server,
+    patch_virtual_server_from_yaml,
+)
+from suite.utils.yaml_utils import (
+    get_first_host_from_yaml,
+    get_paths_from_vs_yaml,
+    get_upstream_namespace_from_vs_yaml,
+)
+
+
+@pytest.fixture(scope="class")
+def virtual_server_foreign_upstream_app_setup(
+    request, kube_apis, ingress_controller_endpoint, test_namespace
+) -> VirtualServerSetup:
+    """
+    Prepare Virtual Server Example with backends in foreign namespaces:
+
+    1st namespace with backend1-svc and deployment in the same namespace as VS,
+    and 2nd namespace with backend2-svc and deployment in another namespace.
+
+    :param request: internal pytest fixture to parametrize this method:
+        {example: virtual-server|virtual-server-tls|..., app_type: simple|split|...}
+        'example' is a directory name in TEST_DATA,
+        'app_type' is a directory name in TEST_DATA/common/app
+     :param kube_apis: client apis
+    :param crd_ingress_controller:
+    :param ingress_controller_endpoint:
+    :param test_namespace:
+    :return: VirtualServerSetup
+    """
+    print("------------------------- Deploy Virtual Server Example -----------------------------------")
+    vs_source = f"{TEST_DATA}/{request.param['example']}/standard/virtual-server.yaml"
+    vs_name = create_virtual_server_from_yaml(kube_apis.custom_objects, vs_source, test_namespace)
+    vs_host = get_first_host_from_yaml(vs_source)
+    vs_paths = get_paths_from_vs_yaml(vs_source)
+    upstream_namespaces = get_upstream_namespace_from_vs_yaml(vs_source, test_namespace)
+    print(f"Upstream namespaces detected in the VS yaml: {upstream_namespaces}")
+    ns_1 = (
+        create_namespace_with_name_from_yaml(kube_apis.v1, upstream_namespaces[0], f"{TEST_DATA}/common/ns.yaml")
+        if upstream_namespaces[0] != test_namespace
+        else test_namespace
+    )
+    ns_2 = (
+        create_namespace_with_name_from_yaml(kube_apis.v1, upstream_namespaces[1], f"{TEST_DATA}/common/ns.yaml")
+        if upstream_namespaces[1] != test_namespace
+        else test_namespace
+    )
+    create_items_from_yaml(kube_apis, f"{TEST_DATA}/common/app/{request.param['app_type']}/backend1.yaml", ns_1)
+    create_items_from_yaml(kube_apis, f"{TEST_DATA}/common/app/{request.param['app_type']}/backend2.yaml", ns_2)
+
+    wait_until_all_pods_are_ready(kube_apis.v1, ns_1)
+    wait_until_all_pods_are_ready(kube_apis.v1, ns_2)
+
+    def fin():
+        if request.config.getoption("--skip-fixture-teardown") == "no":
+            print("Clean up Virtual Server Example:")
+            delete_virtual_server(kube_apis.custom_objects, vs_name, test_namespace)
+            print("Clean up the Application:")
+            if request.param.get("app_type"):
+                delete_items_from_yaml(
+                    kube_apis, f"{TEST_DATA}/common/app/{request.param["app_type"]}/backend1.yaml", ns_1
+                )
+                delete_items_from_yaml(
+                    kube_apis, f"{TEST_DATA}/common/app/{request.param["app_type"]}/backend2.yaml", ns_2
+                )
+
+                try:
+                    delete_namespace(kube_apis.v1, ns_1)
+                    delete_namespace(kube_apis.v1, ns_2)
+
+                except Exception as ex:
+                    print(f"Exception during teardown: {ex}")
+
+    request.addfinalizer(fin)
+
+    return VirtualServerSetup(ingress_controller_endpoint, test_namespace, vs_host, vs_name, vs_paths)
+
+
+@pytest.mark.vs
+@pytest.mark.vs_responses
+@pytest.mark.smoke
+@pytest.mark.parametrize(
+    "crd_ingress_controller, virtual_server_foreign_upstream_app_setup",
+    [
+        (
+            {"type": "complete", "extra_args": [f"-enable-custom-resources"]},
+            {"example": "virtual-server-foreign-upstream", "app_type": "simple-namespaced-upstream"},
+        ),
+    ],
+    indirect=True,
+)
+class TestVirtualServerForeignUpstream:
+    def test_responses_after_setup(self, kube_apis, crd_ingress_controller, virtual_server_foreign_upstream_app_setup):
+        print(f"\nStep 1: initial check")
+        wait_before_test()
+        wait_and_assert_status_code(
+            200,
+            virtual_server_foreign_upstream_app_setup.backend_1_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+        wait_and_assert_status_code(
+            200,
+            virtual_server_foreign_upstream_app_setup.backend_2_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+
+    def test_responses_regex_path(self, kube_apis, crd_ingress_controller, virtual_server_foreign_upstream_app_setup):
+        print(f"\nStep 2: patch VS with regex path and check")
+        vs_source = f"{TEST_DATA}/virtual-server-foreign-upstream/standard/virtual-server-regex.yaml"
+        patch_virtual_server_from_yaml(
+            kube_apis.custom_objects,
+            virtual_server_foreign_upstream_app_setup.vs_name,
+            vs_source,
+            virtual_server_foreign_upstream_app_setup.namespace,
+        )
+
+        new_host = get_first_host_from_yaml(
+            f"{TEST_DATA}/virtual-server-foreign-upstream/standard/virtual-server-regex.yaml"
+        )
+
+        wait_before_test()
+        wait_and_assert_status_code(
+            404,
+            virtual_server_foreign_upstream_app_setup.backend_1_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+        wait_and_assert_status_code(
+            404,
+            virtual_server_foreign_upstream_app_setup.backend_2_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+
+        wait_and_assert_status_code(200, virtual_server_foreign_upstream_app_setup.backend_1_url, new_host)
+        wait_and_assert_status_code(200, virtual_server_foreign_upstream_app_setup.backend_2_url, new_host)
+
+        print("\nStep 3: restore VS and check")
+        patch_virtual_server_from_yaml(
+            kube_apis.custom_objects,
+            virtual_server_foreign_upstream_app_setup.vs_name,
+            f"{TEST_DATA}/virtual-server-foreign-upstream/standard/virtual-server.yaml",
+            virtual_server_foreign_upstream_app_setup.namespace,
+        )
+        wait_before_test()
+
+        wait_and_assert_status_code(404, virtual_server_foreign_upstream_app_setup.backend_1_url, new_host)
+        wait_and_assert_status_code(404, virtual_server_foreign_upstream_app_setup.backend_2_url, new_host)
+
+        wait_and_assert_status_code(
+            200,
+            virtual_server_foreign_upstream_app_setup.backend_1_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+        wait_and_assert_status_code(
+            200,
+            virtual_server_foreign_upstream_app_setup.backend_2_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+
+    def test_responses_vsr_foreign_upstream(
+        self, kube_apis, crd_ingress_controller, virtual_server_foreign_upstream_app_setup
+    ):
+        print(f"\nStep 4: create VS Route in the same namespace and check")
+        vs_source = f"{TEST_DATA}/virtual-server-foreign-upstream/standard/virtual-server-vsr.yaml"
+        patch_virtual_server_from_yaml(
+            kube_apis.custom_objects,
+            virtual_server_foreign_upstream_app_setup.vs_name,
+            vs_source,
+            virtual_server_foreign_upstream_app_setup.namespace,
+        )
+
+        vs_route = create_v_s_route_from_yaml(
+            kube_apis.custom_objects,
+            f"{TEST_DATA}/virtual-server-foreign-upstream/route-backend2.yaml",
+            virtual_server_foreign_upstream_app_setup.namespace,
+        )
+
+        new_host = get_first_host_from_yaml(
+            f"{TEST_DATA}/virtual-server-foreign-upstream/standard/virtual-server-vsr.yaml"
+        )
+
+        wait_before_test()
+
+        wait_and_assert_status_code(
+            404,
+            virtual_server_foreign_upstream_app_setup.backend_1_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+        wait_and_assert_status_code(
+            404,
+            virtual_server_foreign_upstream_app_setup.backend_2_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+
+        wait_and_assert_status_code(200, virtual_server_foreign_upstream_app_setup.backend_1_url, new_host)
+        wait_and_assert_status_code(200, virtual_server_foreign_upstream_app_setup.backend_2_url, new_host)
+
+        print("\nStep 5: remove VSR, restore VS and check")
+        delete_v_s_route(kube_apis.custom_objects, vs_route, virtual_server_foreign_upstream_app_setup.namespace)
+
+        patch_virtual_server_from_yaml(
+            kube_apis.custom_objects,
+            virtual_server_foreign_upstream_app_setup.vs_name,
+            f"{TEST_DATA}/virtual-server-foreign-upstream/standard/virtual-server.yaml",
+            virtual_server_foreign_upstream_app_setup.namespace,
+        )
+        wait_before_test()
+
+        wait_and_assert_status_code(404, virtual_server_foreign_upstream_app_setup.backend_1_url, new_host)
+        wait_and_assert_status_code(404, virtual_server_foreign_upstream_app_setup.backend_2_url, new_host)
+
+        wait_and_assert_status_code(
+            200,
+            virtual_server_foreign_upstream_app_setup.backend_1_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
+        wait_and_assert_status_code(
+            200,
+            virtual_server_foreign_upstream_app_setup.backend_2_url,
+            virtual_server_foreign_upstream_app_setup.vs_host,
+        )
diff --git a/tests/suite/utils/yaml_utils.py b/tests/suite/utils/yaml_utils.py
index 7c7858df0..65e03379e 100644
--- a/tests/suite/utils/yaml_utils.py
+++ b/tests/suite/utils/yaml_utils.py
@@ -107,6 +107,28 @@ def get_route_namespace_from_vs_yaml(file) -> []:
     return res
 
 
+def get_upstream_namespace_from_vs_yaml(file, default_namespace) -> []:
+    """
+    Parse yaml file and return namespaces of all spec.upstreams.service that contain namespace references.
+
+    :param file: an absolute path to file
+    :return: []
+    """
+    res = []
+    with open(file) as f:
+        docs = yaml.safe_load_all(f)
+        for dep in docs:
+            if "upstreams" in dep["spec"]:
+                for upstream in dep["spec"]["upstreams"]:
+                    service = upstream["service"]
+                    if "/" in service:  # namespace/service format
+                        namespace = service.split("/")[0]
+                    else:
+                        namespace = default_namespace
+                    res.append(namespace)
+    return res
+
+
 def get_paths_from_vsr_yaml(file) -> []:
     """
     Parse yaml file and return all the found spec.subroutes.path.