diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in
index f089f0e52..388e7c5d2 100644
--- a/interface-definitions/dns-dynamic.xml.in
+++ b/interface-definitions/dns-dynamic.xml.in
@@ -1,208 +1,212 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dns">
         <properties>
           <help>Domain Name System (DNS) related services</help>
         </properties>
         <children>
           <node name="dynamic" owner="${vyos_conf_scripts_dir}/dns_dynamic.py">
             <properties>
               <help>Dynamic DNS</help>
             </properties>
             <children>
               <tagNode name="name">
                 <properties>
                   <help>Dynamic DNS configuration</help>
                   <valueHelp>
                     <format>txt</format>
                     <description>Dynamic DNS service name</description>
                   </valueHelp>
+                  <constraint>
+                    #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+                  </constraint>
+                  <constraintErrorMessage>Dynamic DNS service name must be alphanumeric and can contain hyphens and underscores</constraintErrorMessage>
                 </properties>
                 <children>
                   #include <include/generic-description.xml.i>
                   <leafNode name="protocol">
                     <properties>
                       <help>ddclient protocol used for Dynamic DNS service</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script>
                       </completionHelp>
                       <constraint>
                         <validator name="ddclient-protocol"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="address">
                     <properties>
                       <help>Obtain IP address to send Dynamic DNS update for</help>
                       <valueHelp>
                         <format>txt</format>
                         <description>Use interface to obtain the IP address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>web</format>
                         <description>Use HTTP(S) web request to obtain the IP address</description>
                       </valueHelp>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_interfaces</script>
                         <list>web</list>
                       </completionHelp>
                       <constraint>
                         #include <include/constraint/interface-name.xml.i>
                         <regex>web</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <node name="web-options">
                     <properties>
                       <help>Options when using HTTP(S) web request to obtain the IP address</help>
                     </properties>
                     <children>
                       #include <include/url-http-https.xml.i>
                       <leafNode name="skip">
                         <properties>
                           <help>Pattern to skip from the HTTP(S) respose</help>
                           <valueHelp>
                             <format>txt</format>
                             <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description>
                           </valueHelp>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <leafNode name="ip-version">
                     <properties>
                       <help>IP address version to use</help>
                       <valueHelp>
                         <format>_ipv4</format>
                         <description>Use only IPv4 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>_ipv6</format>
                         <description>Use only IPv6 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>both</format>
                         <description>Use both IPv4 and IPv6 address</description>
                       </valueHelp>
                       <completionHelp>
                         <list>ipv4 ipv6 both</list>
                       </completionHelp>
                       <constraint>
                         <regex>(ipv[46]|both)</regex>
                       </constraint>
                       <constraintErrorMessage>IP Version must be literal 'ipv4', 'ipv6' or 'both'</constraintErrorMessage>
                     </properties>
                     <defaultValue>ipv4</defaultValue>
                   </leafNode>
                   <leafNode name="host-name">
                     <properties>
                       <help>Hostname to register with Dynamic DNS service</help>
                       <constraint>
                           #include <include/constraint/host-name.xml.i>
                           <regex>(\@|\*)[-.A-Za-z0-9]*</regex>
                       </constraint>
                       <constraintErrorMessage>Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*'</constraintErrorMessage>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="server">
                     <properties>
                       <help>Remote Dynamic DNS server to send updates to</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IPv4 address of the remote server</description>
                       </valueHelp>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of the remote server</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>Fully qualified domain name of the remote server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ip-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                       <constraintErrorMessage>Remote server must be IP address or fully qualified domain name</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="zone">
                     <properties>
                       <help>DNS zone to be updated</help>
                       <valueHelp>
                         <format>txt</format>
                         <description>Name of DNS zone</description>
                       </valueHelp>
                       <constraint>
                         <validator name="fqdn"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/generic-username.xml.i>
                   #include <include/generic-password.xml.i>
                   <leafNode name="key">
                     <properties>
                       <help>File containing TSIG authentication key for RFC2136 nsupdate on remote DNS server</help>
                       <valueHelp>
                         <format>filename</format>
                         <description>File in /config/auth directory</description>
                       </valueHelp>
                       <constraint>
                         <validator name="file-path" argument="--strict --parent-dir /config/auth"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/dns/time-to-live.xml.i>
                   <leafNode name="wait-time">
                     <properties>
                       <help>Time in seconds to wait between update attempts</help>
                       <valueHelp>
                         <format>u32:60-86400</format>
                         <description>Time in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 60-86400"/>
                       </constraint>
                       <constraintErrorMessage>Wait time must be between 60 and 86400 seconds</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="expiry-time">
                     <properties>
                       <help>Time in seconds for the hostname to be marked expired in cache</help>
                       <valueHelp>
                         <format>u32:300-2160000</format>
                         <description>Time in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 300-2160000"/>
                       </constraint>
                       <constraintErrorMessage>Expiry time must be between 300 and 2160000 seconds</constraintErrorMessage>
                     </properties>
                   </leafNode>
                 </children>
               </tagNode>
               <leafNode name="interval">
                 <properties>
                   <help>Interval in seconds to wait between Dynamic DNS updates</help>
                   <valueHelp>
                     <format>u32:60-3600</format>
                     <description>Time in seconds</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 60-3600"/>
                   </constraint>
                   <constraintErrorMessage>Interval must be between 60 and 3600 seconds</constraintErrorMessage>
                 </properties>
                 <defaultValue>300</defaultValue>
               </leafNode>
               #include <include/interface/vrf.xml.i>
             </children>
           </node>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/src/migration-scripts/dns-dynamic/0-to-1 b/src/migration-scripts/dns-dynamic/0-to-1
index d80e8d44a..4f6083eab 100755
--- a/src/migration-scripts/dns-dynamic/0-to-1
+++ b/src/migration-scripts/dns-dynamic/0-to-1
@@ -1,104 +1,117 @@
 #!/usr/bin/env python3
 
 # Copyright (C) 2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # T5144:
 # - migrate "service dns dynamic interface ..."
 #        to "service dns dynamic address ..."
 # - migrate "service dns dynamic interface <interface> use-web ..."
 #        to "service dns dynamic address <address> web-options ..."
 # - migrate "service dns dynamic interface <interface> rfc2136 <config> record ..."
 #        to "service dns dynamic address <address> rfc2136 <config> host-name ..."
 # - migrate "service dns dynamic interface <interface> service <config> login ..."
 #        to "service dns dynamic address <address> service <config> username ..."
 # - apply global 'ipv6-enable' to per <config> 'ip-version: ipv6'
 # - apply service protocol mapping upfront, they are not 'auto-detected' anymore
 
 import sys
 from vyos.configtree import ConfigTree
 
 service_protocol_mapping = {
     'afraid': 'freedns',
     'changeip': 'changeip',
     'cloudflare': 'cloudflare',
     'dnspark': 'dnspark',
     'dslreports': 'dslreports1',
     'dyndns': 'dyndns2',
     'easydns': 'easydns',
     'namecheap': 'namecheap',
     'noip': 'noip',
     'sitelutions': 'sitelutions',
     'zoneedit': 'zoneedit1'
 }
 
 if len(sys.argv) < 2:
     print("Must specify file name!")
     sys.exit(1)
 
 file_name = sys.argv[1]
 
 with open(file_name, 'r') as f:
     config_file = f.read()
 
 config = ConfigTree(config_file)
 
 old_base_path = ['service', 'dns', 'dynamic', 'interface']
 new_base_path = ['service', 'dns', 'dynamic', 'address']
 
 if not config.exists(old_base_path):
     # Nothing to do
     sys.exit(0)
 
 # Migrate "service dns dynamic interface"
 #      to "service dns dynamic address"
 config.rename(old_base_path, new_base_path[-1])
 
 for address in config.list_nodes(new_base_path):
     # Migrate "service dns dynamic interface <interface> rfc2136 <config> record"
     #      to "service dns dynamic address <address> rfc2136 <config> host-name"
     if config.exists(new_base_path + [address, 'rfc2136']):
         for rfc_cfg in config.list_nodes(new_base_path + [address, 'rfc2136']):
             if config.exists(new_base_path + [address, 'rfc2136', rfc_cfg, 'record']):
                 config.rename(new_base_path + [address, 'rfc2136', rfc_cfg, 'record'], 'host-name')
 
     # Migrate "service dns dynamic interface <interface> service <config> login"
     #      to "service dns dynamic address <address> service <config> username"
     if config.exists(new_base_path + [address, 'service']):
         for svc_cfg in config.list_nodes(new_base_path + [address, 'service']):
             if config.exists(new_base_path + [address, 'service', svc_cfg, 'login']):
                 config.rename(new_base_path + [address, 'service', svc_cfg, 'login'], 'username')
             # Apply global 'ipv6-enable' to per <config> 'ip-version: ipv6'
             if config.exists(new_base_path + [address, 'ipv6-enable']):
-                config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'],
-                           value='ipv6', replace=False)
+                config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'], 'ipv6')
                 config.delete(new_base_path + [address, 'ipv6-enable'])
             # Apply service protocol mapping upfront, they are not 'auto-detected' anymore
             if svc_cfg in service_protocol_mapping:
                 config.set(new_base_path + [address, 'service', svc_cfg, 'protocol'],
-                           value=service_protocol_mapping.get(svc_cfg), replace=False)
+                           service_protocol_mapping.get(svc_cfg))
 
-    # Migrate "service dns dynamic interface <interface> use-web"
-    #      to "service dns dynamic address <address> web-options"
-    # Also, rename <address> to 'web' literal for backward compatibility
+    # If use-web is set, then:
+    #   Move "service dns dynamic address <address> <service|rfc2136> <service> ..."
+    #     to "service dns dynamic address web <service|rfc2136> <service>-<address> ..."
+    #   Move "service dns dynamic address web use-web ..."
+    #     to "service dns dynamic address web web-options ..."
+    # Note: The config is named <service>-<address> to avoid name conflict with old entries
     if config.exists(new_base_path + [address, 'use-web']):
-        config.rename(new_base_path + [address], 'web')
-        config.rename(new_base_path + ['web', 'use-web'], 'web-options')
+        for svc_type in ['rfc2136', 'service']:
+            if config.exists(new_base_path + [address, svc_type]):
+                config.set(new_base_path + ['web', svc_type])
+                config.set_tag(new_base_path + ['web', svc_type])
+                for svc_cfg in config.list_nodes(new_base_path + [address, svc_type]):
+                    config.copy(new_base_path + [address, svc_type, svc_cfg],
+                                new_base_path + ['web', svc_type, f'{svc_cfg}-{address}'])
+
+        # Multiple web-options were not supported, so copy only the first one
+        if not config.exists(new_base_path + ['web', 'web-options']):
+            config.copy(new_base_path + [address, 'use-web'], new_base_path + ['web', 'web-options'])
+
+        config.delete(new_base_path + [address])
 
 try:
     with open(file_name, 'w') as f:
         f.write(config.to_string())
 except OSError as e:
     print("Failed to save the modified config: {}".format(e))
     sys.exit(1)
diff --git a/src/migration-scripts/dns-dynamic/2-to-3 b/src/migration-scripts/dns-dynamic/2-to-3
index 187c2a895..e5910f7b4 100755
--- a/src/migration-scripts/dns-dynamic/2-to-3
+++ b/src/migration-scripts/dns-dynamic/2-to-3
@@ -1,88 +1,119 @@
 #!/usr/bin/env python3
 
 # Copyright (C) 2023 VyOS maintainers and contributors
 #
 # This program is free software; you can redistribute it and/or modify
 # it under the terms of the GNU General Public License version 2 or later as
 # published by the Free Software Foundation.
 #
 # This program is distributed in the hope that it will be useful,
 # but WITHOUT ANY WARRANTY; without even the implied warranty of
 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 # GNU General Public License for more details.
 #
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
 # T5791:
 # - migrate "service dns dynamic address web web-options ..."
 #        to "service dns dynamic name <service> address web ..." (per service)
 # - migrate "service dns dynamic address <address> rfc2136 <service> ..."
 #        to "service dns dynamic name <service> address <interface> protocol 'nsupdate'"
 # - migrate "service dns dynamic address <interface> service <service> ..."
 #        to "service dns dynamic name <service> address <interface> ..."
+# - normalize the all service names to conform with name constraints
 
 import sys
+import re
+from unicodedata import normalize
 from vyos.configtree import ConfigTree
 
+def normalize_name(name):
+    """Normalize service names to conform with name constraints.
+
+    This is necessary as part of migration because there were no constraints in
+    the old name format.
+    """
+    # Normalize unicode characters to ASCII (NFKD)
+    # Replace all separators with hypens, strip leading and trailing hyphens
+    name = normalize('NFKD', name).encode('ascii', 'ignore').decode()
+    name = re.sub(r'(\s|\W)+', '-', name).strip('-')
+
+    return name
+
+
 if len(sys.argv) < 2:
     print("Must specify file name!")
     sys.exit(1)
 
 file_name = sys.argv[1]
 
 with open(file_name, 'r') as f:
     config_file = f.read()
 
 config = ConfigTree(config_file)
 
 base_path = ['service', 'dns', 'dynamic']
 address_path = base_path + ['address']
 name_path = base_path + ['name']
 
 if not config.exists(address_path):
     # Nothing to do
     sys.exit(0)
 
 # config.copy does not recursively create a path, so initialize the name path as tagged node
 if not config.exists(name_path):
     config.set(name_path)
     config.set_tag(name_path)
 
 for address in config.list_nodes(address_path):
 
     address_path_tag = address_path + [address]
 
     # Move web-option as a configuration in each service instead of top level web-option
     if config.exists(address_path_tag + ['web-options']) and address == 'web':
         for svc_type in ['service', 'rfc2136']:
             if config.exists(address_path_tag + [svc_type]):
                 for svc_cfg in config.list_nodes(address_path_tag + [svc_type]):
                     config.copy(address_path_tag + ['web-options'],
                                 address_path_tag + [svc_type, svc_cfg, 'web-options'])
         config.delete(address_path_tag + ['web-options'])
 
     for svc_type in ['service', 'rfc2136']:
         if config.exists(address_path_tag + [svc_type]):
-            # Move RFC2136 as service configuration, rename to avoid name conflict and set protocol to 'nsupdate'
+            # Set protocol to 'nsupdate' for RFC2136 configuration
             if svc_type == 'rfc2136':
-                for rfc_cfg_old in config.list_nodes(address_path_tag + ['rfc2136']):
-                    rfc_cfg_new = f'{rfc_cfg_old}-rfc2136'
-                    config.rename(address_path_tag + ['rfc2136', rfc_cfg_old], rfc_cfg_new)
-                    config.set(address_path_tag + ['rfc2136', rfc_cfg_new, 'protocol'], 'nsupdate')
+                for rfc_cfg in config.list_nodes(address_path_tag + ['rfc2136']):
+                    config.set(address_path_tag + ['rfc2136', rfc_cfg, 'protocol'], 'nsupdate')
 
             # Add address as config value in each service before moving the service path
-            # And then copy the services from 'address <interface> service <service>' to 'name <service>'
+            # And then copy the services from 'address <interface> service <service>'
+            #                              to 'name (service|rfc2136)-<service>-<address>'
+            # Note: The new service is named (service|rfc2136)-<service>-<address>
+            #       to avoid name conflict with old entries
             for svc_cfg in config.list_nodes(address_path_tag + [svc_type]):
                 config.set(address_path_tag + [svc_type, svc_cfg, 'address'], address)
-                config.copy(address_path_tag + [svc_type, svc_cfg], name_path + [svc_cfg])
+                config.copy(address_path_tag + [svc_type, svc_cfg],
+                            name_path + ['-'.join([svc_type, svc_cfg, address])])
 
 # Finally cleanup the old address path
 config.delete(address_path)
 
+# Normalize the all service names to conform with name constraints
+index = 1
+for name in config.list_nodes(name_path):
+    new_name = normalize_name(name)
+    if new_name != name:
+        # Append index if there is still a name conflicts after normalization
+        # For example, "foo-?(" and "foo-!)" both normalize to "foo-"
+        if config.exists(name_path + [new_name]):
+            new_name = f'{new_name}-{index}'
+            index += 1
+        config.rename(name_path + [name], new_name)
+
 try:
     with open(file_name, 'w') as f:
         f.write(config.to_string())
 except OSError as e:
     print("Failed to save the modified config: {}".format(e))
     sys.exit(1)