diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml
index e8cdca9f831a5161a70e469ecc0f26fd90b1c933..5b5e417a6431e3d4b45dd22936285c545f3cd5eb 100644
--- a/ansible/group_vars/all.yml
+++ b/ansible/group_vars/all.yml
@@ -1080,6 +1080,7 @@ designate_notifications_topic_name: "notifications_designate"
 #######################
 neutron_bgp_router_id: "1.1.1.1"
 neutron_bridge_name: "{{ 'br-dvs' if neutron_plugin_agent == 'vmware_dvs' else 'br_dpdk' if enable_ovs_dpdk | bool else 'br-ex' }}"
+neutron_physical_networks: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index }}{% if not loop.last %},{% endif %}{% endfor %}"
 # Comma-separated type of enabled ml2 type drivers
 neutron_type_drivers: "flat,vlan,vxlan{% if neutron_plugin_agent == 'ovn' %},geneve{% endif %}"
 # Comma-separated types of tenant networks (should be listed in 'neutron_type_drivers')
diff --git a/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2 b/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2
index 1dbaae0ede8ec46dc9ba213bb7d2516e699abca2..5b0ae990b8833cc0ac59f32cfda9052bb687160c 100644
--- a/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2
+++ b/ansible/roles/neutron/templates/linuxbridge_agent.ini.j2
@@ -5,7 +5,8 @@ extensions = {{ neutron_agent_extensions|map(attribute='name')|join(',') }}
 
 [linux_bridge]
 {% if inventory_hostname in groups["network"] or (inventory_hostname in groups["compute"] and computes_need_external_bridge | bool ) %}
-physical_interface_mappings = {% for interface in neutron_external_interface.split(',') %}physnet{{ loop.index0 + 1 }}:{{ interface }}{% if not loop.last %},{% endif %}{% endfor %}
+{# Format: physnet1:br1,physnet2:br2 #}
+physical_interface_mappings = {{ neutron_physical_networks.split(',') | zip(neutron_external_interface.split(',')) | map('join', ':') | join(',') }}
 {% endif %}
 
 [securitygroup]
diff --git a/ansible/roles/neutron/templates/ml2_conf.ini.j2 b/ansible/roles/neutron/templates/ml2_conf.ini.j2
index 9c70eb898a725349ff450f65f644fe174173dd35..0e344776916869ee6da812841dce7fd545e3bdfd 100644
--- a/ansible/roles/neutron/templates/ml2_conf.ini.j2
+++ b/ansible/roles/neutron/templates/ml2_conf.ini.j2
@@ -15,7 +15,7 @@ extension_drivers = {{ neutron_extension_drivers | map(attribute='name') | join(
 
 [ml2_type_vlan]
 {% if enable_ironic | bool %}
-network_vlan_ranges = physnet1
+network_vlan_ranges = {{ neutron_physical_networks }}
 {% else %}
 network_vlan_ranges =
 {% endif %}
@@ -24,7 +24,7 @@ network_vlan_ranges =
 {% if enable_ironic | bool %}
 flat_networks = *
 {% else %}
-flat_networks = {% for interface in neutron_external_interface.split(',') %}physnet{{ loop.index0 + 1 }}{% if not loop.last %},{% endif %}{% endfor %}
+flat_networks = {{ neutron_physical_networks }}
 {% endif %}
 
 [ml2_type_vxlan]
diff --git a/ansible/roles/neutron/templates/openvswitch_agent.ini.j2 b/ansible/roles/neutron/templates/openvswitch_agent.ini.j2
index 88834e2deae5d0bf80f78d6a4355d5c90c66eb4a..8ac25af7e13e25c01424a04d32663bb775f6eb8d 100644
--- a/ansible/roles/neutron/templates/openvswitch_agent.ini.j2
+++ b/ansible/roles/neutron/templates/openvswitch_agent.ini.j2
@@ -15,7 +15,8 @@ firewall_driver = neutron.agent.linux.iptables_firewall.OVSHybridIptablesFirewal
 
 [ovs]
 {% if inventory_hostname in groups["network"] or (inventory_hostname in groups["compute"] and computes_need_external_bridge | bool ) %}
-bridge_mappings = {% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}
+{# Format: physnet1:br1,physnet2:br2 #}
+bridge_mappings = {{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}
 {% endif %}
 datapath_type = {{ ovs_datapath }}
 ovsdb_connection = tcp:127.0.0.1:{{ ovsdb_port }}
diff --git a/ansible/roles/ovn-controller/tasks/setup-ovs.yml b/ansible/roles/ovn-controller/tasks/setup-ovs.yml
index 5ac61a16b35151098ccbc49870f519df73889d33..0d9ab0e30c35453edba061c4794652a998442813 100644
--- a/ansible/roles/ovn-controller/tasks/setup-ovs.yml
+++ b/ansible/roles/ovn-controller/tasks/setup-ovs.yml
@@ -12,8 +12,10 @@
 
 - name: Configure OVN in OVSDB
   vars:
-    ovn_mappings: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}"
-    ovn_macs: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ ovn_base_mac | random_mac(seed=inventory_hostname + bridge) }}{% if not loop.last %},{% endif %}{% endfor %}"
+    # Format: physnet1:br1,physnet2:br2
+    ovn_mappings: "{{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}"
+    # Format: physnet1:00:11:22:33:44:55,physnet2:00:11:22:33:44:56
+    ovn_macs: "{% for physnet, bridge in neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) %}{{ physnet }}:{{ ovn_base_mac | random_mac(seed=inventory_hostname + bridge) }}{% if not loop.last %},{% endif %}{% endfor %}"
     ovn_cms_opts: "{{ 'enable-chassis-as-gw' if inventory_hostname in groups['ovn-controller-network'] else '' }}{{ ',availability-zones=' + neutron_ovn_availability_zones | join(',') if inventory_hostname in groups['ovn-controller-network'] and neutron_ovn_availability_zones }}"
   become: true
   kolla_toolbox:
diff --git a/ansible/roles/ovs-dpdk/defaults/main.yml b/ansible/roles/ovs-dpdk/defaults/main.yml
index 209eb95fbe0baa844e6f0c73a2e58f2c3d7ea284..ac9cd0c73104423015357fd2740df27c94de8670 100644
--- a/ansible/roles/ovs-dpdk/defaults/main.yml
+++ b/ansible/roles/ovs-dpdk/defaults/main.yml
@@ -37,8 +37,10 @@ ovsdpdk_services:
 ####################
 # OVS
 ####################
-ovs_bridge_mappings: "{% for bridge in neutron_bridge_name.split(',') %}physnet{{ loop.index0 + 1 }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}"
-ovs_port_mappings: "{% for bridge in neutron_bridge_name.split(',') %} {{ neutron_external_interface.split(',')[loop.index0] }}:{{ bridge }}{% if not loop.last %},{% endif %}{% endfor %}"
+# Format: physnet1:br1,physnet2:br2
+ovs_bridge_mappings: "{{ neutron_physical_networks.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}"
+# Format: eth1:br1,eth2:br2
+ovs_port_mappings: "{{ neutron_external_interface.split(',') | zip(neutron_bridge_name.split(',')) | map('join', ':') | join(',') }}"
 tunnel_interface_network: "{{ hostvars[inventory_hostname].ansible_facts[dpdk_tunnel_interface]['ipv4']['network'] }}/{{ hostvars[inventory_hostname].ansible_facts[dpdk_tunnel_interface]['ipv4']['netmask'] }}"
 tunnel_interface_cidr: "{{ dpdk_tunnel_interface_address }}/{{ tunnel_interface_network | ipaddr('prefix') }}"
 ovs_cidr_mappings: "{% if neutron_bridge_name.split(',') | length != 1 %} {neutron_bridge_name.split(',')[0]}:{{ tunnel_interface_cidr }} {% else %} {{ neutron_bridge_name }}:{{ tunnel_interface_cidr }} {% endif %}"
diff --git a/doc/source/reference/networking/neutron.rst b/doc/source/reference/networking/neutron.rst
index c748c6d4e43d6eaaadd10c125806522285268e4f..8a627572d755817301defeea4a816df59e8ead3a 100644
--- a/doc/source/reference/networking/neutron.rst
+++ b/doc/source/reference/networking/neutron.rst
@@ -20,13 +20,20 @@ Neutron external interface is used for communication with the external world,
 for example provider networks, routers and floating IPs.
 For setting up the neutron external interface modify
 ``/etc/kolla/globals.yml`` setting ``neutron_external_interface`` to the
-desired interface name. This interface is used by hosts in the ``network``
-group. It is also used by hosts in the ``compute`` group if
+desired interface name or comma-separated list of interface names. Its default
+value is ``eth1``. These external interfaces are used by hosts in the
+``network`` group.  They are also used by hosts in the ``compute`` group if
 ``enable_neutron_provider_networks`` is set or DVR is enabled.
 
-The interface is plugged into a bridge (Open vSwitch or Linux Bridge, depending
-on the driver) defined by ``neutron_bridge_name``, which defaults to ``br-ex``.
-The default Neutron physical network is ``physnet1``.
+The external interfaces are each plugged into a bridge (Open vSwitch or Linux
+Bridge, depending on the driver) defined by ``neutron_bridge_name``, which
+defaults to ``br-ex``. When there are multiple external interfaces,
+``neutron_bridge_name`` should be a comma-separated list of the same length.
+
+The default Neutron physical network is ``physnet1``, or ``physnet1`` to
+``physnetN`` when there are multiple external network interfaces. This may be
+changed by setting ``neutron_physical_networks`` to a comma-separated list of
+networks of the same length.
 
 Example: single interface
 -------------------------
@@ -54,6 +61,30 @@ These two lists are "zipped" together, such that ``eth1`` is plugged into the
 Ansible maps these interfaces to Neutron physical networks ``physnet1`` and
 ``physnet2`` respectively.
 
+Example: custom physical networks
+---------------------------------
+
+Sometimes we may want to customise the physical network names used. This may be
+to allow for not all hosts having access to all physical networks, or to use
+more descriptive names.
+
+For example, in an environment with a separate physical network for Ironic
+provisioning, controllers might have access to two physical networks:
+
+.. code-block:: yaml
+
+   neutron_external_interface: "eth1,eth2"
+   neutron_bridge_name: "br-ex1,br-ex2"
+   neutron_physical_network: "physnet1,physnet2"
+
+While compute nodes have access only to ``physnet2``.
+
+.. code-block:: yaml
+
+   neutron_external_interface: "eth1"
+   neutron_bridge_name: "br-ex1"
+   neutron_physical_network: "physnet2"
+
 Example: shared interface
 -------------------------
 
diff --git a/releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml b/releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml
new file mode 100644
index 0000000000000000000000000000000000000000..a62e400089de43d9effee076e97e11c7ec57d54c
--- /dev/null
+++ b/releasenotes/notes/neutron-physical-networks-5b908bed9809c3b4.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    Adds a ``neutron_physical_networks`` variable for customising Neutron
+    physical network names. The default behaviour of using ``physnet1`` to
+    ``physnetN`` is unchanged.