diff --git a/software/erp5/instance-erp5-input-schema.json b/software/erp5/instance-erp5-input-schema.json
index 793be2c09efc1e88ab255c424012105aa4a71f93..6b71f8cd69a51c5c738e3fbd9c15fbef894523fa 100644
--- a/software/erp5/instance-erp5-input-schema.json
+++ b/software/erp5/instance-erp5-input-schema.json
@@ -5,7 +5,7 @@
   "additionalProperties": false,
   "definitions": {
     "routing-rule-list": {
-      "description": "Maps the path received in requests to given zope path. Rules are applied in the order they are given. This requires the path received from the outside world (typically: frontend) to have its root correspond to Zope's root (for frontend: 'path' parameter must be empty), with the customary VirtualHostMonster construct (for frontend: 'type' must be 'zope').",
+      "description": "Maps the path received in requests to given zope path. Rules are applied in the order they are given, after 'internal-path' from 'frontend' parameter. This also supports legacy frontends, using Rapid CDN with \"zope\" type.",
       "type": "array",
       "default": [
         [
diff --git a/software/erp5/test/test/__init__.py b/software/erp5/test/test/__init__.py
index f3ec3d3138c8cd147690956f22afc6b442099332..d8b54c467b531b710515b0a7143bea37adb273fd 100644
--- a/software/erp5/test/test/__init__.py
+++ b/software/erp5/test/test/__init__.py
@@ -314,7 +314,7 @@ class CaucaseCertificate(ManagedResource):
     )
     return os.path.join(software_release_root_path, 'bin', 'caucase')
 
-  def request(self, common_name: str, caucase: CaucaseService) -> None:
+  def request(self, common_name: str, caucase: CaucaseService, san: x509.SubjectAlternativeName=None) -> None:
     """Generate certificate and request signature to the caucase service.
 
     This overwrite any previously requested certificate for this instance.
@@ -345,11 +345,10 @@ class CaucaseCertificate(ManagedResource):
                 NameOID.COMMON_NAME,
                 common_name,
             ),
-        ])).sign(
-            key,
-            hashes.SHA256(),
-            default_backend(),
-        )
+        ]))
+    if san:
+      csr = csr.add_extension(san, critical=True)
+    csr = csr.sign(key, hashes.SHA256(), default_backend())
     with open(self.csr_file, 'wb') as f:
       f.write(csr.public_bytes(serialization.Encoding.PEM))
 
diff --git a/software/erp5/test/test/test_balancer.py b/software/erp5/test/test/test_balancer.py
index 62f3c1ef0254e1b28eb3cfbdb5a5e50c0c451ecc..715711ee41d00e940c875313df82915ff48ed8d3 100644
--- a/software/erp5/test/test/test_balancer.py
+++ b/software/erp5/test/test/test_balancer.py
@@ -113,6 +113,12 @@ class BalancerTestCase(ERP5InstanceTestCase):
         'ssl-authentication-dict': {'default': False},
         'ssl': {},
         'timeout-dict': {'default': None},
+        'frontend-parameter-dict': {
+          'default': {
+            'internal-path': '',
+            'zope-family': 'default',
+          },
+        },
         'family-path-routing-dict': {},
         'path-routing-list': [],
       }
@@ -122,24 +128,72 @@ class BalancerTestCase(ERP5InstanceTestCase):
     return {'_': json.dumps(cls._getInstanceParameterDict())}
 
   def setUp(self) -> None:
-    self.default_balancer_url = json.loads(
+    self.default_balancer_direct_url = json.loads(
         self.computer_partition.getConnectionParameterDict()['_'])['default']
+    self.default_balancer_zope_url = json.loads(
+        self.computer_partition.getConnectionParameterDict()['_'])['url-backend-default']
+
+
+class TestURLRewrite(BalancerTestCase):
+  __partition_reference__ = 'ur'
+  def test_direct(self):
+    self.assertEqual(requests.get(self.default_balancer_direct_url, verify=False).json()['Path'], '/')
+    self.assertEqual(
+      requests.get(
+        urllib.parse.urljoin(
+          self.default_balancer_direct_url,
+          '/VirtualHostBase/https/example.com:443/VirtualHostRoot/path'),
+        verify=False
+      ).json()['Path'],
+      '/VirtualHostBase/https/example.com:443/VirtualHostRoot/path')
+
+  def test_zope(self):
+    netloc = urllib.parse.urlparse(self.default_balancer_zope_url).netloc
+    self.assertEqual(
+      requests.get(self.default_balancer_zope_url, verify=False).json()['Path'],
+      f'/VirtualHostBase/https/{netloc}/VirtualHostRoot/')
+    self.assertEqual(
+      requests.get(urllib.parse.urljoin(
+        self.default_balancer_zope_url, 'path'), verify=False).json()['Path'],
+      f'/VirtualHostBase/https/{netloc}/VirtualHostRoot/path')
+    self.assertEqual(
+      requests.get(
+        urllib.parse.urljoin(
+          self.default_balancer_zope_url,
+          '/VirtualHostBase/https/example.com:443/VirtualHostRoot/path'),
+        verify=False
+      ).json()['Path'],
+      f'/VirtualHostBase/https/{netloc}/VirtualHostRoot/VirtualHostBase/https/example.com:443/VirtualHostRoot/path')
+
+  def test_bad_host(self):
+    self.assertEqual(
+      requests.get(self.default_balancer_zope_url, headers={'Host': 'a/b'}, verify=False).status_code,
+      requests.codes.bad_request)
 
 
 class SlowHTTPServer(ManagedHTTPServer):
   """An HTTP Server which reply after a timeout.
 
-  Timeout is 2 seconds by default, and can be specified in the path of the URL
+  Timeout is 2 seconds by default, and can be specified in the path of the URL:
+
+    GET /{timeout}
+
+  but because balancer rewrites the URL, the actual URL used by this server is:
+
+    GET /VirtualHostBase/https/{host}/VirtualHostRoot/{timeout}
+
   """
   class RequestHandler(BaseHTTPRequestHandler):
     def do_GET(self) -> None:
-      self.send_response(200)
-      self.send_header("Content-Type", "text/plain")
       timeout = 2
+      if self.path == '/':  # for health checks
+        timeout = 0
       try:
-        timeout = int(self.path[1:])
-      except ValueError:
+        timeout = int(self.path.split('/')[5])
+      except (ValueError, IndexError):
         pass
+      self.send_response(200)
+      self.send_header("Content-Type", "text/plain")
       time.sleep(timeout)
       self.end_headers()
       self.wfile.write(b"OK\n")
@@ -161,12 +215,12 @@ class TestTimeout(BalancerTestCase, CrontabMixin):
   def test_timeout(self) -> None:
     self.assertEqual(
       requests.get(
-          urllib.parse.urljoin(self.default_balancer_url, '/1'),
+          urllib.parse.urljoin(self.default_balancer_zope_url, '/1'),
           verify=False).status_code,
       requests.codes.ok)
     self.assertEqual(
       requests.get(
-          urllib.parse.urljoin(self.default_balancer_url, '/5'),
+          urllib.parse.urljoin(self.default_balancer_zope_url, '/5'),
           verify=False).status_code,
       requests.codes.gateway_timeout)
 
@@ -178,13 +232,13 @@ class TestLog(BalancerTestCase, CrontabMixin):
   @classmethod
   def _getInstanceParameterDict(cls) -> dict:
     parameter_dict = super()._getInstanceParameterDict()
-    # use a slow server instead
+    # use a slow server instead, so that we can test logs with slow requests
     parameter_dict['dummy_http_server'] = [[cls.getManagedResource("slow_web_server", SlowHTTPServer).netloc, 1, False]]
     return parameter_dict
 
   def test_access_log_format(self) -> None:
     requests.get(
-        urllib.parse.urljoin(self.default_balancer_url, '/url_path'),
+        urllib.parse.urljoin(self.default_balancer_zope_url, '/url_path'),
         verify=False,
     )
     time.sleep(.5) # wait a bit more until access is logged
@@ -197,7 +251,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
     # the request - but our test machines can be slow sometimes, so we tolerate
     # it can take up to 20 seconds.
     match = re.match(
-        r'([(\d\.)]+) - - \[(.*?)\] "(.*?)" (\d+) (\d+) "(.*?)" "(.*?)" (\d+)',
+        r'([(\da-fA-F:\.)]+) - - \[(.*?)\] "(.*?)" (\d+) (\d+) "(.*?)" "(.*?)" (\d+)',
         access_line
     )
     self.assertTrue(match)
@@ -208,7 +262,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
 
   def test_access_log_apachedex_report(self) -> None:
     # make a request so that we have something in the logs
-    requests.get(self.default_balancer_url, verify=False)
+    requests.get(self.default_balancer_zope_url, verify=False)
 
     # crontab for apachedex is executed
     self._executeCrontabAtDate('generate-apachedex-report', '23:59')
@@ -233,7 +287,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
     self._executeCrontabAtDate('logrotate', '2000-01-01')
 
     # make a request so that we have something in the logs
-    requests.get(self.default_balancer_url, verify=False).raise_for_status()
+    requests.get(self.default_balancer_zope_url, verify=False).raise_for_status()
 
     # slow query crontab depends on crontab for log rotation
     # to be executed first.
@@ -248,7 +302,7 @@ class TestLog(BalancerTestCase, CrontabMixin):
     )
     self.assertTrue(os.path.exists(rotated_log_file))
 
-    requests.get(self.default_balancer_url, verify=False).raise_for_status()
+    requests.get(self.default_balancer_zope_url, verify=False).raise_for_status()
     # on next day execution of logrotate, log files are compressed
     self._executeCrontabAtDate('logrotate', '2050-01-02')
     self.assertTrue(os.path.exists(rotated_log_file + '.xz'))
@@ -262,11 +316,11 @@ class TestLog(BalancerTestCase, CrontabMixin):
     # after a while, balancer should detect and log this event in error log
     time.sleep(5)
     self.assertEqual(
-        requests.get(self.default_balancer_url, verify=False).status_code,
+        requests.get(self.default_balancer_zope_url, verify=False).status_code,
         requests.codes.service_unavailable)
     with open(os.path.join(self.computer_partition_root_path, 'var', 'log', 'apache-error.log')) as error_log_file:
       error_line = error_log_file.read().splitlines()[-1]
-    self.assertIn('proxy family_default has no server available!', error_line)
+    self.assertIn('backend default has no server available!', error_line)
     # this log also include a timestamp
     self.assertRegex(error_line, r'\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}')
 
@@ -274,7 +328,9 @@ class TestLog(BalancerTestCase, CrontabMixin):
 class BalancerCookieHTTPServer(ManagedHTTPServer):
   """An HTTP Server which can set balancer cookie.
 
-  This server set cookie when requested /set-cookie path.
+  This server set cookie when requested /set-cookie path (actually
+  /VirtualHostBase/https/{host}/VirtualHostRoot/set-cookie , which is
+  added by balancer proxy)
 
   The reply body is the name used when registering this resource
   using getManagedResource. This way we can assert which
@@ -288,7 +344,8 @@ class BalancerCookieHTTPServer(ManagedHTTPServer):
       def do_GET(self) -> None:
         self.send_response(200)
         self.send_header("Content-Type", "text/plain")
-        if self.path == '/set_cookie':
+
+        if self.path != '/' and self.path.split('/')[5] == 'set_cookie':
           # the balancer tells the backend what's the name of the balancer cookie with
           # the X-Balancer-Current-Cookie header.
           self.send_header('Set-Cookie', '%s=anything' % self.headers['X-Balancer-Current-Cookie'])
@@ -319,7 +376,7 @@ class TestBalancer(BalancerTestCase):
   def test_balancer_round_robin(self) -> None:
     # requests are by default balanced to both servers
     self.assertEqual(
-        {requests.get(self.default_balancer_url, verify=False).text for _ in range(10)},
+        {requests.get(self.default_balancer_zope_url, verify=False).text for _ in range(10)},
         {'backend_web_server1', 'backend_web_server2'}
     )
 
@@ -328,7 +385,7 @@ class TestBalancer(BalancerTestCase):
     self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).close()
     self.addCleanup(self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).open)
     self.assertEqual(
-        {requests.get(self.default_balancer_url, verify=False).text for _ in range(10)},
+        {requests.get(self.default_balancer_zope_url, verify=False).text for _ in range(10)},
         {'backend_web_server1',}
     )
 
@@ -336,7 +393,7 @@ class TestBalancer(BalancerTestCase):
     # if backend provides a "SERVERID" cookie, balancer will overwrite it with the
     # backend selected by balancing algorithm
     self.assertIn(
-        requests.get(urllib.parse.urljoin(self.default_balancer_url, '/set_cookie'), verify=False).cookies['SERVERID'],
+        requests.get(urllib.parse.urljoin(self.default_balancer_zope_url, '/set_cookie'), verify=False).cookies['SERVERID'],
         ('default-0', 'default-1'),
     )
 
@@ -344,7 +401,7 @@ class TestBalancer(BalancerTestCase):
     # if request is made with the sticky cookie, the client stick on one balancer
     cookies = dict(SERVERID='default-1')
     self.assertEqual(
-        {requests.get(self.default_balancer_url, verify=False, cookies=cookies).text for _ in range(10)},
+        {requests.get(self.default_balancer_zope_url, verify=False, cookies=cookies).text for _ in range(10)},
         {'backend_web_server2',}
     )
 
@@ -352,7 +409,7 @@ class TestBalancer(BalancerTestCase):
     self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).close()
     self.addCleanup(self.getManagedResource("backend_web_server2", BalancerCookieHTTPServer).open)
     self.assertEqual(
-        requests.get(self.default_balancer_url, verify=False, cookies=cookies).text,
+        requests.get(self.default_balancer_zope_url, verify=False, cookies=cookies).text,
         'backend_web_server1')
 
   def test_balancer_stats_socket(self) -> None:
@@ -372,7 +429,7 @@ class TestBalancer(BalancerTestCase):
       raise
     self.assertEqual(socat_process.poll(), 0)
     # output is a csv
-    self.assertIn(b'family_default,FRONTEND,', output)
+    self.assertIn(b'\ndefault,BACKEND,', output)
 
 
 class TestTestRunnerEntryPoints(BalancerTestCase):
@@ -471,7 +528,7 @@ class TestHTTP(BalancerTestCase):
             '--insecure',
             '--write-out',
             '%{http_version}',
-            self.default_balancer_url,
+            self.default_balancer_zope_url,
         ]),
         b'2',
     )
@@ -482,16 +539,16 @@ class TestHTTP(BalancerTestCase):
       session.verify = False
 
       # do a first request, which establish a first connection
-      session.get(self.default_balancer_url).raise_for_status()
+      session.get(self.default_balancer_zope_url).raise_for_status()
 
       # "break" new connection method and check we can make another request
       with mock.patch(
           "requests.packages.urllib3.connectionpool.HTTPSConnectionPool._new_conn",
       ) as new_conn:
-        session.get(self.default_balancer_url).raise_for_status()
+        session.get(self.default_balancer_zope_url).raise_for_status()
       new_conn.assert_not_called()
 
-      parsed_url = urllib.parse.urlparse(self.default_balancer_url)
+      parsed_url = urllib.parse.urlparse(self.default_balancer_zope_url)
 
       # check that we have an open file for the ip connection
       self.assertTrue([
@@ -538,10 +595,10 @@ class TestServerTLSEmbeddedCaucase(BalancerTestCase):
     return crypto_cert
 
   def test_certificate_validates_with_caucase_ca(self) -> None:
-    requests.get(self.default_balancer_url, verify=self._getCaucaseCACertificatePath())
+    requests.get(self.default_balancer_zope_url, verify=self._getCaucaseCACertificatePath())
 
   def test_certificate_renewal(self) -> None:
-    balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_url)
+    balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_zope_url)
     certificate_before_renewal = self._getServerCertificate(
       balancer_parsed_url.hostname,
       balancer_parsed_url.port)
@@ -640,7 +697,7 @@ class TestServerTLSCSRTemplateParameter(TestServerTLSExternalCaucase):
 
   def test_certificate_validates_with_caucase_ca(self) -> None:
     super().test_certificate_validates_with_caucase_ca()
-    balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_url)
+    balancer_parsed_url = urllib.parse.urlparse(self.default_balancer_zope_url)
     cert = self._getServerCertificate(
       balancer_parsed_url.hostname,
       balancer_parsed_url.port,
@@ -655,6 +712,8 @@ class ContentTypeHTTPServer(ManagedHTTPServer):
 
   For example when requested http://host/text/plain it will reply
   with Content-Type: text/plain header.
+  This actually uses a URL like this to support zope style virtual host:
+  GET /VirtualHostBase/https/{host}/VirtualHostRoot/text/plain
 
   The body is always "OK"
   """
@@ -665,7 +724,7 @@ class ContentTypeHTTPServer(ManagedHTTPServer):
       if self.path == '/':
         self.send_header("Content-Length", '0')
         return self.end_headers()
-      content_type = self.path[1:]
+      content_type = '/'.join(self.path.split('/')[5:])
       body = b"OK"
       self.send_header("Content-Type", content_type)
       self.send_header("Content-Length", str(len(body)))
@@ -707,7 +766,7 @@ class TestContentEncoding(BalancerTestCase):
         'application/font-woff2',
         'application/x-font-opentype',
         'application/wasm',):
-      resp = requests.get(urllib.parse.urljoin(self.default_balancer_url, content_type), verify=False)
+      resp = requests.get(urllib.parse.urljoin(self.default_balancer_zope_url, content_type), verify=False)
       self.assertEqual(resp.headers['Content-Type'], content_type)
       self.assertEqual(
           resp.headers.get('Content-Encoding'),
@@ -716,7 +775,7 @@ class TestContentEncoding(BalancerTestCase):
       self.assertEqual(resp.text, 'OK')
 
   def test_no_gzip_encoding(self) -> None:
-    resp = requests.get(urllib.parse.urljoin(self.default_balancer_url, '/image/png'), verify=False)
+    resp = requests.get(urllib.parse.urljoin(self.default_balancer_zope_url, '/image/png'), verify=False)
     self.assertNotIn('Content-Encoding', resp.headers)
     self.assertEqual(resp.text, 'OK')
 
@@ -806,7 +865,19 @@ class TestServerTLSProvidedCertificate(BalancerTestCase):
   def _getInstanceParameterDict(cls) -> dict:
     server_caucase = cls.getManagedResource('server_caucase', CaucaseService)
     server_certificate = cls.getManagedResource('server_certificate', CaucaseCertificate)
-    server_certificate.request(cls._ipv4_address, server_caucase)
+    # Add all IPs of the computer in SubjectAlternativeName, we don't
+    # know what will be the IP of the balancer partition.
+    with sqlite3.connect(cls.slap._proxy_database) as db:
+      ip_address_list = [
+        x509.IPAddress(ipaddress.ip_address(r)) for (r, ) in db.execute(
+          f"SELECT address FROM partition_network{DB_VERSION}").fetchall()
+      ]
+    assert ip_address_list
+    server_certificate.request(
+      cls.__name__,
+      server_caucase,
+      x509.SubjectAlternativeName(ip_address_list))
+
     parameter_dict = super()._getInstanceParameterDict()
     with open(server_certificate.cert_file) as f:
       parameter_dict['ssl']['cert'] = f.read()
@@ -816,7 +887,7 @@ class TestServerTLSProvidedCertificate(BalancerTestCase):
 
   def test_certificate_validates_with_provided_ca(self) -> None:
     server_certificate = self.getManagedResource("server_certificate", CaucaseCertificate)
-    requests.get(self.default_balancer_url, verify=server_certificate.ca_crt_file)
+    requests.get(self.default_balancer_zope_url, verify=server_certificate.ca_crt_file)
 
 
 class TestClientTLS(BalancerTestCase):
@@ -862,7 +933,7 @@ class TestClientTLS(BalancerTestCase):
       # the client certificate in "remote-user" header
       def _make_request() -> dict:
         return requests.get(
-            self.default_balancer_url,
+            self.default_balancer_zope_url,
             cert=(client_certificate.cert_file, client_certificate.key_file),
             verify=False,
         ).json()
diff --git a/software/erp5/test/test/test_erp5.py b/software/erp5/test/test/test_erp5.py
index 55343862c741de5be9e4bd5155a5db2ca4f679f3..4c0a1379e2be79e32cdfb557997b6e391469eb91 100644
--- a/software/erp5/test/test/test_erp5.py
+++ b/software/erp5/test/test/test_erp5.py
@@ -60,15 +60,8 @@ class TestPublishedURLIsReachableMixin:
   """Mixin that checks that default page of ERP5 is reachable.
   """
 
-  def _checkERP5IsReachable(self, base_url, site_id, verify):
-    # We access ERP5 trough a "virtual host", which should make
-    # ERP5 produce URLs using https://virtual-host-name:1234/virtual_host_root
-    # as base.
-    virtual_host_url = urllib.parse.urljoin(
-        base_url,
-        '/VirtualHostBase/https/virtual-host-name:1234/{}/VirtualHostRoot/_vh_virtual_host_root/'
-        .format(site_id))
-
+  @contextlib.contextmanager
+  def requestSession(self, base_url):
     # What happens is that instantiation just create the services, but does not
     # wait for ERP5 to be initialized. When this test run ERP5 instance is
     # instantiated, but zope is still busy creating the site and haproxy replies
@@ -84,7 +77,32 @@ class TestPublishedURLIsReachableMixin:
                   total=20,
                   backoff_factor=.5,
                   status_forcelist=(404, 500, 503))))
+      yield session
+
+  def _checkERP5IsReachableWithVirtualHost(self, url, verify):
+    with self.requestSession(urllib.parse.urljoin(url, '/')) as session:
+      r = session.get(url, verify=verify, allow_redirects=True)
+      # access on / are redirected to login form
+      self.assertTrue(r.url.endswith('/login_form'))
+      self.assertEqual(r.status_code, requests.codes.ok)
+      self.assertIn("ERP5", r.text)
 
+      # host header is used in redirected URL. The URL is always https
+      r = session.get(url, verify=verify, allow_redirects=False, headers={'Host': 'www.example.com'})
+      self.assertEqual(r.headers.get('Location'), 'https://www.example.com/login_form')
+      r = session.get(url, verify=verify, allow_redirects=False, headers={'Host': 'www.example.com:1234'})
+      self.assertEqual(r.headers.get('Location'), 'https://www.example.com:1234/login_form')
+
+  def _checkERP5IsReachableWithoutVirtualHost(self, base_url, site_id, verify):
+    # We access ERP5 trough a "virtual host", which should make
+    # ERP5 produce URLs using https://virtual-host-name:1234/virtual_host_root
+    # as base.
+    virtual_host_url = urllib.parse.urljoin(
+        base_url,
+        '/VirtualHostBase/https/virtual-host-name:1234/{}/VirtualHostRoot/_vh_virtual_host_root/'
+        .format(site_id))
+
+    with self.requestSession(base_url) as session:
       r = session.get(virtual_host_url, verify=verify, allow_redirects=False)
       self.assertEqual(r.status_code, requests.codes.found)
       # access on / are redirected to login form, with virtual host preserved
@@ -119,7 +137,7 @@ class TestPublishedURLIsReachableMixin:
     """Tests the IPv6 URL published by the root partition is reachable.
     """
     param_dict = self.getRootPartitionConnectionParameterDict()
-    self._checkERP5IsReachable(
+    self._checkERP5IsReachableWithoutVirtualHost(
       param_dict['family-default-v6'],
       param_dict['site-id'],
       self._getCaucaseServiceCACertificate(),
@@ -129,7 +147,7 @@ class TestPublishedURLIsReachableMixin:
     """Tests the IPv4 URL published by the root partition is reachable.
     """
     param_dict = self.getRootPartitionConnectionParameterDict()
-    self._checkERP5IsReachable(
+    self._checkERP5IsReachableWithoutVirtualHost(
       param_dict['family-default'],
       param_dict['site-id'],
       self._getCaucaseServiceCACertificate(),
@@ -139,9 +157,8 @@ class TestPublishedURLIsReachableMixin:
     """Tests the frontend URL published by the root partition is reachable.
     """
     param_dict = self.getRootPartitionConnectionParameterDict()
-    self._checkERP5IsReachable(
+    self._checkERP5IsReachableWithVirtualHost(
       param_dict['url-frontend-default'],
-      param_dict['site-id'],
       self._getCaucaseServiceCACertificate(),
     )
 
@@ -157,9 +174,8 @@ class TestDefaultParameters(ERP5InstanceTestCase, TestPublishedURLIsReachableMix
                            '.installed-switch-softwaretype.cfg')) as f:
       installed = zc.buildout.configparser.parse(f, 'installed')
     self.assertEqual(
-      installed['request-frontend-default']['config-type'], 'zope')
-    self.assertEqual(
-      installed['request-frontend-default']['config-path'], '/erp5')
+      installed['request-frontend-default']['config-type'], '')
+    self.assertNotIn('config-path', installed['request-frontend-default'])
     self.assertEqual(
       installed['request-frontend-default']['config-authenticate-to-backend'], 'true')
     self.assertEqual(installed['request-frontend-default']['shared'], 'true')
@@ -310,8 +326,8 @@ class TestBalancerPorts(ERP5InstanceTestCase):
     # frontend pointing to this port.
     self.assertEqual(urllib.parse.urlparse(param_dict['family-family1']).port, 2152)
     self.assertEqual(urllib.parse.urlparse(param_dict['family-family1-v6']).port, 2152)
-    self.assertEqual(urllib.parse.urlparse(param_dict['family-family2']).port, 2154)
-    self.assertEqual(urllib.parse.urlparse(param_dict['family-family2-v6']).port, 2154)
+    self.assertEqual(urllib.parse.urlparse(param_dict['family-family2']).port, 2155)
+    self.assertEqual(urllib.parse.urlparse(param_dict['family-family2-v6']).port, 2155)
 
   def test_published_test_runner_url(self):
     # each family's also a list of test test runner URLs, by default 3 per family
@@ -390,6 +406,8 @@ class TestBalancerPortsStable(ERP5InstanceTestCase):
 
   def test_same_balancer_ports_when_adding_zopes_or_frontends(self):
     param_dict_before = self.getRootPartitionConnectionParameterDict()
+    balancer_param_dict_before = json.loads(
+      self.getComputerPartition('balancer').getConnectionParameter('_'))
 
     # re-request with one more frontend and one more backend, that are before
     # the existing ones when sorting alphabetically
@@ -410,10 +428,15 @@ class TestBalancerPortsStable(ERP5InstanceTestCase):
     rerequest()
     self.slap.waitForInstance(max_retry=10)
     param_dict_after = json.loads(rerequest().getConnectionParameterDict()['_'])
+    balancer_param_dict_after = json.loads(
+      self.getComputerPartition('balancer').getConnectionParameter('_'))
+
     self.assertEqual(param_dict_before['family-zzz-v6'], param_dict_after['family-zzz-v6'])
     self.assertEqual(param_dict_before['url-frontend-zzz'], param_dict_after['url-frontend-zzz'])
+    self.assertEqual(balancer_param_dict_before['url-backend-zzz'], balancer_param_dict_after['url-backend-zzz'])
     self.assertNotEqual(param_dict_before['family-zzz-v6'], param_dict_after['family-aaa-v6'])
     self.assertNotEqual(param_dict_before['url-frontend-zzz'], param_dict_after['url-frontend-aaa'])
+    self.assertNotEqual(balancer_param_dict_before['url-backend-zzz'], balancer_param_dict_after['url-backend-aaa'])
 
 
 class TestSeleniumTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMixin):
@@ -476,14 +499,15 @@ class TestDisableTestRunner(ERP5InstanceTestCase, TestPublishedURLIsReachableMix
     self.assertNotIn('runTestSuite', bin_programs)
 
   def test_no_haproxy_testrunner_port(self):
-    # Haproxy only listen on two ports, there is no haproxy ports allocated for test runner
+    # Haproxy only listen on two ports for frontend, two ports for legacy entry points
+    # and there is no haproxy ports allocated for test runner
     with self.slap.instance_supervisor_rpc as supervisor:
       all_process_info = supervisor.getAllProcessInfo()
     process_info, = (p for p in all_process_info if p['name'].startswith('haproxy'))
     haproxy_master_process = psutil.Process(process_info['pid'])
     haproxy_worker_process, = haproxy_master_process.children()
     self.assertEqual(
-        sorted([socket.AF_INET, socket.AF_INET6]),
+        sorted([socket.AF_INET, socket.AF_INET6, socket.AF_INET, socket.AF_INET6]),
         sorted(
             c.family
             for c in haproxy_worker_process.connections()
@@ -1356,10 +1380,13 @@ class TestFrontend(ERP5InstanceTestCase):
             },
             "web": {
               "family": "web",
+              "port-base": 2300,
             },
             "activities": {
               # this family will not have frontend
-              "family": "activities"
+              "family": "activities",
+              "port-base": 2400,
+
             },
           },
           "frontend": {
@@ -1368,7 +1395,7 @@ class TestFrontend(ERP5InstanceTestCase):
             },
             "website": {
               "zope-family": "web",
-              "internal-path": "/%(site-id)s/web_site_module/my_website/",
+              "internal-path": "/%(site-id)s/web_site_module/my_website",
               "instance-parameters": {
                 # some extra frontend parameters
                 "enable_cache": "true",
@@ -1396,19 +1423,20 @@ class TestFrontend(ERP5InstanceTestCase):
 
   def test_request_parameters(self):
     param_dict = self.getRootPartitionConnectionParameterDict()
+    balancer_param_dict = json.loads(
+      self.getComputerPartition('balancer').getConnectionParameter('_'))
 
     with open(os.path.join(self.computer_partition_root_path,
                            '.installed-switch-softwaretype.cfg')) as f:
       installed = zc.buildout.configparser.parse(f, 'installed')
     self.assertEqual(
-      installed['request-frontend-backoffice']['config-type'], 'zope')
+      installed['request-frontend-backoffice']['config-type'], '')
     self.assertEqual(
       installed['request-frontend-backoffice']['shared'], 'true')
     self.assertEqual(
       installed['request-frontend-backoffice']['config-url'],
-      param_dict['family-default-v6'])
-    self.assertEqual(
-      installed['request-frontend-backoffice']['config-path'], '/erp5')
+      balancer_param_dict['url-backend-backoffice'])
+    self.assertNotIn('config-path', installed['request-frontend-backoffice'])
     self.assertEqual(
       installed['request-frontend-backoffice']['sla-computer_guid'],
       'COMP-1234')
@@ -1421,7 +1449,7 @@ class TestFrontend(ERP5InstanceTestCase):
       param_dict['url-frontend-backoffice'])
 
     self.assertEqual(
-      installed['request-frontend-website']['config-type'], 'zope')
+      installed['request-frontend-website']['config-type'], '')
     # no SLA by default
     self.assertFalse([k for k in installed['request-frontend-website'] if k.startswith('sla-')])
     # instance parameters are propagated
@@ -1429,10 +1457,8 @@ class TestFrontend(ERP5InstanceTestCase):
       installed['request-frontend-website']['config-enable_cache'], 'true')
     self.assertEqual(
       installed['request-frontend-website']['config-url'],
-      param_dict['family-web-v6'])
-    self.assertEqual(
-      installed['request-frontend-website']['config-path'],
-      '/erp5/web_site_module/my_website/')
+      balancer_param_dict['url-backend-website'])
+    self.assertNotIn('config-path', installed['request-frontend-website'])
     self.assertEqual(
       installed['request-frontend-website']['connection-secure_access'],
       param_dict['url-frontend-website'])
@@ -1440,6 +1466,32 @@ class TestFrontend(ERP5InstanceTestCase):
     # no frontend was requested for activities family
     self.assertNotIn('request-frontend-activities', installed)
     self.assertNotIn('url-frontend-activities', param_dict)
+    self.assertNotIn('url-backend-activities', balancer_param_dict)
+
+  def test_path_virtualhost(self):
+    balancer_param_dict = json.loads(
+      self.getComputerPartition('balancer').getConnectionParameter('_'))
+    found_line = False
+    retries = 10
+    while retries:
+      requests.get(balancer_param_dict['url-backend-website'], verify=False)
+      for logfile in glob.glob(os.path.join(self.getComputerPartitionPath('zope-web'), 'var/log/*Z2.log')):
+        with open(logfile) as f:
+          for line in f:
+            if 'GET /VirtualHost' in line:
+              found_line = True
+              break
+      if found_line:
+        break
+      time.sleep(1)
+      retries = retries - 1
+    self.assertTrue(found_line)
+
+    percent_encoded_netloc = urllib.parse.quote(
+      urllib.parse.urlparse(
+        balancer_param_dict['url-backend-website']).netloc)
+    self.assertIn(
+      f'/VirtualHostBase/https/{percent_encoded_netloc}/erp5/web_site_module/my_website/VirtualHostRoot/ HTTP', line)
 
 
 class TestDefaultFrontendWithZopePartitionDict(ERP5InstanceTestCase):
@@ -1468,12 +1520,15 @@ class TestDefaultFrontendWithZopePartitionDict(ERP5InstanceTestCase):
 
   def test_frontend_requested(self):
     param_dict = self.getRootPartitionConnectionParameterDict()
+    balancer_param_dict = json.loads(
+      self.getComputerPartition('balancer').getConnectionParameter('_'))
+
     with open(os.path.join(self.computer_partition_root_path,
                            '.installed-switch-softwaretype.cfg')) as f:
       installed = zc.buildout.configparser.parse(f, 'installed')
     self.assertEqual(
       installed['request-frontend-default']['config-url'],
-      param_dict['family-backoffice-v6'])
+      balancer_param_dict['url-backend-default'])
 
     requests.get(
       param_dict['url-frontend-default'],
diff --git a/software/slapos-master/buildout.hash.cfg b/software/slapos-master/buildout.hash.cfg
index b59e8f848c242cca20b38aa2cc43efbe84fec49f..a12fafb63f3a038a398785d6b13b9f745c1c02c9 100644
--- a/software/slapos-master/buildout.hash.cfg
+++ b/software/slapos-master/buildout.hash.cfg
@@ -14,7 +14,7 @@
 # not need these here).
 [template-erp5]
 filename = instance-erp5.cfg.in
-md5sum = ba46a66da1c834df14a80a20b21e4a96
+md5sum = 6db19ee819a960a34012308e29c5bbfb
 
 [template-balancer]
 filename = instance-balancer.cfg.in
diff --git a/software/slapos-master/instance-erp5.cfg.in b/software/slapos-master/instance-erp5.cfg.in
index 819ea68efcbbd29733ed08320d3a168346379bec..94d9b91a8dc6b208c57b2491d067cb6d7ccb2558 100644
--- a/software/slapos-master/instance-erp5.cfg.in
+++ b/software/slapos-master/instance-erp5.cfg.in
@@ -435,6 +435,7 @@ return =
 {% endfor -%}
 {% do monitor_base_url_dict.__setitem__('request-balancer', '${' ~ 'request-balancer' ~ ':connection-monitor-base-url}') -%}
 config-zope-family-dict = {{ dumps(zope_family_parameter_dict) }}
+config-frontend-parameter-dict = {{ dumps({}) }}
 config-tcpv4-port = {{ dumps(balancer_dict.get('tcpv4-port', 2150)) }}
 {% for zope_section_id, name in zope_address_list_id_dict.items() -%}
 config-{{ name }} = {{ ' ${' ~ zope_section_id ~ ':connection-zope-address-list}' }}
diff --git a/stack/erp5/buildout.hash.cfg b/stack/erp5/buildout.hash.cfg
index 141fc17ecc65504f9a15567243359cd7417657e8..1b91f68a2ec6902915b5fb76a1f0d55774ce1b05 100644
--- a/stack/erp5/buildout.hash.cfg
+++ b/stack/erp5/buildout.hash.cfg
@@ -74,7 +74,7 @@ md5sum = 55463b0abdbe0118ef1c27e6b71c3324
 
 [template-erp5]
 filename = instance-erp5.cfg.in
-md5sum = 855fdc8ed0b2671f9e2c74d127cc6858
+md5sum = ae9c380ae04dde4f20e139c66ef7c22a
 
 [template-zeo]
 filename = instance-zeo.cfg.in
@@ -90,11 +90,11 @@ md5sum = 6178ba7b42848f9e2412ab898a7b026c
 
 [template-balancer]
 filename = instance-balancer.cfg.in
-md5sum = 42cb68905f92e7df38cc5c64b94be3de
+md5sum = 0fad9497da12ed0186dca5236c23f3a7
 
 [template-haproxy-cfg]
 filename = haproxy.cfg.in
-md5sum = 9988a14c4108e3bce3f871e34673cdd5
+md5sum = 2cd76971b64b0bf7771978ad07bfc2e5
 
 [template-rsyslogd-cfg]
 filename = rsyslogd.cfg.in
diff --git a/stack/erp5/haproxy.cfg.in b/stack/erp5/haproxy.cfg.in
index 36d4dba4d220824d02945864e6ad5090c6ad3cd7..a2efbaca32d457fac8ad87a9dda6a9777cb90c50 100644
--- a/stack/erp5/haproxy.cfg.in
+++ b/stack/erp5/haproxy.cfg.in
@@ -18,11 +18,11 @@
  #       "stats-socket": "<file_path>",
  #
  #       #  IPv4 to listen on
- #       #  All backends from `backend-dict` will listen on this IP.
+ #       #  All frontends from `frontend-dict` will listen on this IP.
  #       "ipv4": "0.0.0.0",
  #
  #       #  IPv6 to listen on
- #       #  All backends from `backend-dict` will listen on this IP.
+ #       #  All frontends from `frontend-dict` will listen on this IP.
  #       "ipv6": "::1",
  #
  #       #  Certificate and key in PEM format. All ports will serve TLS using
@@ -41,34 +41,59 @@
  #       #  Path to use for HTTP health check on backends from `backend-dict`.
  #       "server-check-path": "/",
  #
+ #       #  The mapping of frontend, keyed by frontend name
+ #       "frontend-dict": {
+ #         "frontend-default": {
+ #            "port": 8080,
+ #            "client-cert-required": False,
+ #            "backend-name": "family-default",
+ #            "request-path-prepend": "/erp5",
+ #         }
+ #         "legacy-frontend-family-secure": {
+ #            "port": 8000,
+ #            "client-cert-required": False,
+ #            "backend-name": "family-secure",
+ #            "request-path-prepend": None, # None means do not rewrite the request path
+ #         }
+ #         "legacy-frontend-family-default": {
+ #            "port": 8002,
+ #            "client-cert-required": False,
+ #            "backend-name": "family-default",
+ #            "request-path-prepend": None, # None means do not rewrite the request path
+ #         }
+ #       }
  #       #  The mapping of backends, keyed by family name
  #       "backend-dict": {
- #          "family-secure": {
- #            (  8000, # port int
- #               True, # ssl_required bool
- #               None,  # timeout (in seconds) int | None
- #               [  # backends
- #                  '10.0.0.10:8001', # netloc str
- #                   1, # max_connection_count int
- #                   False, # is_web_dav bool
- #               ],
- #            ),
- #          },
- #          "family-default": {
- #            (  8002, # port int
- #               False, # ssl_required bool
- #               None,  # timeout (in seconds) int | None
- #               [  # backends
- #                  '10.0.0.10:8003', # netloc str
- #                   1, # max_connection_count int
- #                   False, # is_web_dav bool
- #               ],
- #            ),
+ #           "family-secure": {
+ #               "timeout": None,  # in seconds
+ #               "backend-list": [
+ #                   [
+ #                       '10.0.0.10:8001', # netloc str
+ #                        1, # max_connection_count int
+ #                        False, # is_web_dav bool
+ #                   ]
+ #               ]
+ #           },
+ #           "family-default": {
+ #               "timeout": None,  # in seconds
+ #               "backend-list": [
+ #                   [
+ #                      '10.0.0.10:8003', # netloc str
+ #                       1, # max_connection_count int
+ #                       False, # is_web_dav bool
+ #                   ],
+ #                   [
+ #                      '10.0.0.10:8004', # netloc str
+ #                       1, # max_connection_count int
+ #                       False, # is_web_dav bool
+ #                   ],
+ #               ]
  #          },
  #
  #       # The mapping of zope paths.
- #       # This is a Zope specific feature.
- #       # `enable_authentication` has same meaning as for `backend-list`.
+ #       # This is a Zope specific feature used only to provide https while running
+ #       # ERP5 "unit test" suite.
+ #       # `enable_authentication` has same meaning as for `backend-dict`.
  #       "zope-virtualhost-monster-backend-dict": {
  #          # {(ip, port): ( enable_authentication, {frontend_path: ( internal_url ) }, ) }
  #          ('[::1]', 8004): (
@@ -81,15 +106,20 @@
  #     }
  #
  #  This sample of `parameter_dict` will make haproxy listening to :
- #  From to `backend-list`:
- #  For "family-secure":
+ #  For "frontend-default":
+ #   - 0.0.0.0:8080 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
+ #   - [::1]:8080 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
+ #  accepting requests from any client and rewriting the path to add a Zope rewrite rule
+ #  so that the a request on https://0.0.0.0:8080/path is rewritten to serve a Zope object at
+ #  path /erp5/path , visible as /path.
+ #  For "legacy-frontend-family-secure":
  #   - 0.0.0.0:8000 redirecting internaly to http://10.0.0.10:8001 and
  #   - [::1]:8000 redirecting internaly to http://10.0.0.10:8001
  #  only accepting requests from clients providing a verified TLS certificate
  #  emitted by a CA from `ca-cert` and not revoked in `crl`.
- #  For "family-default":
- #   - 0.0.0.0:8002 redirecting internaly to http://10.0.0.10:8003
- #   - [::1]:8002 redirecting internaly to http://10.0.0.10:8003
+ #  For "legacy-frontend-family-default":
+ #   - 0.0.0.0:8002 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
+ #   - [::1]:8002 redirecting internaly to http://10.0.0.10:8003 or http://10.0.0.10:8004
  #  accepting requests from any client.
  #
  #  For both families, X-Forwarded-For header will be stripped unless
@@ -102,7 +132,7 @@
  #   with some VirtualHostMonster rewrite rules so zope writes URLs with
  #  [::1]:8004 as server name.
  #  For more details, refer to
- #  https://docs.zope.org/zope2/zope2book/VirtualHosting.html#using-virtualhostroot-and-virtualhostbase-together
+ #  https://zope.readthedocs.io/en/latest/zopebook/VirtualHosting.html#using-virtualhostroot-and-virtualhostbase-together
 -#}
 
 {% set server_check_path = parameter_dict['server-check-path'] -%}
@@ -148,26 +178,17 @@ defaults
 {% set family_path_routing_dict = parameter_dict['family-path-routing-dict'] %}
 {% set path_routing_list = parameter_dict['path-routing-list'] %}
 
-{% for name, (port, certificate_authentication, timeout, backend_list) in sorted(six.iteritems(parameter_dict['backend-dict'])) -%}
-listen family_{{ name }}
+
+{% for name, frontend in sorted(six.iteritems(parameter_dict['frontend-dict'])) %}
+listen {{ name }}
+
 {%-  if parameter_dict.get('ca-cert') -%}
-{%-    set ssl_auth = ' ca-file ' ~ parameter_dict['ca-cert'] ~ ' verify' ~ ( ' required' if certificate_authentication else ' optional crt-ignore-err all' ) ~ ' crl-file ' ~ parameter_dict['crl'] %}
+{%-    set ssl_auth = ' ca-file ' ~ parameter_dict['ca-cert'] ~ ' verify' ~ ( ' required' if frontend['client-cert-required'] else ' optional crt-ignore-err all' ) ~ ' crl-file ' ~ parameter_dict['crl'] %}
 {%-  else %}
 {%-    set ssl_auth = '' %}
 {%-  endif %}
-  bind {{ parameter_dict['ipv4'] }}:{{ port }} {{ bind_ssl_crt }} {{ ssl_auth }}
-  bind {{ parameter_dict['ipv6'] }}:{{ port }} {{ bind_ssl_crt }} {{ ssl_auth }}
-  cookie SERVERID rewrite
-  http-request set-header X-Balancer-Current-Cookie SERVERID
-
-{% if timeout %}
-  {#
-    Apply a slightly longer timeout than the zope timeout so that clients can see the
-    TimeoutReachedError from zope, that is a bit more informative than the 504 error
-    page from haproxy.
-  #}
-  timeout server {{ timeout + 3 }}s
-{%-  endif %}
+  bind {{ parameter_dict['ipv4'] }}:{{ frontend['port'] }} {{ bind_ssl_crt }} {{ ssl_auth }}
+  bind {{ parameter_dict['ipv6'] }}:{{ frontend['port'] }} {{ bind_ssl_crt }} {{ ssl_auth }}
 
   # remove X-Forwarded-For unless client presented a verified certificate
   http-request del-header X-Forwarded-For unless { ssl_c_verify 0 } { ssl_c_used 1 }
@@ -175,18 +196,43 @@ listen family_{{ name }}
   http-request del-header Remote-User
   http-request set-header Remote-User %{+Q}[ssl_c_s_dn(cn)] if { ssl_c_verify 0 } { ssl_c_used 1 }
 
+  # reject invalid host header before using it in path
+  http-request deny deny_status 400 if { req.hdr(host) -m sub / }
+
   # logs
   capture request header Referer len 512
   capture request header User-Agent len 512
   log-format "%{+Q}o %{-Q}ci - - [%trg] %r %ST %B %{+Q}[capture.req.hdr(0)] %{+Q}[capture.req.hdr(1)] %Ta"
 
-{%   for outer_prefix, inner_prefix in family_path_routing_dict.get(name, []) + path_routing_list %}
+{% if frontend['request-path-prepend'] is not none %}
+  http-request replace-path ^/(.*) /VirtualHostBase/https/%[req.hdr(Host)]{{ frontend['request-path-prepend'] }}/VirtualHostRoot/\1
+{% endif %}
+
+{%   for outer_prefix, inner_prefix in family_path_routing_dict.get(frontend['backend-name'], []) + path_routing_list %}
   {%   set outer_prefix = outer_prefix.strip('/') -%}
   http-request replace-path ^(/+VirtualHostBase/+[^/]+/+[^/]+)/+VirtualHostRoot/+{% if outer_prefix %}{{ outer_prefix }}($|/.*){% else %}(.*){% endif %} \1/{{ inner_prefix.strip('/') }}/VirtualHostRoot/{% if outer_prefix %}_vh_{{ outer_prefix.replace('/', '/_vh_') }}{% endif %}\2
 {%  endfor %}
 
+  use_backend {{ frontend['backend-name'] }}
+{% endfor %}
+
+
+{% for name, backend in sorted(six.iteritems(parameter_dict['backend-dict'])) %}
+backend {{ name }}
+  cookie SERVERID rewrite
+  http-request set-header X-Balancer-Current-Cookie SERVERID
+
+{% if backend['timeout'] %}
+  {#
+    Apply a slightly longer timeout than the zope timeout so that clients can see the
+    TimeoutReachedError from zope, that is a bit more informative than the 504 error
+    page from haproxy.
+  #}
+  timeout server {{ backend['timeout'] + 3 }}s
+{%-  endif %}
+
 {%   set has_webdav = [] -%}
-{%   for address, connection_count, webdav in backend_list -%}
+{%   for address, connection_count, webdav in backend['backend-list'] -%}
 {%     if webdav %}{% do has_webdav.append(None) %}{% endif -%}
 {%     set server_name = name ~ '-' ~ loop.index0 %}
   server {{ server_name }} {{ address }} cookie {{ server_name }} check inter 3s rise 1 fall 2 maxqueue 5 maxconn {{ connection_count }}
diff --git a/stack/erp5/instance-balancer.cfg.in b/stack/erp5/instance-balancer.cfg.in
index 970c6d6dd4dc22214b36feb0637ba4fbb712fafb..b4aadaa2029dc8a6ac5602591dbfc829d621882b 100644
--- a/stack/erp5/instance-balancer.cfg.in
+++ b/stack/erp5/instance-balancer.cfg.in
@@ -257,7 +257,8 @@ init =
     port_dict[name] = port
     return port
 
-  haproxy_dict = {}
+  backend_dict = {}
+  frontend_dict = {}
   zope_virtualhost_monster_backend_dict = {}
   for family_name, parameter_id_list in sorted(
       six.iteritems(slapparameter_dict['zope-family-dict'])):
@@ -290,17 +291,33 @@ init =
     # a port for monitoring promise (which port is not important, the promise checks
     # that haproxy is healthy enough to listen on a port)
     options['haproxy-promise-port'] = legacy_port
-    haproxy_dict[family_name] = (
-      legacy_port,
-      ssl_authentication,
-      slapparameter_dict['timeout-dict'][family_name],
-      zope_family_address_list,
-    )
+    frontend_dict['legacy-frontend-' + family_name] = {
+      'port': legacy_port,
+      'client-cert-required': ssl_authentication,
+      'backend-name': family_name,
+      'request-path-prepend': None,
+    }
+    backend_dict[family_name] = {
+      'timeout': slapparameter_dict['timeout-dict'][family_name],
+      'backend-list': zope_family_address_list,
+    }
     external_scheme = 'webdavs' if any(a[2] for a in zope_family_address_list) else 'https'
     self.buildout['publish'][family_name] = "{external_scheme}://{ipv4}:{legacy_port}".format(**locals())
     self.buildout['publish'][family_name + "-v6"] = "{external_scheme}://[{ipv6}]:{legacy_port}".format(**locals())
 
-  options['backend-dict'] = haproxy_dict
+  for frontend_name, frontend in six.iteritems(slapparameter_dict['frontend-parameter-dict']):
+    frontend_port = get_port('frontend-' + frontend_name)
+    family_name = frontend['zope-family']
+    frontend_dict['frontend-' + frontend_name] = {
+      'port': frontend_port,
+      'client-cert-required': slapparameter_dict['ssl-authentication-dict'][family_name],
+      'backend-name': family_name,
+      'request-path-prepend': frontend['internal-path'],
+    }
+    self.buildout['publish']['url-backend-' + frontend_name] = "https://[{ipv6}]:{frontend_port}".format(**locals())
+
+  options['backend-dict'] = backend_dict
+  options['frontend-dict'] = frontend_dict
   options['zope-virtualhost-monster-backend-dict'] = zope_virtualhost_monster_backend_dict
 
   if port_dict != previous_port_dict:
diff --git a/stack/erp5/instance-erp5.cfg.in b/stack/erp5/instance-erp5.cfg.in
index 37fccb600959e2605e2de0214b00c69aa3c3ef28..6a9a2249badc5fc66bd61e95faafefdd2533534e 100644
--- a/stack/erp5/instance-erp5.cfg.in
+++ b/stack/erp5/instance-erp5.cfg.in
@@ -388,13 +388,13 @@ return =
 {%   set request_frontend_name = 'request-frontend-' ~ frontend_name -%}
 {%   set frontend_software_url = frontend_parameters.get('software-url', 'http://git.erp5.org/gitweb/slapos.git/blob_plain/HEAD:/software/apache-frontend/software.cfg') -%}
 {%   set frontend_software_type = frontend_parameters.get('software-type', '') -%}
+{%   do frontend_parameters.__setitem__('internal-path', frontend_parameters.get('internal-path', '/%(site-id)s') % {'site-id': site_id}) %}
 {%   set frontend_instance_parameters = frontend_parameters.get('instance-parameters', {}) -%}
-{%   if frontend_instance_parameters.setdefault('type', 'zope') == 'zope' -%}
+{%   if frontend_instance_parameters.setdefault('type', '') == '' -%}
 {%     do frontend_instance_parameters.setdefault('authenticate-to-backend', 'true') -%}
 {%     set zope_family_name = frontend_parameters['zope-family'] -%}
 {%     do assert(zope_family_name in zope_family_dict, 'Unknown family %s for frontend %s' % (zope_family_name, frontend_name)) -%}
-{%     do frontend_instance_parameters.setdefault('url', '${request-balancer:connection-' ~ zope_family_name ~ '-v6}') -%}
-{%     do frontend_instance_parameters.setdefault('path', frontend_parameters.get('internal-path', '/%(site-id)s') % {'site-id': site_id}) -%}
+{%     do frontend_instance_parameters.setdefault('url', '${request-balancer:connection-url-backend-' ~ frontend_name ~ '}') -%}
 {%   endif %}
 [{{ request_frontend_name }}]
 <= request-frontend-base
@@ -446,6 +446,9 @@ config-allow-redirects = 0
 {%     do balancer_ret_dict.__setitem__(family + '-test-runner-url-list', False) -%}
 {%   endif -%}
 {% endfor -%}
+{% for frontend_name in frontend_parameter_dict -%}
+{%   do balancer_ret_dict.__setitem__('url-backend-' ~ frontend_name, False) -%}
+{% endfor -%}
 {% set balancer_key_config_dict = {
   'monitor-passwd': 'monitor-htpasswd:passwd',
 } -%}
@@ -467,6 +470,7 @@ config-allow-redirects = 0
   config_key='balancer',
   config={
     'zope-family-dict': zope_family_parameter_dict,
+    'frontend-parameter-dict': frontend_parameter_dict,
     'ssl-authentication-dict': ssl_authentication_dict,
     'timeout-dict': balancer_timeout_dict,
     'apachedex-promise-threshold': monitor_dict.get('apachedex-promise-threshold', 70),