diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index e495495a2cefb7192270fdf7f198febb5052a974..f1bb5edefdfd44fef170fb39c8e324d9a50c581b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,11 +1,10 @@
-stages:
-  - test
-  - build
-
 include:
   - project: 'paas-tools/infrastructure-ci'
     file: 'docker-images-ci-templates/DockerImages.gitlab-ci.yml'
 
+stages:
+  - test
+  - build
 
 Test Operator:
   stage: test
diff --git a/controllers/controller_test.go b/controllers/controller_test.go
index 5838158bc2563da05250470ff62b08a0df1cf271..734f82baecf7c17387d12179851945bf827ed5cd 100644
--- a/controllers/controller_test.go
+++ b/controllers/controller_test.go
@@ -299,6 +299,7 @@ var _ = Describe("GitlabPagesSite controller", func() {
 				}
 			}
 		})
+
 		It("GPS sites in blocked projects are unavailable", func() {
 			gpsSite := newGpsSite(gpsName, gpsNamespace)
 			gpsSite.Spec.Hosts = webservicescernchv1alpha1.SiteUrls{"my-gps-site.web.cern.ch"}
@@ -336,9 +337,107 @@ var _ = Describe("GitlabPagesSite controller", func() {
 			Expect(len(routes.Items)).To(Equal(1))
 
 			for _, route := range routes.Items {
-				Expect(route.Spec.Host).To(Equal("my-gps-site.web.cern.ch"))
 				Expect(route.Spec.To.Name).To(Equal(blockedServiceName))
 			}
 		})
+
+		It("GPS's Route is assigned to the default router shard", func() {
+			gpsSite := newGpsSite(gpsName, gpsNamespace)
+			gpsSite.Spec.Hosts = webservicescernchv1alpha1.SiteUrls{"my-gps-site.web.cern.ch"}
+
+			// create a GPS site as usual
+			Expect(k8sClient.Create(ctx, gpsSite)).Should(Succeed())
+			Eventually(func() bool {
+				err := k8sClient.Get(ctx, gpsLookupKey, gpsSite)
+				if err != nil {
+					return false
+				}
+
+				return len(gpsSite.Status.Conditions) == 1 &&
+					gpsSite.Status.Conditions[0].Type == webservicescernchv1alpha1.ConditionTypeGitlabPagesSiteCreated &&
+					gpsSite.Status.Conditions[0].Status == metav1.ConditionTrue
+			}, timeout, interval).Should(BeTrue())
+
+			route := &routev1.Route{}
+			routeName := generateRouteName(gpsSite, "my-gps-site.web.cern.ch")
+
+			Expect(k8sClient.Get(ctx, types.NamespacedName{Name: routeName, Namespace: operatorNamespace}, route)).Should(Succeed())
+			Expect(route.Labels[routerShardLabel]).To(Equal(defaultRouterShard))
+		})
+
+		It("GPS's Route is assigned to a custom router shard if configured properly", func() {
+			gpsSite := newGpsSite(gpsName, gpsNamespace)
+			gpsSite.Spec.Hosts = webservicescernchv1alpha1.SiteUrls{"my-gps-site.web.cern.ch"}
+
+			gpsNs := &corev1.Namespace{}
+			nsKey := types.NamespacedName{Name: gpsNamespace, Namespace: gpsNamespace}
+
+			Expect(k8sClient.Get(ctx, nsKey, gpsNs)).Should(Succeed())
+
+			if gpsNs.Annotations == nil {
+				gpsNs.Annotations = map[string]string{}
+			}
+
+			gpsNs.Annotations[forceRouterShardNamespaceAnnotation] = "example-router-shard"
+
+			Expect(k8sClient.Update(ctx, gpsNs)).Should(Succeed())
+
+			// create a GPS site as usual
+			Expect(k8sClient.Create(ctx, gpsSite)).Should(Succeed())
+			Eventually(func() bool {
+				err := k8sClient.Get(ctx, gpsLookupKey, gpsSite)
+				if err != nil {
+					return false
+				}
+
+				return len(gpsSite.Status.Conditions) == 1 &&
+					gpsSite.Status.Conditions[0].Type == webservicescernchv1alpha1.ConditionTypeGitlabPagesSiteCreated &&
+					gpsSite.Status.Conditions[0].Status == metav1.ConditionTrue
+			}, timeout, interval).Should(BeTrue())
+
+			// confirm that the proper shard-related labels/annotations have been applied on the Route
+			route := &routev1.Route{}
+			routeName := generateRouteName(gpsSite, "my-gps-site.web.cern.ch")
+
+			Expect(k8sClient.Get(ctx, types.NamespacedName{Name: routeName, Namespace: operatorNamespace}, route)).Should(Succeed())
+			Expect(route.Labels[routerShardLabel]).To(Equal("example-router-shard"))
+		})
+
+		It("Namespace's IP whitelist is propagated to the Route", func() {
+			gpsSite := newGpsSite(gpsName, gpsNamespace)
+			gpsSite.Spec.Hosts = webservicescernchv1alpha1.SiteUrls{"my-gps-site.web.cern.ch"}
+
+			gpsNs := &corev1.Namespace{}
+			nsKey := types.NamespacedName{Name: gpsNamespace, Namespace: gpsNamespace}
+
+			Expect(k8sClient.Get(ctx, nsKey, gpsNs)).Should(Succeed())
+
+			if gpsNs.Annotations == nil {
+				gpsNs.Annotations = map[string]string{}
+			}
+
+			gpsNs.Annotations[forceIpWhitelistNamespaceAnnotation] = "example-ip-whitelist"
+
+			Expect(k8sClient.Update(ctx, gpsNs)).Should(Succeed())
+
+			// create a GPS site as usual
+			Expect(k8sClient.Create(ctx, gpsSite)).Should(Succeed())
+			Eventually(func() bool {
+				err := k8sClient.Get(ctx, gpsLookupKey, gpsSite)
+				if err != nil {
+					return false
+				}
+
+				return len(gpsSite.Status.Conditions) == 1 &&
+					gpsSite.Status.Conditions[0].Type == webservicescernchv1alpha1.ConditionTypeGitlabPagesSiteCreated &&
+					gpsSite.Status.Conditions[0].Status == metav1.ConditionTrue
+			}, timeout, interval).Should(BeTrue())
+
+			route := &routev1.Route{}
+			routeName := generateRouteName(gpsSite, "my-gps-site.web.cern.ch")
+
+			Expect(k8sClient.Get(ctx, types.NamespacedName{Name: routeName, Namespace: operatorNamespace}, route)).Should(Succeed())
+			Expect(route.Annotations["haproxy.router.openshift.io/ip_whitelist"]).To(Equal("example-ip-whitelist"))
+		})
 	})
 })
diff --git a/controllers/gitlabpagessite_controller.go b/controllers/gitlabpagessite_controller.go
index 91cc967745b7055516cfd4c30012ec587fd25fad..c9a5c88ff10970f72ca29a8d2a77e7a640cb44f4 100644
--- a/controllers/gitlabpagessite_controller.go
+++ b/controllers/gitlabpagessite_controller.go
@@ -47,6 +47,11 @@ const gitlabPagesSiteRouteOwnershipLabel = "staticsites.webservices.cern.ch/stat
 const gitlabPagesSiteRouteOwnershipAnnotation = "staticsites.webservices.cern.ch/static-site-owner"
 const userNamespaceLabel = "okd.cern.ch/user-project"
 
+// namespace annotations to override certain behaviors on sites provisioned from resources in that namespace
+// (annotations shared with webeos-site-operator)
+const forceRouterShardNamespaceAnnotation = "webeos.webservices.cern.ch/force-router-shard" // assign routes to a specific ingress controller
+const forceIpWhitelistNamespaceAnnotation = "webeos.webservices.cern.ch/force-ip-whitelist" // set an IP whitelist on routes (e.g. to block access from Internet)
+
 // GitlabPagesSiteReconciler reconciles a GitlabPagesSite object
 type GitlabPagesSiteReconciler struct {
 	client.Client
diff --git a/controllers/operator_methods.go b/controllers/operator_methods.go
index 45f0ffd475bf2a43a3a6a8eb5156c1f9cda89d6d..75f1678598185c66049ba859e955c01f75414eed 100644
--- a/controllers/operator_methods.go
+++ b/controllers/operator_methods.go
@@ -100,6 +100,14 @@ func (r *GitlabPagesSiteReconciler) ensureRoutes(ctx context.Context, gitlabPage
 	if err := r.Get(ctx, types.NamespacedName{Namespace: gitlabPagesSite.Namespace, Name: gitlabPagesSite.Namespace}, namespace); err != nil {
 		return err
 	}
+	// the router shard to use for the routes associated with this GitlabPagesSite
+	// the default can be overriden with a namespace annotation (same annotation as for webeos site)
+	routerShard := namespace.Annotations[forceRouterShardNamespaceAnnotation]
+	if routerShard == "" {
+		routerShard = r.DefaultRouterShard
+	}
+	// evaluates to an empty string (= allow any IP) if no such annotation on namespace
+	routeIpWhitelist := namespace.Annotations[forceIpWhitelistNamespaceAnnotation]
 
 	for _, host := range hosts {
 		route := &routev1.Route{
@@ -122,12 +130,11 @@ func (r *GitlabPagesSiteReconciler) ensureRoutes(ctx context.Context, gitlabPage
 			route.Annotations[gitlabPagesSiteRouteOwnershipAnnotation] = types.NamespacedName{Name: gitlabPagesSite.Name, Namespace: gitlabPagesSite.Namespace}.String()
 			route.Labels[gitlabPagesSiteRouteOwnershipLabel] = generateOwnershipLabelValue(gitlabPagesSite)
 
-			if r.RouterShardLabel != "" && r.DefaultRouterShard != "" {
-				// assign the Route to a specific router shard
-				if _, ok := route.Labels[r.RouterShardLabel]; !ok {
-					route.Labels[r.RouterShardLabel] = r.DefaultRouterShard
-				}
+			if r.RouterShardLabel != "" && routerShard != "" {
+				// assign the Route to the desired router shard
+				route.Labels[r.RouterShardLabel] = routerShard
 			}
+			route.Annotations["haproxy.router.openshift.io/ip_whitelist"] = routeIpWhitelist
 
 			// if the host doesn't match the shared domain regex, we will make sure a certificate is automatically provisioned
 			if r.SharedSubdomainRegex != nil && !r.SharedSubdomainRegex.MatchString(string(host)) {
@@ -165,7 +172,7 @@ func (r *GitlabPagesSiteReconciler) ensureRoutes(ctx context.Context, gitlabPage
 				TargetPort: intstr.FromInt(r.AuthProxyServicePort),
 			}
 			// If the project is blocked, we redirect the traffic to a non-existing service endpoint
-			// In this case, HAProxy routers will return a "503 Application not avaiable" message
+			// In this case, HAProxy routers will return a "503 Application not available" message
 			if projectBlocked(*namespace) {
 				route.Spec.To.Name = blockedServiceName
 				route.Spec.Port.TargetPort = intstr.FromInt(blockedServicePort)