diff --git a/interface-definitions/include/accel-ppp/client-ip-pool.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i
index dff574e6c..71fe69f8d 100644
--- a/interface-definitions/include/accel-ppp/client-ip-pool.xml.i
+++ b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i
@@ -1,46 +1,46 @@
 <!-- include start from accel-ppp/client-ip-pool.xml.i -->
 <tagNode name="client-ip-pool">
   <properties>
     <help>Client IP pool</help>
     <valueHelp>
       <format>txt</format>
       <description>Name of IP pool</description>
     </valueHelp>
     <constraint>
-      <regex>[-_a-zA-Z0-9.]+</regex>
+      #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
     </constraint>
   </properties>
   <children>
     <leafNode name="range">
       <properties>
         <help>Range of IP addresses</help>
         <valueHelp>
           <format>ipv4net</format>
           <description>IPv4 prefix</description>
         </valueHelp>
         <valueHelp>
           <format>ipv4range</format>
           <description>IPv4 address range inside /24 network</description>
         </valueHelp>
         <constraint>
           <validator name="ipv4-prefix"/>
           <validator name="ipv4-host"/>
           <validator name="ipv4-range-mask"  argument="-m 24 -r"/>
         </constraint>
       </properties>
     </leafNode>
     <leafNode name="next-pool">
       <properties>
         <help>Next pool name</help>
         <valueHelp>
           <format>txt</format>
           <description>Name of IP pool</description>
         </valueHelp>
         <constraint>
-          <regex>[-_a-zA-Z0-9.]+</regex>
+          #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
         </constraint>
       </properties>
     </leafNode>
   </children>
 </tagNode>
 <!-- include end -->
diff --git a/interface-definitions/include/accel-ppp/default-pool.xml.i b/interface-definitions/include/accel-ppp/default-pool.xml.i
index 832594c12..a08b066b1 100644
--- a/interface-definitions/include/accel-ppp/default-pool.xml.i
+++ b/interface-definitions/include/accel-ppp/default-pool.xml.i
@@ -1,14 +1,14 @@
 <!-- include start from accel-ppp/default-pool.xml.i -->
 <leafNode name="default-pool">
   <properties>
     <help>Default client IP pool name</help>
     <valueHelp>
       <format>txt</format>
       <description>Default IP pool</description>
     </valueHelp>
     <constraint>
-      <regex>[-_a-zA-Z0-9.]+</regex>
+      #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
     </constraint>
   </properties>
 </leafNode>
 <!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i b/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
index c218937c8..388991241 100644
--- a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
+++ b/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
@@ -1,34 +1,34 @@
 <!-- include start from bgp/afi-route-map.xml.i -->
 <leafNode name="export">
   <properties>
     <help>Route-map to filter outgoing route updates</help>
     <completionHelp>
       <path>policy route-map</path>
     </completionHelp>
     <valueHelp>
       <format>txt</format>
       <description>Route map name</description>
     </valueHelp>
     <constraint>
-      <regex>[-_a-zA-Z0-9.]+</regex>
+      #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
     </constraint>
     <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
   </properties>
 </leafNode>
 <leafNode name="import">
   <properties>
     <help>Route-map to filter incoming route updates</help>
     <completionHelp>
       <path>policy route-map</path>
     </completionHelp>
     <valueHelp>
       <format>txt</format>
       <description>Route map name</description>
     </valueHelp>
     <constraint>
-      <regex>[-_a-zA-Z0-9.]+</regex>
+      #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
     </constraint>
     <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
   </properties>
 </leafNode>
 <!-- include end -->
diff --git a/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i
index 9ec513da9..c8ad68700 100644
--- a/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i
+++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i
@@ -1,205 +1,205 @@
 <!-- include start from bgp/neighbor-afi-ipv4-ipv6-common.xml.i -->
 
 <leafNode name="addpath-tx-all">
   <properties>
     <help>Use addpath to advertise all paths to a neighbor</help>
     <valueless/>
   </properties>
 </leafNode>
 <leafNode name="addpath-tx-per-as">
   <properties>
     <help>Use addpath to advertise the bestpath per each neighboring AS</help>
     <valueless/>
   </properties>
 </leafNode>
 <node name="conditionally-advertise">
   <properties>
     <help>Use route-map to conditionally advertise routes</help>
   </properties>
   <children>
     <leafNode name="advertise-map">
       <properties>
         <help>Route-map to conditionally advertise routes</help>
         <completionHelp>
           <path>policy route-map</path>
         </completionHelp>
         <valueHelp>
           <format>txt</format>
           <description>Route map name</description>
         </valueHelp>
         <constraint>
-          <regex>[-_a-zA-Z0-9.]+</regex>
+          #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
         </constraint>
         <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
       </properties>
     </leafNode>
     <leafNode name="exist-map">
       <properties>
         <help>Advertise routes only if prefixes in exist-map are installed in BGP table</help>
         <completionHelp>
           <path>policy route-map</path>
         </completionHelp>
         <valueHelp>
           <format>txt</format>
           <description>Route map name</description>
         </valueHelp>
         <constraint>
-          <regex>[-_a-zA-Z0-9.]+</regex>
+          #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
         </constraint>
         <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
       </properties>
     </leafNode>
     <leafNode name="non-exist-map">
       <properties>
         <help>Advertise routes only if prefixes in non-exist-map are not installed in BGP table</help>
         <completionHelp>
           <path>policy route-map</path>
         </completionHelp>
         <valueHelp>
           <format>txt</format>
           <description>Route map name</description>
         </valueHelp>
         <constraint>
-          <regex>[-_a-zA-Z0-9.]+</regex>
+          #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
         </constraint>
         <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
       </properties>
     </leafNode>
   </children>
 </node>
 #include <include/bgp/afi-allowas-in.xml.i>
 <leafNode name="as-override">
   <properties>
     <help>Override ASN in outbound updates to configured neighbor local-as</help>
     <valueless/>
   </properties>
 </leafNode>
 #include <include/bgp/afi-attribute-unchanged.xml.i>
 <node name="disable-send-community">
   <properties>
     <help>Disable sending community attributes to this peer</help>
   </properties>
   <children>
     <leafNode name="extended">
       <properties>
         <help>Disable sending extended community attributes to this peer</help>
         <valueless/>
       </properties>
     </leafNode>
     <leafNode name="standard">
       <properties>
         <help>Disable sending standard community attributes to this peer</help>
         <valueless/>
       </properties>
     </leafNode>
   </children>
 </node>
 <node name="distribute-list">
   <properties>
     <help>Access-list to filter route updates to/from this peer-group</help>
   </properties>
   <children>
     <leafNode name="export">
       <properties>
         <help>Access-list to filter outgoing route updates to this peer-group</help>
         <completionHelp>
           <path>policy access-list</path>
         </completionHelp>
         <valueHelp>
           <format>u32:1-65535</format>
           <description>Access-list to filter outgoing route updates to this peer-group</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 1-65535"/>
         </constraint>
       </properties>
     </leafNode>
     <leafNode name="import">
       <properties>
         <help>Access-list to filter incoming route updates from this peer-group</help>
         <completionHelp>
           <path>policy access-list</path>
         </completionHelp>
         <valueHelp>
           <format>u32:1-65535</format>
           <description>Access-list to filter incoming route updates from this peer-group</description>
         </valueHelp>
         <constraint>
           <validator name="numeric" argument="--range 1-65535"/>
         </constraint>
       </properties>
     </leafNode>
   </children>
 </node>
 #include <include/bgp/afi-filter-list.xml.i>
 <leafNode name="maximum-prefix">
   <properties>
     <help>Maximum number of prefixes to accept from this peer</help>
     <valueHelp>
       <format>u32:1-4294967295</format>
       <description>Prefix limit</description>
     </valueHelp>
     <constraint>
       <validator name="numeric" argument="--range 1-4294967295"/>
     </constraint>
   </properties>
 </leafNode>
 <leafNode name="maximum-prefix-out">
   <properties>
     <help>Maximum number of prefixes to be sent to this peer</help>
     <valueHelp>
       <format>u32:1-4294967295</format>
       <description>Prefix limit</description>
     </valueHelp>
     <constraint>
       <validator name="numeric" argument="--range 1-4294967295"/>
     </constraint>
   </properties>
 </leafNode>
 #include <include/bgp/afi-nexthop-self.xml.i>
 <node name="remove-private-as">
   <properties>
     <help>Remove private AS numbers from AS path in outbound route updates</help>
   </properties>
   <children>
     <leafNode name="all">
       <properties>
         <help>Remove private AS numbers to all AS numbers in outbound route updates</help>
         <valueless/>
       </properties>
     </leafNode>
   </children>
 </node>
 #include <include/bgp/afi-route-map.xml.i>
 #include <include/bgp/afi-route-reflector-client.xml.i>
 #include <include/bgp/afi-route-server-client.xml.i>
 #include <include/bgp/afi-soft-reconfiguration.xml.i>
 <leafNode name="unsuppress-map">
   <properties>
     <help>Route-map to selectively unsuppress suppressed routes</help>
     <completionHelp>
       <path>policy route-map</path>
     </completionHelp>
     <valueHelp>
       <format>txt</format>
       <description>Route map name</description>
     </valueHelp>
     <constraint>
-      <regex>[-_a-zA-Z0-9.]+</regex>
+      #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
     </constraint>
     <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
   </properties>
 </leafNode>
 <leafNode name="weight">
   <properties>
     <help>Default weight for routes from this peer</help>
     <valueHelp>
       <format>u32:1-65535</format>
       <description>Default weight</description>
     </valueHelp>
     <constraint>
       <validator name="numeric" argument="--range 1-65535"/>
     </constraint>
   </properties>
 </leafNode>
 <!-- include end -->
diff --git a/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i b/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i
new file mode 100644
index 000000000..7aeb85260
--- /dev/null
+++ b/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from constraint/alpha-numeric-hyphen-underscore-dot.xml.i -->
+<regex>[-_a-zA-Z0-9.]+</regex>
+<!-- include end -->
diff --git a/interface-definitions/include/route-map.xml.i b/interface-definitions/include/route-map.xml.i
index 019868373..e49c388d6 100644
--- a/interface-definitions/include/route-map.xml.i
+++ b/interface-definitions/include/route-map.xml.i
@@ -1,18 +1,18 @@
 <!-- include start from route-map.xml.i -->
 <leafNode name="route-map">
   <properties>
     <help>Specify route-map name to use</help>
     <completionHelp>
       <path>policy route-map</path>
     </completionHelp>
     <valueHelp>
       <format>txt</format>
       <description>Route map name</description>
     </valueHelp>
     <constraint>
-      <regex>[-_a-zA-Z0-9.]+</regex>
+      #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
     </constraint>
     <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
   </properties>
 </leafNode>
 <!-- include end -->
diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in
index 3449819be..097c541ac 100644
--- a/interface-definitions/pki.xml.in
+++ b/interface-definitions/pki.xml.in
@@ -1,234 +1,243 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="pki" owner="${vyos_conf_scripts_dir}/pki.py">
     <properties>
       <help>VyOS PKI configuration</help>
       <priority>300</priority>
     </properties>
     <children>
       <tagNode name="ca">
         <properties>
           <help>Certificate Authority</help>
+          <constraint>
+            #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+          </constraint>
         </properties>
         <children>
           <leafNode name="certificate">
             <properties>
               <help>CA certificate in PEM format</help>
               <constraint>
                 <validator name="base64"/>
               </constraint>
               <constraintErrorMessage>CA certificate is not base64-encoded</constraintErrorMessage>
             </properties>
           </leafNode>
           #include <include/generic-description.xml.i>
           <node name="private">
             <properties>
               <help>CA private key in PEM format</help>
             </properties>
             <children>
               <leafNode name="key">
                 <properties>
                   <help>CA private key in PEM format</help>
                   <constraint>
                     <validator name="base64"/>
                   </constraint>
                   <constraintErrorMessage>CA private key is not base64-encoded</constraintErrorMessage>
                 </properties>
               </leafNode>
               <leafNode name="password-protected">
                 <properties>
                   <help>CA private key is password protected</help>
                   <valueless/>
                 </properties>
               </leafNode>
             </children>
           </node>
           <leafNode name="crl">
             <properties>
               <help>Certificate revocation list in PEM format</help>
               <constraint>
                 <validator name="base64"/>
               </constraint>
               <constraintErrorMessage>CRL is not base64-encoded</constraintErrorMessage>
               <multi/>
             </properties>
           </leafNode>
           <leafNode name="revoke">
             <properties>
               <help>If parent CA is present, this CA certificate will be included in generated CRLs</help>
               <valueless/>
             </properties>
           </leafNode>
         </children>
       </tagNode>
       <tagNode name="certificate">
         <properties>
           <help>Certificate</help>
+          <constraint>
+            #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+          </constraint>
         </properties>
         <children>
           <leafNode name="certificate">
             <properties>
               <help>Certificate in PEM format</help>
               <constraint>
                 <validator name="base64"/>
               </constraint>
               <constraintErrorMessage>Certificate is not base64-encoded</constraintErrorMessage>
             </properties>
           </leafNode>
           #include <include/generic-description.xml.i>
           <node name="private">
             <properties>
               <help>Certificate private key</help>
             </properties>
             <children>
               <leafNode name="key">
                 <properties>
                   <help>Certificate private key in PEM format</help>
                   <constraint>
                     <validator name="base64"/>
                   </constraint>
                   <constraintErrorMessage>Certificate private key is not base64-encoded</constraintErrorMessage>
                 </properties>
               </leafNode>
               <leafNode name="password-protected">
                 <properties>
                   <help>Certificate private key is password protected</help>
                   <valueless/>
                 </properties>
               </leafNode>
             </children>
           </node>
           <leafNode name="revoke">
             <properties>
               <help>If CA is present, this certificate will be included in generated CRLs</help>
               <valueless/>
             </properties>
           </leafNode>
         </children>
       </tagNode>
       <tagNode name="dh">
         <properties>
           <help>Diffie-Hellman parameters</help>
+          <constraint>
+            #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+          </constraint>
         </properties>
         <children>
           <leafNode name="parameters">
             <properties>
               <help>DH parameters in PEM format</help>
               <constraint>
                 <validator name="base64"/>
               </constraint>
               <constraintErrorMessage>DH parameters are not base64-encoded</constraintErrorMessage>
             </properties>
           </leafNode>
         </children>
       </tagNode>
       <tagNode name="key-pair">
         <properties>
           <help>Public and private keys</help>
         </properties>
         <children>
           <node name="public">
             <properties>
               <help>Public key</help>
             </properties>
             <children>
               <leafNode name="key">
                 <properties>
                   <help>Public key in PEM format</help>
                   <constraint>
                     <validator name="base64"/>
                   </constraint>
                   <constraintErrorMessage>Public key is not base64-encoded</constraintErrorMessage>
                 </properties>
               </leafNode>
             </children>
           </node>
           <node name="private">
             <properties>
               <help>Private key</help>
             </properties>
             <children>
               <leafNode name="key">
                 <properties>
                   <help>Private key in PEM format</help>
                   <constraint>
                     <validator name="base64"/>
                   </constraint>
                   <constraintErrorMessage>Private key is not base64-encoded</constraintErrorMessage>
                 </properties>
               </leafNode>
               <leafNode name="password-protected">
                 <properties>
                   <help>Private key is password protected</help>
                   <valueless/>
                 </properties>
               </leafNode>
             </children>
           </node>
         </children>
       </tagNode>
       <node name="openvpn">
         <properties>
           <help>OpenVPN keys</help>
         </properties>
         <children>
           <tagNode name="shared-secret">
             <properties>
               <help>OpenVPN shared secret key</help>
             </properties>
             <children>
               <leafNode name="key">
                 <properties>
                   <help>OpenVPN shared secret key data</help>
                 </properties>
               </leafNode>
               <leafNode name="version">
                 <properties>
                   <help>OpenVPN shared secret key version</help>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </node>
       <node name="x509">
         <properties>
           <help>X509 Settings</help>
         </properties>
         <children>
           <node name="default">
             <properties>
               <help>X509 Default Values</help>
             </properties>
             <children>
               <leafNode name="country">
                 <properties>
                   <help>Default country</help>
                 </properties>
                 <defaultValue>GB</defaultValue>
               </leafNode>
               <leafNode name="state">
                 <properties>
                   <help>Default state</help>
                 </properties>
                 <defaultValue>Some-State</defaultValue>
               </leafNode>
               <leafNode name="locality">
                 <properties>
                   <help>Default locality</help>
                 </properties>
                 <defaultValue>Some-City</defaultValue>
               </leafNode>
               <leafNode name="organization">
                 <properties>
                   <help>Default organization</help>
                 </properties>
                 <defaultValue>VyOS</defaultValue>
               </leafNode>
             </children>
           </node>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index 0d2ed9746..0d82cd3f8 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -1,1570 +1,1570 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="policy" owner="${vyos_conf_scripts_dir}/policy.py">
     <properties>
       <priority>200</priority>
       <help>Routing policy</help>
     </properties>
     <children>
       <tagNode name="access-list">
         <properties>
           <help>IP access-list filter</help>
           <valueHelp>
             <format>u32:1-99</format>
             <description>IP standard access list</description>
           </valueHelp>
           <valueHelp>
             <format>u32:100-199</format>
             <description>IP extended access list</description>
           </valueHelp>
           <valueHelp>
             <format>u32:1300-1999</format>
             <description>IP standard access list (expanded range)</description>
           </valueHelp>
           <valueHelp>
             <format>u32:2000-2699</format>
             <description>IP extended access list (expanded range)</description>
           </valueHelp>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this access-list</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Access-list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <node name="destination">
                 <properties>
                   <help>Destination network or address</help>
                 </properties>
                 <children>
                   <leafNode name="any">
                     <properties>
                       <help>Any IP address to match</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   #include <include/policy/host.xml.i>
                   #include <include/policy/inverse-mask.xml.i>
                   #include <include/policy/network.xml.i>
                 </children>
               </node>
               <node name="source">
                 <properties>
                   <help>Source network or address to match</help>
                 </properties>
                 <children>
                   <leafNode name="any">
                     <properties>
                       <help>Any IP address to match</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   #include <include/policy/host.xml.i>
                   #include <include/policy/inverse-mask.xml.i>
                   #include <include/policy/network.xml.i>
                 </children>
               </node>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="access-list6">
         <properties>
           <help>IPv6 access-list filter</help>
           <valueHelp>
             <format>txt</format>
             <description>Name of IPv6 access-list</description>
           </valueHelp>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this access-list6</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Access-list6 rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <node name="source">
                 <properties>
                   <help>Source IPv6 network to match</help>
                 </properties>
                 <children>
                   <leafNode name="any">
                     <properties>
                       <help>Any IP address to match</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   <leafNode name="exact-match">
                     <properties>
                       <help>Exact match of the network prefixes</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   <leafNode name="network">
                     <properties>
                       <help>Network/netmask to match</help>
                       <valueHelp>
                         <format>ipv6net</format>
                         <description>IPv6 address and prefix length</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-prefix"/>
                       </constraint>
                     </properties>
                   </leafNode>
                 </children>
               </node>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="as-path-list">
         <properties>
           <help>Add a BGP autonomous system path filter</help>
           <valueHelp>
             <format>txt</format>
             <description>AS path list name</description>
           </valueHelp>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this as-path-list</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>AS path list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="regex">
                 <properties>
                   <help>Regular expression to match against an AS path</help>
                   <valueHelp>
                     <format>txt</format>
                     <description>AS path regular expression (ex: "64501 64502")</description>
                   </valueHelp>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="community-list">
         <properties>
           <help>Add a BGP community list entry</help>
           <valueHelp>
             <format>txt</format>
             <description>BGP community-list name</description>
           </valueHelp>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this BGP community list</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Community-list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="regex">
                 <properties>
                   <help>Regular expression to match against a community-list</help>
                   <completionHelp>
                     <list>local-AS no-advertise no-export internet additive</list>
                   </completionHelp>
                   <valueHelp>
                     <format>&lt;aa:nn&gt;</format>
                     <description>Community number in AA:NN format</description>
                   </valueHelp>
                   <valueHelp>
                     <format>local-AS</format>
                     <description>Well-known communities value NO_EXPORT_SUBCONFED 0xFFFFFF03</description>
                   </valueHelp>
                   <valueHelp>
                     <format>no-advertise</format>
                     <description>Well-known communities value NO_ADVERTISE 0xFFFFFF02</description>
                   </valueHelp>
                   <valueHelp>
                     <format>no-export</format>
                     <description>Well-known communities value NO_EXPORT 0xFFFFFF01</description>
                   </valueHelp>
                   <valueHelp>
                     <format>internet</format>
                     <description>Well-known communities value 0</description>
                   </valueHelp>
                   <valueHelp>
                     <format>additive</format>
                     <description>New value is appended to the existing value</description>
                   </valueHelp>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="extcommunity-list">
         <properties>
           <help>Add a BGP extended community list entry</help>
           <valueHelp>
             <format>txt</format>
             <description>BGP extended community-list name</description>
           </valueHelp>
           <constraint>
             #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
           </constraint>
           <constraintErrorMessage>Should be an alphanumeric name</constraintErrorMessage>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this BGP extended community list</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Extended community-list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="regex">
                 <properties>
                   <help>Regular expression to match against an extended community list</help>
                   <valueHelp>
                     <format>&lt;aa:nn:nn&gt;</format>
                     <description>Extended community list regular expression</description>
                   </valueHelp>
                   <valueHelp>
                     <format>&lt;rt aa:nn:nn&gt;</format>
                     <description>Route Target regular expression</description>
                   </valueHelp>
                   <valueHelp>
                     <format>&lt;soo aa:nn:nn&gt;</format>
                     <description>Site of Origin regular expression</description>
                   </valueHelp>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="large-community-list">
         <properties>
           <help>Add a BGP large community list entry</help>
           <valueHelp>
             <format>txt</format>
             <description>BGP large-community-list name</description>
           </valueHelp>
           <constraint>
             #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
           </constraint>
           <constraintErrorMessage>Should be an alphanumeric name</constraintErrorMessage>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this BGP extended community list</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Large community-list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="regex">
                 <properties>
                   <help>Regular expression to match against a large community list</help>
                   <valueHelp>
                     <format>ASN:NN:NN</format>
                     <description>BGP large-community-list filter</description>
                   </valueHelp>
                   <valueHelp>
                     <format>IP:NN:NN</format>
                     <description>BGP large-community-list filter (IPv4 address format)</description>
                   </valueHelp>
                   <constraint>
                     <validator name="bgp-large-community-list"/>
                   </constraint>
                   <constraintErrorMessage>Malformed large-community-list</constraintErrorMessage>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="prefix-list">
         <properties>
           <help>IP prefix-list filter</help>
           <valueHelp>
             <format>txt</format>
             <description>Name of IPv4 prefix-list</description>
           </valueHelp>
           <constraint>
             #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
           </constraint>
           <constraintErrorMessage>Name of prefix-list can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this prefix-list</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Prefix-list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="ge">
                 <properties>
                   <help>Prefix length to match a netmask greater than or equal to it</help>
                   <valueHelp>
                     <format>u32:0-32</format>
                     <description>Netmask greater than length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 0-32"/>
                   </constraint>
                 </properties>
               </leafNode>
               <leafNode name="le">
                 <properties>
                   <help>Prefix length to match a netmask less than or equal to it</help>
                   <valueHelp>
                     <format>u32:0-32</format>
                     <description>Netmask less than length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 0-32"/>
                   </constraint>
                 </properties>
               </leafNode>
               <leafNode name="prefix">
                 <properties>
                   <help>Prefix to match</help>
                   <valueHelp>
                     <format>ipv4net</format>
                     <description>Prefix to match against</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv4-prefix"/>
                   </constraint>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="prefix-list6">
         <properties>
           <help>IPv6 prefix-list filter</help>
           <valueHelp>
             <format>txt</format>
             <description>Name of IPv6 prefix-list</description>
           </valueHelp>
           <constraint>
             #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
           </constraint>
           <constraintErrorMessage>Name of prefix-list6 can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this prefix-list6</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Prefix-list rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="ge">
                 <properties>
                   <help>Prefix length to match a netmask greater than or equal to it</help>
                   <valueHelp>
                     <format>u32:0-128</format>
                     <description>Netmask greater than length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 0-128"/>
                   </constraint>
                 </properties>
               </leafNode>
               <leafNode name="le">
                 <properties>
                   <help>Prefix length to match a netmask less than or equal to it</help>
                   <valueHelp>
                     <format>u32:0-128</format>
                     <description>Netmask less than length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="numeric" argument="--range 0-128"/>
                   </constraint>
                 </properties>
               </leafNode>
               <leafNode name="prefix">
                 <properties>
                   <help>Prefix to match</help>
                   <valueHelp>
                     <format>ipv6net</format>
                     <description>IPv6 prefix</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv6-prefix"/>
                   </constraint>
                 </properties>
               </leafNode>
             </children>
           </tagNode>
         </children>
       </tagNode>
       <tagNode name="route-map">
         <properties>
           <help>IP route-map</help>
           <valueHelp>
             <format>txt</format>
             <description>Route map name</description>
           </valueHelp>
           <constraint>
-            <regex>[-_a-zA-Z0-9.]+</regex>
+            #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
           </constraint>
           <constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
         </properties>
         <children>
           #include <include/generic-description.xml.i>
           <tagNode name="rule">
             <properties>
               <help>Rule for this route-map</help>
               <valueHelp>
                 <format>u32:1-65535</format>
                 <description>Route-map rule number</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 1-65535"/>
               </constraint>
             </properties>
             <children>
               #include <include/policy/action.xml.i>
               <leafNode name="call">
                 <properties>
                   <help>Call another route-map on match</help>
                   <valueHelp>
                     <format>txt</format>
                     <description>Route map name</description>
                   </valueHelp>
                   <completionHelp>
                     <path>policy route-map</path>
                   </completionHelp>
                 </properties>
               </leafNode>
               <leafNode name="continue">
                 <properties>
                   <help>Jump to a different rule in this route-map on a match</help>
                   <valueHelp>
                     <format>u32:1-65535</format>
                     <description>Rule number</description>
                   </valueHelp>
                 </properties>
               </leafNode>
               #include <include/generic-description.xml.i>
               <node name="match">
                 <properties>
                   <help>Route parameters to match</help>
                 </properties>
                 <children>
                   <leafNode name="as-path">
                     <properties>
                       <help>BGP as-path-list to match</help>
                       <completionHelp>
                         <path>policy as-path-list</path>
                       </completionHelp>
                     </properties>
                   </leafNode>
                   <node name="community">
                     <properties>
                       <help>BGP community-list to match</help>
                     </properties>
                     <children>
                       <leafNode name="community-list">
                         <properties>
                           <help>BGP community-list to match</help>
                           <completionHelp>
                             <path>policy community-list</path>
                           </completionHelp>
                         </properties>
                       </leafNode>
                       <leafNode name="exact-match">
                         <properties>
                           <help>Community-list to exactly match</help>
                           <valueless/>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <node name="evpn">
                     <properties>
                       <help>Ethernet Virtual Private Network</help>
                     </properties>
                     <children>
                       <leafNode name="default-route">
                         <properties>
                           <help>Default EVPN type-5 route</help>
                           <valueless/>
                         </properties>
                       </leafNode>
                       #include <include/bgp/route-distinguisher.xml.i>
                       <leafNode name="route-type">
                         <properties>
                           <help>Match route-type</help>
                           <completionHelp>
                             <list>macip multicast prefix</list>
                           </completionHelp>
                           <valueHelp>
                             <format>macip</format>
                             <description>mac-ip route</description>
                           </valueHelp>
                           <valueHelp>
                             <format>multicast</format>
                             <description>IMET route</description>
                           </valueHelp>
                           <valueHelp>
                             <format>prefix</format>
                             <description>Prefix route</description>
                           </valueHelp>
                           <constraint>
                             <regex>(macip|multicast|prefix)</regex>
                           </constraint>
                         </properties>
                       </leafNode>
                       #include <include/vni.xml.i>
                     </children>
                   </node>
                   <leafNode name="extcommunity">
                     <properties>
                       <help>BGP extended community to match</help>
                       <completionHelp>
                         <path>policy extcommunity-list</path>
                       </completionHelp>
                     </properties>
                   </leafNode>
                   #include <include/generic-interface.xml.i>
                   <node name="ip">
                     <properties>
                       <help>IP prefix parameters to match</help>
                     </properties>
                     <children>
                       <node name="address">
                         <properties>
                           <help>IP address of route to match</help>
                         </properties>
                         <children>
                           <leafNode name="access-list">
                             <properties>
                               <help>IP access-list to match</help>
                               <valueHelp>
                                 <format>u32:1-99</format>
                                 <description>IP standard access list</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:100-199</format>
                                 <description>IP extended access list</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:1300-1999</format>
                                 <description>IP standard access list (expanded range)</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:2000-2699</format>
                                 <description>IP extended access list (expanded range)</description>
                               </valueHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-list">
                             <properties>
                               <help>IP prefix-list to match</help>
                               <completionHelp>
                                 <path>policy prefix-list</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-len">
                             <properties>
                               <help>IP prefix-length to match (can be used for kernel routes only)</help>
                               <valueHelp>
                                 <format>u32:0-32</format>
                                 <description>Prefix length</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 0-32"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                 <!--  T3304 but it overwrite node nexthop
                       <leafNode name="nexthop">
                         <properties>
                           <help>IP next-hop of route to match</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>Next-hop IPv4 router address</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>   -->
                       <node name="nexthop">
                         <properties>
                           <help>IP next-hop of route to match</help>
                         </properties>
                         <children>
                           <leafNode name="address">
                             <properties>
                               <help>IP address to match</help>
                               <valueHelp>
                                 <format>ipv4</format>
                                 <description>Nexthop IP address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv4-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                           <leafNode name="access-list">
                             <properties>
                               <help>IP access-list to match</help>
                               <valueHelp>
                                 <format>u32:1-99</format>
                                 <description>IP standard access list</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:100-199</format>
                                 <description>IP extended access list</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:1300-1999</format>
                                 <description>IP standard access list (expanded range)</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:2000-2699</format>
                                 <description>IP extended access list (expanded range)</description>
                               </valueHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-len">
                             <properties>
                               <help>IP prefix-length to match</help>
                               <valueHelp>
                                 <format>u32:0-32</format>
                                 <description>Prefix length</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 0-32"/>
                               </constraint>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-list">
                             <properties>
                               <help>IP prefix-list to match</help>
                               <completionHelp>
                                 <path>policy prefix-list</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="type">
                             <properties>
                               <help>Match type</help>
                               <completionHelp>
                                 <list>blackhole</list>
                               </completionHelp>
                               <valueHelp>
                                 <format>blackhole</format>
                                 <description>Blackhole</description>
                               </valueHelp>
                               <constraint>
                                 <regex>(blackhole)</regex>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                       <node name="route-source">
                         <properties>
                           <help>Match advertising source address of route</help>
                         </properties>
                         <children>
                           <leafNode name="access-list">
                             <properties>
                               <help>IP access-list to match</help>
                               <valueHelp>
                                 <format>u32:1-99</format>
                                 <description>IP standard access list</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:100-199</format>
                                 <description>IP extended access list</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:1300-1999</format>
                                 <description>IP standard access list (expanded range)</description>
                               </valueHelp>
                               <valueHelp>
                                 <format>u32:2000-2699</format>
                                 <description>IP extended access list (expanded range)</description>
                               </valueHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-list">
                             <properties>
                               <help>IP prefix-list to match</help>
                               <completionHelp>
                                 <path>policy prefix-list</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <node name="ipv6">
                     <properties>
                       <help>IPv6 prefix parameters to match</help>
                     </properties>
                     <children>
                       <node name="address">
                         <properties>
                           <help>IPv6 address of route to match</help>
                         </properties>
                         <children>
                           <leafNode name="access-list">
                             <properties>
                               <help>IPv6 access-list to match</help>
                               <valueHelp>
                                 <format>txt</format>
                                 <description>IPV6 access list name</description>
                               </valueHelp>
                               <completionHelp>
                                 <path>policy access-list6</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-list">
                             <properties>
                               <help>IPv6 prefix-list to match</help>
                               <completionHelp>
                                 <path>policy prefix-list6</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-len">
                             <properties>
                               <help>IPv6 prefix-length to match (can be used for kernel routes only)</help>
                               <valueHelp>
                                 <format>u32:0-128</format>
                                 <description>Prefix length</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 0-128"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                 <!--  T3976 but it overwrite node nexthop
                       <leafNode name="nexthop">
                         <properties>
                           <help>IPv6 next-hop of route to match</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>Nexthop IPv6 address</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </node> -->
                       <node name="nexthop">
                         <properties>
                           <help>IPv6 next-hop of route to match</help>
                         </properties>
                         <children>
                           <leafNode name="address">
                             <properties>
                               <help>IPv6 address of next-hop</help>
                               <valueHelp>
                                 <format>ipv6</format>
                                 <description>Nexthop IPv6 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv6-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                           <leafNode name="access-list">
                             <properties>
                               <help>IPv6 access-list to match</help>
                               <valueHelp>
                                 <format>txt</format>
                                 <description>IPV6 access list name</description>
                               </valueHelp>
                               <completionHelp>
                                 <path>policy access-list6</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="prefix-list">
                             <properties>
                               <help>IPv6 prefix-list to match</help>
                               <completionHelp>
                                 <path>policy prefix-list6</path>
                               </completionHelp>
                             </properties>
                           </leafNode>
                           <leafNode name="type">
                             <properties>
                               <help>Match type</help>
                               <completionHelp>
                                 <list>blackhole</list>
                               </completionHelp>
                               <valueHelp>
                                 <format>blackhole</format>
                                 <description>Blackhole</description>
                               </valueHelp>
                               <constraint>
                                 <regex>(blackhole)</regex>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <node name="large-community">
                     <properties>
                       <help>Match BGP large communities</help>
                     </properties>
                     <children>
                       <leafNode name="large-community-list">
                         <properties>
                           <help>BGP large-community-list to match</help>
                           <completionHelp>
                             <path>policy large-community-list</path>
                           </completionHelp>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <leafNode name="local-preference">
                     <properties>
                       <help>Local Preference</help>
                       <valueHelp>
                         <format>u32:0-4294967295</format>
                         <description>Local Preference</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="metric">
                     <properties>
                       <help>Metric of route to match</help>
                       <valueHelp>
                         <format>u32:1-65535</format>
                         <description>Route metric</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-65535"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="origin">
                     <properties>
                       <help>BGP origin code to match</help>
                       <completionHelp>
                         <list>egp igp incomplete</list>
                       </completionHelp>
                       <valueHelp>
                         <format>egp</format>
                         <description>Exterior gateway protocol origin</description>
                       </valueHelp>
                       <valueHelp>
                         <format>igp</format>
                         <description>Interior gateway protocol origin</description>
                       </valueHelp>
                       <valueHelp>
                         <format>incomplete</format>
                         <description>Incomplete origin</description>
                       </valueHelp>
                       <constraint>
                         <regex>(egp|igp|incomplete)</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="peer">
                     <properties>
                       <help>Peer address to match</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Peer IP address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>Peer IPv6 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ip-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="protocol">
                     <properties>
                       <help>Match protocol via which the route was learnt</help>
                       <completionHelp>
                         <list>babel bgp connected isis kernel ospf ospfv3 rip ripng static table vnc</list>
                       </completionHelp>
                       <valueHelp>
                         <format>babel</format>
                         <description>Babel routing protocol (Babel)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>bgp</format>
                         <description>Border Gateway Protocol (BGP)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>connected</format>
                         <description>Connected routes (directly attached subnet or host)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>isis</format>
                         <description>Intermediate System to Intermediate System (IS-IS)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>kernel</format>
                         <description>Kernel routes</description>
                       </valueHelp>
                       <valueHelp>
                         <format>ospf</format>
                         <description>Open Shortest Path First (OSPFv2)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>ospfv3</format>
                         <description>Open Shortest Path First (IPv6) (OSPFv3)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>rip</format>
                         <description>Routing Information Protocol (RIP)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>ripng</format>
                         <description>Routing Information Protocol next-generation (IPv6) (RIPng)</description>
                       </valueHelp>
                       <valueHelp>
                         <format>static</format>
                         <description>Statically configured routes</description>
                       </valueHelp>
                       <valueHelp>
                         <format>table</format>
                         <description>Non-main Kernel Routing Table</description>
                       </valueHelp>
                       <valueHelp>
                         <format>vnc</format>
                         <description>Virtual Network Control (VNC)</description>
                       </valueHelp>
                       <constraint>
                         <regex>(babel|bgp|connected|isis|kernel|ospf|ospfv3|rip|ripng|static|table|vnc)</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="rpki">
                     <properties>
                       <help>Match RPKI validation result</help>
                       <completionHelp>
                         <list>invalid notfound valid</list>
                       </completionHelp>
                       <valueHelp>
                         <format>invalid</format>
                         <description>Match invalid entries</description>
                       </valueHelp>
                       <valueHelp>
                         <format>notfound</format>
                         <description>Match notfound entries</description>
                       </valueHelp>
                       <valueHelp>
                         <format>valid</format>
                         <description>Match valid entries</description>
                       </valueHelp>
                       <constraint>
                         <regex>(invalid|notfound|valid)</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/policy/tag.xml.i>
                 </children>
               </node>
               <node name="on-match">
                 <properties>
                   <help>Exit policy on matches</help>
                 </properties>
                 <children>
                   <leafNode name="goto">
                     <properties>
                       <help>Rule number to goto on match</help>
                       <valueHelp>
                         <format>u32:1-65535</format>
                         <description>Rule number</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-65535"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="next">
                     <properties>
                       <help>Next sequence number to goto on match</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                 </children>
               </node>
               <node name="set">
                 <properties>
                   <help>Route parameters</help>
                 </properties>
                 <children>
                   <node name="aggregator">
                     <properties>
                       <help>BGP aggregator attribute</help>
                     </properties>
                     <children>
                       <leafNode name="as">
                         <properties>
                           <help>AS number of an aggregation</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>Rule number</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="ip">
                         <properties>
                           <help>IP address of an aggregation</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IP address</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <node name="as-path">
                     <properties>
                       <help>Transform BGP AS_PATH attribute</help>
                     </properties>
                     <children>
                       <leafNode name="exclude">
                         <properties>
                           <help>Remove/exclude from the as-path attribute</help>
                           <valueHelp>
                             <format>u32</format>
                             <description>AS number</description>
                           </valueHelp>
                           <constraint>
                             <validator name="as-number-list"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="prepend">
                         <properties>
                           <help>Prepend to the as-path</help>
                           <valueHelp>
                             <format>u32</format>
                             <description>AS number</description>
                           </valueHelp>
                           <constraint>
                             <validator name="as-number-list"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="prepend-last-as">
                         <properties>
                           <help>Use the last AS-number in the as-path</help>
                           <valueHelp>
                             <format>u32:1-10</format>
                             <description>Number of times to insert</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-10"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <leafNode name="atomic-aggregate">
                     <properties>
                       <help>BGP atomic aggregate attribute</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   <node name="community">
                     <properties>
                       <help>BGP community attribute</help>
                     </properties>
                     <children>
                       <leafNode name="add">
                         <properties>
                           <help>Add communities to a prefix</help>
                           #include <include/policy/community-value-list.xml.i>
                         </properties>
                       </leafNode>
                       <leafNode name="replace">
                         <properties>
                           <help>Set communities for a prefix</help>
                           #include <include/policy/community-value-list.xml.i>
                         </properties>
                       </leafNode>
                       #include <include/policy/community-clear.xml.i>
                       <leafNode name="delete">
                         <properties>
                           <help>Remove communities defined in a list from a prefix</help>
                           <completionHelp>
                             <path>policy community-list</path>
                           </completionHelp>
                           <valueHelp>
                             <description>Community-list</description>
                             <format>txt</format>
                           </valueHelp>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <node name="large-community">
                     <properties>
                       <help>BGP large community attribute</help>
                     </properties>
                     <children>
                       <leafNode name="add">
                         <properties>
                           <help>Add large communities to a prefix ;</help>
                           #include <include/policy/large-community-value-list.xml.i>
                         </properties>
                       </leafNode>
                       <leafNode name="replace">
                         <properties>
                           <help>Set large communities for a prefix</help>
                           #include <include/policy/large-community-value-list.xml.i>
                         </properties>
                       </leafNode>
                       #include <include/policy/community-clear.xml.i>
                       <leafNode name="delete">
                         <properties>
                           <help>Remove communities defined in a list from a prefix</help>
                           <completionHelp>
                             <path>policy large-community-list</path>
                           </completionHelp>
                           <valueHelp>
                             <description>Community-list</description>
                             <format>txt</format>
                           </valueHelp>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <node name="extcommunity">
                     <properties>
                       <help>BGP extended community attribute</help>
                     </properties>
                     <children>
                       <leafNode name="bandwidth">
                         <properties>
                           <help>Bandwidth value in Mbps</help>
                           <completionHelp>
                             <list>cumulative num-multipaths</list>
                           </completionHelp>
                           <valueHelp>
                             <format>u32:1-25600</format>
                             <description>Bandwidth value in Mbps</description>
                           </valueHelp>
                           <valueHelp>
                             <format>cumulative</format>
                             <description>Cumulative bandwidth of all multipaths (outbound-only)</description>
                           </valueHelp>
                           <valueHelp>
                             <format>num-multipaths</format>
                             <description>Internally computed bandwidth based on number of multipaths (outbound-only)</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-25600"/>
                             <regex>(cumulative|num-multipaths)</regex>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="bandwidth-non-transitive">
                         <properties>
                           <help>The link bandwidth extended community is encoded as non-transitive</help>
                           <valueless/>
                         </properties>
                       </leafNode>
                       <leafNode name="rt">
                         <properties>
                           <help>Set route target value</help>
                           #include <include/policy/extended-community-value-list.xml.i>
                         </properties>
                       </leafNode>
                       <leafNode name="soo">
                         <properties>
                           <help>Set Site of Origin value</help>
                           #include <include/policy/extended-community-value-list.xml.i>
                         </properties>
                       </leafNode>
                       #include <include/policy/community-clear.xml.i>
                     </children>
                   </node>
                   <leafNode name="distance">
                     <properties>
                       <help>Locally significant administrative distance</help>
                       <valueHelp>
                         <format>u32:0-255</format>
                         <description>Distance value</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-255"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <node name="evpn">
                     <properties>
                       <help>Ethernet Virtual Private Network</help>
                     </properties>
                     <children>
                       <node name="gateway">
                         <properties>
                           <help>Set gateway IP for prefix advertisement route</help>
                         </properties>
                         <children>
                           <leafNode name="ipv4">
                             <properties>
                               <help>Set gateway IPv4 address</help>
                               <valueHelp>
                                 <format>ipv4</format>
                                 <description>Gateway IPv4 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv4-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                           <leafNode name="ipv6">
                             <properties>
                               <help>Set gateway IPv6 address</help>
                               <valueHelp>
                                 <format>ipv6</format>
                                 <description>Gateway IPv6 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv6-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <leafNode name="ip-next-hop">
                     <properties>
                       <help>Nexthop IP address</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_local_ips.sh --ipv4</script>
                         <list>unchanged peer-address</list>
                       </completionHelp>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IP address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>unchanged</format>
                         <description>Set the BGP nexthop address as unchanged</description>
                       </valueHelp>
                       <valueHelp>
                         <format>peer-address</format>
                         <description>Set the BGP nexthop address to the address of the peer</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                         <regex>(unchanged|peer-address)</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <node name="ipv6-next-hop">
                     <properties>
                       <help>Nexthop IPv6 address</help>
                     </properties>
                     <children>
                       <leafNode name="global">
                         <properties>
                           <help>Nexthop IPv6 global address</help>
                           <completionHelp>
                             <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script>
                           </completionHelp>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address and prefix length</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="local">
                         <properties>
                           <help>Nexthop IPv6 local address</help>
                           <completionHelp>
                             <script>${vyos_completion_dir}/list_local_ips.sh --ipv6</script>
                           </completionHelp>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address and prefix length</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="peer-address">
                         <properties>
                           <help>Use peer address (for BGP only)</help>
                           <valueless/>
                         </properties>
                       </leafNode>
                       <leafNode name="prefer-global">
                         <properties>
                           <help>Prefer global address as the nexthop</help>
                           <valueless/>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   <node name="l3vpn-nexthop">
                     <properties>
                       <help>Next hop Information</help>
                     </properties>
                     <children>
                       <node name="encapsulation">
                         <properties>
                           <help>Encapsulation options (for BGP only)</help>
                         </properties>
                         <children>
                           <leafNode name="gre">
                             <properties>
                               <help>Accept L3VPN traffic over GRE encapsulation</help>
                               <valueless/>
                             </properties>
                           </leafNode>
                         </children>
                        </node>
                     </children>
                   </node>
                   <leafNode name="local-preference">
                     <properties>
                       <help>BGP local preference attribute</help>
                       <valueHelp>
                         <format>u32:0-4294967295</format>
                         <description>Local preference value</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="metric">
                     <properties>
                       <help>Destination routing protocol metric</help>
                       <valueHelp>
                         <format>&lt;+/-metric&gt;</format>
                         <description>Add or subtract metric</description>
                       </valueHelp>
                       <valueHelp>
                         <format>u32:0-4294967295</format>
                         <description>Metric value</description>
                       </valueHelp>
                       <valueHelp>
                         <format>&lt;+/-rtt&gt;</format>
                         <description>Add or subtract round trip time</description>
                       </valueHelp>
                       <valueHelp>
                         <format>&lt;rtt&gt;</format>
                         <description>Round trip time</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--relative --"/>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                         <regex>^[+|-]?rtt$</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="metric-type">
                     <properties>
                       <help>Open Shortest Path First (OSPF) external metric-type</help>
                       <completionHelp>
                         <list>type-1 type-2</list>
                       </completionHelp>
                       <valueHelp>
                         <format>type-1</format>
                         <description>OSPF external type 1 metric</description>
                       </valueHelp>
                       <valueHelp>
                         <format>type-2</format>
                         <description>OSPF external type 2 metric</description>
                       </valueHelp>
                       <constraint>
                         <regex>(type-1|type-2)</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="origin">
                     <properties>
                       <help>Border Gateway Protocl (BGP) origin code</help>
                       <completionHelp>
                         <list>igp egp incomplete</list>
                       </completionHelp>
                       <valueHelp>
                         <format>igp</format>
                         <description>Interior gateway protocol origin</description>
                       </valueHelp>
                       <valueHelp>
                         <format>egp</format>
                         <description>Exterior gateway protocol origin</description>
                       </valueHelp>
                       <valueHelp>
                         <format>incomplete</format>
                         <description>Incomplete origin</description>
                       </valueHelp>
                       <constraint>
                         <regex>(igp|egp|incomplete)</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="originator-id">
                     <properties>
                       <help>BGP originator ID attribute</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Orignator IP address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="src">
                     <properties>
                       <help>Source address for route</help>
                       <completionHelp>
                         <script>${vyos_completion_dir}/list_local_ips.sh --both</script>
                       </completionHelp>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IPv4 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ip-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="table">
                     <properties>
                       <help>Set prefixes to table</help>
                       <valueHelp>
                         <format>u32:1-200</format>
                         <description>Table value</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-200"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/policy/tag.xml.i>
                   <leafNode name="weight">
                     <properties>
                       <help>BGP weight attribute</help>
                       <valueHelp>
                         <format>u32:0-4294967295</format>
                         <description>BGP weight</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                     </properties>
                   </leafNode>
                 </children>
               </node>
             </children>
           </tagNode>
         </children>
       </tagNode>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index e35d845f1..8e13f9372 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -1,456 +1,456 @@
 <?xml version="1.0"?>
 <!-- DHCP server configuration -->
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dhcp-server" owner="${vyos_conf_scripts_dir}/service_dhcp-server.py">
         <properties>
           <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help>
           <priority>911</priority>
         </properties>
         <children>
           #include <include/generic-disable-node.xml.i>
           <leafNode name="dynamic-dns-update">
             <properties>
               <help>Dynamically update Domain Name System (RFC4702)</help>
               <valueless/>
             </properties>
           </leafNode>
           <node name="failover">
             <properties>
               <help>DHCP failover configuration</help>
             </properties>
             <children>
               #include <include/source-address-ipv4.xml.i>
               <leafNode name="remote">
                 <properties>
                   <help>IPv4 remote address used for connectio</help>
                   <valueHelp>
                     <format>ipv4</format>
                     <description>IPv4 address of failover peer</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv4-address"/>
                   </constraint>
                 </properties>
               </leafNode>
               <leafNode name="name">
                 <properties>
                   <help>Peer name used to identify connection</help>
                   <constraint>
-                    <regex>[-_a-zA-Z0-9.]+</regex>
+                    #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                   </constraint>
                   <constraintErrorMessage>Invalid failover peer name. May only contain letters, numbers and .-_</constraintErrorMessage>
                 </properties>
               </leafNode>
               <leafNode name="status">
                 <properties>
                   <help>Failover hierarchy</help>
                   <completionHelp>
                     <list>primary secondary</list>
                   </completionHelp>
                   <valueHelp>
                     <format>primary</format>
                     <description>Configure this server to be the primary node</description>
                   </valueHelp>
                   <valueHelp>
                     <format>secondary</format>
                     <description>Configure this server to be the secondary node</description>
                   </valueHelp>
                   <constraint>
                     <regex>(primary|secondary)</regex>
                   </constraint>
                   <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage>
                 </properties>
               </leafNode>
               #include <include/pki/ca-certificate.xml.i>
               #include <include/pki/certificate.xml.i>
             </children>
           </node>
           <leafNode name="hostfile-update">
             <properties>
               <help>Updating /etc/hosts file (per client lease)</help>
               <valueless/>
             </properties>
           </leafNode>
           #include <include/listen-address-ipv4.xml.i>
           <tagNode name="shared-network-name">
             <properties>
               <help>Name of DHCP shared network</help>
               <constraint>
-                <regex>[-_a-zA-Z0-9.]+</regex>
+                #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
               </constraint>
               <constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
             </properties>
             <children>
               <leafNode name="authoritative">
                 <properties>
                   <help>Option to make DHCP server authoritative for this physical network</help>
                   <valueless/>
                 </properties>
               </leafNode>
               #include <include/dhcp/domain-name.xml.i>
               #include <include/dhcp/domain-search.xml.i>
               #include <include/dhcp/ntp-server.xml.i>
               #include <include/generic-description.xml.i>
               #include <include/generic-disable-node.xml.i>
               #include <include/name-server-ipv4.xml.i>
               <tagNode name="subnet">
                 <properties>
                   <help>DHCP subnet for shared network</help>
                   <valueHelp>
                     <format>ipv4net</format>
                     <description>IPv4 address and prefix length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv4-prefix"/>
                   </constraint>
                   <constraintErrorMessage>Invalid IPv4 subnet definition</constraintErrorMessage>
                 </properties>
                 <children>
                   <leafNode name="bootfile-name">
                     <properties>
                       <help>Bootstrap file name</help>
                       <constraint>
                         <regex>[[:ascii:]]{1,253}</regex>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="bootfile-server">
                     <properties>
                       <help>Server from which the initial boot file is to be loaded</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Bootfile server IPv4 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>Bootfile server FQDN</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="bootfile-size">
                     <properties>
                       <help>Bootstrap file size</help>
                       <valueHelp>
                         <format>u32:1-16</format>
                         <description>Bootstrap file size in 512 byte blocks</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-16"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/dhcp/captive-portal.xml.i>
                   <leafNode name="client-prefix-length">
                     <properties>
                       <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help>
                       <valueHelp>
                         <format>u32:0-32</format>
                         <description>DHCP client prefix length must be 0 to 32</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-32"/>
                       </constraint>
                       <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="default-router">
                     <properties>
                       <help>IP address of default router</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Default router IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/dhcp/domain-name.xml.i>
                   #include <include/dhcp/domain-search.xml.i>
                   #include <include/generic-description.xml.i>
                   #include <include/name-server-ipv4.xml.i>
                   <leafNode name="exclude">
                     <properties>
                       <help>IP address to exclude from DHCP lease range</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>IPv4 address to exclude from lease range</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="ip-forwarding">
                     <properties>
                       <help>Enable IP forwarding on client</help>
                       <valueless/>
                     </properties>
                   </leafNode>
                   <leafNode name="lease">
                     <properties>
                       <help>Lease timeout in seconds</help>
                       <valueHelp>
                         <format>u32</format>
                         <description>DHCP lease time in seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                       <constraintErrorMessage>DHCP lease time must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
                     </properties>
                     <defaultValue>86400</defaultValue>
                   </leafNode>
                   #include <include/dhcp/ntp-server.xml.i>
                   <leafNode name="pop-server">
                     <properties>
                       <help>IP address of POP3 server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>POP3 server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="server-identifier">
                     <properties>
                       <help>Address for DHCP server identifier</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>DHCP server identifier IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="smtp-server">
                     <properties>
                       <help>IP address of SMTP server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>SMTP server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <tagNode name="range">
                     <properties>
                       <help>DHCP lease range</help>
                       <constraint>
-                        <regex>[-_a-zA-Z0-9.]+</regex>
+                        #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                       </constraint>
                       <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage>
                     </properties>
                     <children>
                       <leafNode name="start">
                         <properties>
                           <help>First IP address for DHCP lease range</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 start address of pool</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="stop">
                         <properties>
                           <help>Last IP address for DHCP lease range</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 end address of pool</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
                   <tagNode name="static-mapping">
                     <properties>
                       <help>Hostname for static mapping reservation</help>
                       <constraint>
                         <validator name="fqdn"/>
                       </constraint>
                       <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage>
                     </properties>
                     <children>
                       #include <include/generic-disable-node.xml.i>
                       <leafNode name="ip-address">
                         <properties>
                           <help>Fixed IP address of static mapping</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 address used in static mapping</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv4-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       #include <include/interface/mac.xml.i>
                       #include <include/interface/duid.xml.i>
                     </children>
                   </tagNode>
                   <tagNode name="static-route">
                     <properties>
                       <help>Classless static route destination subnet</help>
                       <valueHelp>
                         <format>ipv4net</format>
                         <description>IPv4 address and prefix length</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-prefix"/>
                       </constraint>
                     </properties>
                     <children>
                       <leafNode name="next-hop">
                         <properties>
                           <help>IP address of router to be used to reach the destination subnet</help>
                           <valueHelp>
                             <format>ipv4</format>
                             <description>IPv4 address of router</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ip-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode >
                   <leafNode name="ipv6-only-preferred">
                     <properties>
                       <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help>
                       <valueHelp>
                         <format>u32</format>
                         <description>Seconds</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 0-4294967295"/>
                       </constraint>
                       <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="tftp-server-name">
                     <properties>
                       <help>TFTP server name</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>TFTP server IPv4 address</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>TFTP server FQDN</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <leafNode name="time-offset">
                     <properties>
                       <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help>
                       <valueHelp>
                         <format>[-]N</format>
                         <description>Time offset (number, may be negative)</description>
                       </valueHelp>
                       <constraint>
                         <regex>-?[0-9]+</regex>
                       </constraint>
                       <constraintErrorMessage>Invalid time offset value</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="time-server">
                     <properties>
                       <help>IP address of time server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>Time server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="time-zone">
                     <properties>
                       <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help>
                       <completionHelp>
                         <script>timedatectl list-timezones</script>
                       </completionHelp>
                       <constraint>
                         <validator name="timezone" argument="--validate"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   <node name="vendor-option">
                     <properties>
                       <help>Vendor Specific Options</help>
                     </properties>
                     <children>
                       <node name="ubiquiti">
                         <properties>
                           <help>Ubiquiti specific parameters</help>
                         </properties>
                         <children>
                           <leafNode name="unifi-controller">
                             <properties>
                               <help>Address of UniFi controller</help>
                               <valueHelp>
                                 <format>ipv4</format>
                                 <description>IP address of UniFi controller</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv4-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                   <leafNode name="wins-server">
                     <properties>
                       <help>IP address for Windows Internet Name Service (WINS) server</help>
                       <valueHelp>
                         <format>ipv4</format>
                         <description>WINS server IPv4 address</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv4-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="wpad-url">
                     <properties>
                       <help>Web Proxy Autodiscovery (WPAD) URL</help>
                     </properties>
                   </leafNode>
                 </children>
               </tagNode>
             </children>
           </tagNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/interface-definitions/service_dhcpv6-server.xml.in b/interface-definitions/service_dhcpv6-server.xml.in
index 102c164a6..6f7f3c1da 100644
--- a/interface-definitions/service_dhcpv6-server.xml.in
+++ b/interface-definitions/service_dhcpv6-server.xml.in
@@ -1,375 +1,375 @@
 <?xml version="1.0"?>
 <interfaceDefinition>
   <node name="service">
     <children>
       <node name="dhcpv6-server" owner="${vyos_conf_scripts_dir}/service_dhcpv6-server.py">
         <properties>
           <help>DHCP for IPv6 (DHCPv6) server</help>
           <priority>900</priority>
         </properties>
         <children>
           #include <include/generic-disable-node.xml.i>
           <node name="global-parameters">
             <properties>
               <help>Additional global parameters for DHCPv6 server</help>
             </properties>
             <children>
               #include <include/name-server-ipv6.xml.i>
             </children>
           </node>
           <leafNode name="preference">
             <properties>
               <help>Preference of this DHCPv6 server compared with others</help>
               <valueHelp>
                 <format>u32:0-255</format>
                 <description>DHCPv6 server preference (0-255)</description>
               </valueHelp>
               <constraint>
                 <validator name="numeric" argument="--range 0-255"/>
               </constraint>
               <constraintErrorMessage>Preference must be between 0 and 255</constraintErrorMessage>
             </properties>
           </leafNode>
           <tagNode name="shared-network-name">
             <properties>
               <help>DHCPv6 shared network name</help>
               <constraint>
-                <regex>[-_a-zA-Z0-9.]+</regex>
+                #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
               </constraint>
               <constraintErrorMessage>Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
             </properties>
             <children>
               #include <include/generic-disable-node.xml.i>
               #include <include/generic-description.xml.i>
               <leafNode name="interface">
                 <properties>
                   <help>Optional interface for this shared network to accept requests from</help>
                   <completionHelp>
                     <script>${vyos_completion_dir}/list_interfaces</script>
                   </completionHelp>
                   <valueHelp>
                     <format>txt</format>
                     <description>Interface name</description>
                   </valueHelp>
                   <constraint>
                     #include <include/constraint/interface-name.xml.i>
                   </constraint>
                 </properties>
               </leafNode>
               <node name="common-options">
                 <properties>
                   <help>Common options to distribute to all clients, including stateless clients</help>
                 </properties>
                 <children>
                   <leafNode name="info-refresh-time">
                     <properties>
                       <help>Time (in seconds) that stateless clients should wait between refreshing the information they were given</help>
                       <valueHelp>
                         <format>u32:1-4294967295</format>
                         <description>DHCPv6 information refresh time</description>
                       </valueHelp>
                       <constraint>
                         <validator name="numeric" argument="--range 1-4294967295"/>
                       </constraint>
                     </properties>
                   </leafNode>
                   #include <include/dhcp/domain-search.xml.i>
                   #include <include/name-server-ipv6.xml.i>
                 </children>
               </node>
               <tagNode name="subnet">
                 <properties>
                   <help>IPv6 DHCP subnet for this shared network</help>
                   <valueHelp>
                     <format>ipv6net</format>
                     <description>IPv6 address and prefix length</description>
                   </valueHelp>
                   <constraint>
                     <validator name="ipv6-prefix"/>
                   </constraint>
                 </properties>
                 <children>
                   <node name="address-range">
                     <properties>
                       <help>Parameters setting ranges for assigning IPv6 addresses</help>
                     </properties>
                     <children>
                       <leafNode name="prefix">
                         <properties>
                           <help>IPv6 prefix defining range of addresses to assign</help>
                           <valueHelp>
                             <format>ipv6net</format>
                             <description>IPv6 address and prefix length</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-prefix"/>
                           </constraint>
                           <multi/>
                         </properties>
                       </leafNode>
                       <tagNode name="start">
                         <properties>
                           <help>First in range of consecutive IPv6 addresses to assign</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                         <children>
                           <leafNode name="stop">
                             <properties>
                               <help>Last in range of consecutive IPv6 addresses</help>
                               <valueHelp>
                                 <format>ipv6</format>
                                 <description>IPv6 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv6-address"/>
                               </constraint>
                             </properties>
                           </leafNode>
                         </children>
                       </tagNode>
                     </children>
                   </node>
                   #include <include/dhcp/captive-portal.xml.i>
                   #include <include/dhcp/domain-search.xml.i>
                   <node name="lease-time">
                     <properties>
                       <help>Parameters relating to the lease time</help>
                     </properties>
                     <children>
                       <leafNode name="default">
                         <properties>
                           <help>Default time (in seconds) that will be assigned to a lease</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>DHCPv6 valid lifetime</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="maximum">
                         <properties>
                           <help>Maximum time (in seconds) that will be assigned to a lease</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>Maximum lease time in seconds</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="minimum">
                         <properties>
                           <help>Minimum time (in seconds) that will be assigned to a lease</help>
                           <valueHelp>
                             <format>u32:1-4294967295</format>
                             <description>Minimum lease time in seconds</description>
                           </valueHelp>
                           <constraint>
                             <validator name="numeric" argument="--range 1-4294967295"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </node>
                   #include <include/name-server-ipv6.xml.i>
                   <leafNode name="nis-domain">
                     <properties>
                       <help>NIS domain name for client to use</help>
                       <constraint>
-                        <regex>[-_a-zA-Z0-9.]+</regex>
+                        #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                       </constraint>
                       <constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="nis-server">
                     <properties>
                       <help>IPv6 address of a NIS Server</help>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of NIS server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="nisplus-domain">
                     <properties>
                       <help>NIS+ domain name for client to use</help>
                       <constraint>
-                        <regex>[-_a-zA-Z0-9.]+</regex>
+                        #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
                       </constraint>
                       <constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage>
                     </properties>
                   </leafNode>
                   <leafNode name="nisplus-server">
                     <properties>
                       <help>IPv6 address of a NIS+ Server</help>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of NIS+ server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <node name="prefix-delegation">
                     <properties>
                       <help>Parameters relating to IPv6 prefix delegation</help>
                     </properties>
                     <children>
                       <tagNode name="prefix">
                         <properties>
                           <help>IPv6 prefix to be used in prefix delegation</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 prefix used in prefix delegation</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                         <children>
                           <leafNode name="prefix-length">
                             <properties>
                               <help>Length in bits of prefix</help>
                               <valueHelp>
                                 <format>u32:32-64</format>
                                 <description>Prefix length (32-64)</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 32-64"/>
                               </constraint>
                               <constraintErrorMessage>Prefix length must be between 32 and 64</constraintErrorMessage>
                             </properties>
                           </leafNode>
                           <leafNode name="delegated-length">
                             <properties>
                               <help>Length in bits of prefixes to be delegated</help>
                               <valueHelp>
                                 <format>u32:32-64</format>
                                 <description>Delegated prefix length (32-64)</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="numeric" argument="--range 32-96"/>
                               </constraint>
                               <constraintErrorMessage>Delegated prefix length must be between 32 and 96</constraintErrorMessage>
                             </properties>
                           </leafNode>
                         </children>
                       </tagNode>
                     </children>
                   </node>
                   <leafNode name="sip-server">
                     <properties>
                       <help>IPv6 address of SIP server</help>
                       <valueHelp>
                         <format>ipv6</format>
                         <description>IPv6 address of SIP server</description>
                       </valueHelp>
                       <valueHelp>
                         <format>hostname</format>
                         <description>FQDN of SIP server</description>
                       </valueHelp>
                       <constraint>
                         <validator name="ipv6-address"/>
                         <validator name="fqdn"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <leafNode name="sntp-server">
                     <properties>
                       <help>IPv6 address of an SNTP server for client to use</help>
                       <constraint>
                         <validator name="ipv6-address"/>
                       </constraint>
                       <multi/>
                     </properties>
                   </leafNode>
                   <tagNode name="static-mapping">
                     <properties>
                       <help>Hostname for static mapping reservation</help>
                       <constraint>
                         <validator name="fqdn"/>
                       </constraint>
                       <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage>
                     </properties>
                     <children>
                       #include <include/generic-disable-node.xml.i>
                       #include <include/interface/mac.xml.i>
                       #include <include/interface/duid.xml.i>
                       <leafNode name="ipv6-address">
                         <properties>
                           <help>Client IPv6 address for this static mapping</help>
                           <valueHelp>
                             <format>ipv6</format>
                             <description>IPv6 address for this static mapping</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-address"/>
                           </constraint>
                         </properties>
                       </leafNode>
                       <leafNode name="ipv6-prefix">
                         <properties>
                           <help>Client IPv6 prefix for this static mapping</help>
                           <valueHelp>
                             <format>ipv6net</format>
                             <description>IPv6 prefix for this static mapping</description>
                           </valueHelp>
                           <constraint>
                             <validator name="ipv6-prefix"/>
                           </constraint>
                         </properties>
                       </leafNode>
                     </children>
                   </tagNode>
                   <node name="vendor-option">
                     <properties>
                       <help>Vendor Specific Options</help>
                     </properties>
                     <children>
                       <node name="cisco">
                         <properties>
                           <help>Cisco specific parameters</help>
                         </properties>
                         <children>
                           <leafNode name="tftp-server">
                             <properties>
                               <help>TFTP server name</help>
                               <valueHelp>
                                 <format>ipv6</format>
                                 <description>TFTP server IPv6 address</description>
                               </valueHelp>
                               <constraint>
                                 <validator name="ipv6-address"/>
                               </constraint>
                               <multi/>
                             </properties>
                           </leafNode>
                         </children>
                       </node>
                     </children>
                   </node>
                 </children>
               </tagNode>
             </children>
           </tagNode>
         </children>
       </node>
     </children>
   </node>
 </interfaceDefinition>
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 0ca41718f..ca7b035e5 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -1,573 +1,583 @@
 # Copyright 2017, 2019-2023 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library 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
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 """
 A library for reading VyOS running config data.
 
 This library is used internally by all config scripts of VyOS,
 but its API should be considered stable and safe to use
 in user scripts.
 
 Note that this module will not work outside VyOS.
 
 Node taxonomy
 #############
 
 There are multiple types of config tree nodes in VyOS, each requires
 its own set of operations.
 
 *Leaf nodes* (such as "address" in interfaces) can have values, but cannot
-have children. 
+have children.
 Leaf nodes can have one value, multiple values, or no values at all.
 
 For example, "system host-name" is a single-value leaf node,
 "system name-server" is a multi-value leaf node (commonly abbreviated "multi node"),
 and "system ip disable-forwarding" is a valueless leaf node.
 
 Non-leaf nodes cannot have values, but they can have child nodes. They are divided into
 two classes depending on whether the names of their children are fixed or not.
 For example, under "system", the names of all valid child nodes are predefined
 ("login", "name-server" etc.).
 
 To the contrary, children of the "system task-scheduler task" node can have arbitrary names.
 Such nodes are called *tag nodes*. This terminology is confusing but we keep using it for lack
 of a better word. No one remembers if the "tag" in "task Foo" is "task" or "Foo",
 but the distinction is irrelevant in practice.
 
 Configuration modes
 ###################
 
 VyOS has two distinct modes: operational mode and configuration mode. When a user logins,
 the CLI is in the operational mode. In this mode, only the running (effective) config is accessible for reading.
 
 When a user enters the "configure" command, a configuration session is setup. Every config session
 has its *proposed* (or *session*) config built on top of the current running config. When changes are commited, if commit succeeds,
 the proposed config is merged into the running config.
 
 In configuration mode, "base" functions like `exists`, `return_value` return values from the session config,
 while functions prefixed "effective" return values from the running config.
 
 In operational mode, all functions return values from the running config.
 
 """
 
 import re
 import json
 from copy import deepcopy
 from typing import Union
 
 import vyos.configtree
 from vyos.xml_ref import multi_to_list
 from vyos.xml_ref import from_source
 from vyos.xml_ref import ext_dict_merge
 from vyos.xml_ref import relative_defaults
 from vyos.utils.dict import get_sub_dict
 from vyos.utils.dict import mangle_dict_keys
 from vyos.configsource import ConfigSource
 from vyos.configsource import ConfigSourceSession
 
 class ConfigDict(dict):
     _from_defaults = {}
     _dict_kwargs = {}
     def from_defaults(self, path: list[str]) -> bool:
         return from_source(self._from_defaults, path)
     @property
     def kwargs(self) -> dict:
         return self._dict_kwargs
 
 def config_dict_merge(src: dict, dest: Union[dict, ConfigDict]) -> ConfigDict:
     if not isinstance(dest, ConfigDict):
         dest = ConfigDict(dest)
     return ext_dict_merge(src, dest)
 
 class Config(object):
     """
     The class of config access objects.
 
     Internally, in the current implementation, this object is *almost* stateless,
     the only state it keeps is relative *config path* for convenient access to config
     subtrees.
     """
     def __init__(self, session_env=None, config_source=None):
         if config_source is None:
             self._config_source = ConfigSourceSession(session_env)
         else:
             if not isinstance(config_source, ConfigSource):
                 raise TypeError("config_source not of type ConfigSource")
             self._config_source = config_source
 
         self._level = []
         self._dict_cache = {}
         (self._running_config,
          self._session_config) = self._config_source.get_configtree_tuple()
 
     def get_config_tree(self, effective=False):
         if effective:
             return self._running_config
         return self._session_config
 
     def _make_path(self, path):
         # Backwards-compatibility stuff: original implementation used string paths
         # libvyosconfig paths are lists, but since node names cannot contain whitespace,
         # splitting at whitespace is reasonably safe.
         # It may cause problems with exists() when it's used for checking values,
         # since values may contain whitespace.
         if isinstance(path, str):
             path = re.split(r'\s+', path)
         elif isinstance(path, list):
             pass
         else:
             raise TypeError("Path must be a whitespace-separated string or a list")
         return (self._level + path)
 
     def set_level(self, path):
         """
         Set the *edit level*, that is, a relative config tree path.
         Once set, all operations will be relative to this path,
         for example, after ``set_level("system")``, calling
         ``exists("name-server")`` is equivalent to calling
         ``exists("system name-server"`` without ``set_level``.
 
         Args:
             path (str|list): relative config path
         """
         # Make sure there's always a space between default path (level)
         # and path supplied as method argument
         # XXX: for small strings in-place concatenation is not a problem
         if isinstance(path, str):
             if path:
                 self._level = re.split(r'\s+', path)
             else:
                 self._level = []
         elif isinstance(path, list):
             self._level = path.copy()
         else:
             raise TypeError("Level path must be either a whitespace-separated string or a list")
 
     def get_level(self):
         """
         Gets the current edit level.
 
         Returns:
             str: current edit level
         """
         return(self._level.copy())
 
     def exists(self, path):
         """
         Checks if a node or value with given path exists in the proposed config.
 
         Args:
             path (str): Configuration tree path
 
         Returns:
             True if node or value exists in the proposed config, False otherwise
 
         Note:
             This function should not be used outside of configuration sessions.
             In operational mode scripts, use ``exists_effective``.
         """
         if self._session_config is None:
             return False
 
         # Assume the path is a node path first
         if self._session_config.exists(self._make_path(path)):
             return True
         else:
             # If that check fails, it may mean the path has a value at the end.
             # libvyosconfig exists() works only for _nodes_, not _values_
             # libvyattacfg also worked for values, so we emulate that case here
             if isinstance(path, str):
                 path = re.split(r'\s+', path)
             path_without_value = path[:-1]
             try:
                 # return_values() is safe to use with single-value nodes,
                 # it simply returns a single-item list in that case.
                 values = self._session_config.return_values(self._make_path(path_without_value))
 
                 # If we got this far, the node does exist and has values,
                 # so we need to check if it has the value in question among its values.
                 return (path[-1] in values)
             except vyos.configtree.ConfigTreeError:
                 # Even the parent node doesn't exist at all
                 return False
 
     def session_changed(self):
         """
         Returns:
             True if the config session has uncommited changes, False otherwise.
         """
         return self._config_source.session_changed()
 
     def in_session(self):
         """
         Returns:
             True if called from a configuration session, False otherwise.
         """
         return self._config_source.in_session()
 
     def show_config(self, path=[], default=None, effective=False):
         """
         Args:
             path (str list): Configuration tree path, or empty
             default (str): Default value to return
 
         Returns:
             str: working configuration
         """
         return self._config_source.show_config(path, default, effective)
 
     def get_cached_root_dict(self, effective=False):
         cached = self._dict_cache.get(effective, {})
         if cached:
             return cached
 
         if effective:
             config = self._running_config
         else:
             config = self._session_config
 
         if config:
             config_dict = json.loads(config.to_json())
         else:
             config_dict = {}
 
         self._dict_cache[effective] = config_dict
 
         return config_dict
 
     def verify_mangling(self, key_mangling):
         if not (isinstance(key_mangling, tuple) and \
                 (len(key_mangling) == 2) and \
                 isinstance(key_mangling[0], str) and \
                 isinstance(key_mangling[1], str)):
             raise ValueError("key_mangling must be a tuple of two strings")
 
     def get_config_dict(self, path=[], effective=False, key_mangling=None,
                         get_first_key=False, no_multi_convert=False,
                         no_tag_node_value_mangle=False,
-                        with_defaults=False, with_recursive_defaults=False):
+                        with_defaults=False,
+                        with_recursive_defaults=False,
+                        with_pki=False):
         """
         Args:
             path (str list): Configuration tree path, can be empty
             effective=False: effective or session config
             key_mangling=None: mangle dict keys according to regex and replacement
             get_first_key=False: if k = path[:-1], return sub-dict d[k] instead of {k: d[k]}
             no_multi_convert=False: if convert, return single value of multi node as list
 
         Returns: a dict representation of the config under path
         """
         kwargs = locals().copy()
         del kwargs['self']
         del kwargs['no_multi_convert']
         del kwargs['with_defaults']
         del kwargs['with_recursive_defaults']
+        del kwargs['with_pki']
 
         lpath = self._make_path(path)
         root_dict = self.get_cached_root_dict(effective)
         conf_dict = get_sub_dict(root_dict, lpath, get_first_key=get_first_key)
 
         rpath = lpath if get_first_key else lpath[:-1]
 
         if not no_multi_convert:
             conf_dict = multi_to_list(rpath, conf_dict)
 
         if key_mangling is not None:
             self.verify_mangling(key_mangling)
             conf_dict = mangle_dict_keys(conf_dict,
                                          key_mangling[0], key_mangling[1],
                                          abs_path=rpath,
                                          no_tag_node_value_mangle=no_tag_node_value_mangle)
 
         if with_defaults or with_recursive_defaults:
             defaults = self.get_config_defaults(**kwargs,
                                                 recursive=with_recursive_defaults)
             conf_dict = config_dict_merge(defaults, conf_dict)
         else:
             conf_dict = ConfigDict(conf_dict)
 
+        if with_pki and conf_dict:
+            pki_dict = self.get_config_dict(['pki'], key_mangling=('-', '_'),
+                                            no_tag_node_value_mangle=True,
+                                            get_first_key=True)
+            if pki_dict:
+                conf_dict['pki'] = pki_dict
+
         # save optional args for a call to get_config_defaults
         setattr(conf_dict, '_dict_kwargs', kwargs)
 
         return conf_dict
 
     def get_config_defaults(self, path=[], effective=False, key_mangling=None,
                             no_tag_node_value_mangle=False, get_first_key=False,
                             recursive=False) -> dict:
         lpath = self._make_path(path)
         root_dict = self.get_cached_root_dict(effective)
         conf_dict = get_sub_dict(root_dict, lpath, get_first_key)
 
         defaults = relative_defaults(lpath, conf_dict,
                                      get_first_key=get_first_key,
                                      recursive=recursive)
 
         rpath = lpath if get_first_key else lpath[:-1]
 
         if key_mangling is not None:
             self.verify_mangling(key_mangling)
             defaults = mangle_dict_keys(defaults,
                                         key_mangling[0], key_mangling[1],
                                         abs_path=rpath,
                                         no_tag_node_value_mangle=no_tag_node_value_mangle)
 
         return defaults
 
     def merge_defaults(self, config_dict: ConfigDict, recursive=False):
         if not isinstance(config_dict, ConfigDict):
             raise TypeError('argument is not of type ConfigDict')
         if not config_dict.kwargs:
             raise ValueError('argument missing metadata')
 
         args = config_dict.kwargs
         d = self.get_config_defaults(**args, recursive=recursive)
         config_dict = config_dict_merge(d, config_dict)
         return config_dict
 
     def is_multi(self, path):
         """
         Args:
             path (str): Configuration tree path
 
         Returns:
             True if a node can have multiple values, False otherwise.
 
         Note:
             It also returns False if node doesn't exist.
         """
         self._config_source.set_level(self.get_level)
         return self._config_source.is_multi(path)
 
     def is_tag(self, path):
         """
          Args:
             path (str): Configuration tree path
 
         Returns:
             True if a node is a tag node, False otherwise.
 
         Note:
             It also returns False if node doesn't exist.
         """
         self._config_source.set_level(self.get_level)
         return self._config_source.is_tag(path)
 
     def is_leaf(self, path):
         """
          Args:
             path (str): Configuration tree path
 
         Returns:
             True if a node is a leaf node, False otherwise.
 
         Note:
             It also returns False if node doesn't exist.
         """
         self._config_source.set_level(self.get_level)
         return self._config_source.is_leaf(path)
 
     def return_value(self, path, default=None):
         """
         Retrieve a value of single-value leaf node in the running or proposed config
 
         Args:
             path (str): Configuration tree path
             default (str): Default value to return if node does not exist
 
         Returns:
             str: Node value, if it has any
             None: if node is valueless *or* if it doesn't exist
 
         Note:
             Due to the issue with treatment of valueless nodes by this function,
             valueless nodes should be checked with ``exists`` instead.
 
             This function cannot be used outside a configuration session.
             In operational mode scripts, use ``return_effective_value``.
         """
         if self._session_config:
             try:
                 value = self._session_config.return_value(self._make_path(path))
             except vyos.configtree.ConfigTreeError:
                 value = None
         else:
             value = None
 
         if not value:
             return(default)
         else:
             return(value)
 
     def return_values(self, path, default=[]):
         """
         Retrieve all values of a multi-value leaf node in the running or proposed config
 
         Args:
             path (str): Configuration tree path
 
         Returns:
             str list: Node values, if it has any
             []: if node does not exist
 
         Note:
             This function cannot be used outside a configuration session.
             In operational mode scripts, use ``return_effective_values``.
         """
         if self._session_config:
             try:
                 values = self._session_config.return_values(self._make_path(path))
             except vyos.configtree.ConfigTreeError:
                 values = []
         else:
             values = []
 
         if not values:
             return(default.copy())
         else:
             return(values)
 
     def list_nodes(self, path, default=[]):
         """
         Retrieve names of all children of a tag node in the running or proposed config
 
         Args:
             path (str): Configuration tree path
 
         Returns:
             string list: child node names
 
         """
         if self._session_config:
             try:
                 nodes = self._session_config.list_nodes(self._make_path(path))
             except vyos.configtree.ConfigTreeError:
                 nodes = []
         else:
             nodes = []
 
         if not nodes:
             return(default.copy())
         else:
             return(nodes)
 
     def exists_effective(self, path):
         """
         Checks if a node or value exists in the running (effective) config.
 
         Args:
             path (str): Configuration tree path
 
         Returns:
             True if node exists in the running config, False otherwise
 
         Note:
             This function is safe to use in operational mode. In configuration mode,
             it ignores uncommited changes.
         """
         if self._running_config is None:
             return False
 
         # Assume the path is a node path first
         if self._running_config.exists(self._make_path(path)):
             return True
         else:
             # If that check fails, it may mean the path has a value at the end.
             # libvyosconfig exists() works only for _nodes_, not _values_
             # libvyattacfg also worked for values, so we emulate that case here
             if isinstance(path, str):
                 path = re.split(r'\s+', path)
             path_without_value = path[:-1]
             try:
                 # return_values() is safe to use with single-value nodes,
                 # it simply returns a single-item list in that case.
                 values = self._running_config.return_values(self._make_path(path_without_value))
 
                 # If we got this far, the node does exist and has values,
                 # so we need to check if it has the value in question among its values.
                 return (path[-1] in values)
             except vyos.configtree.ConfigTreeError:
                 # Even the parent node doesn't exist at all
                 return False
 
 
     def return_effective_value(self, path, default=None):
         """
         Retrieve a values of a single-value leaf node in a running (effective) config
 
         Args:
             path (str): Configuration tree path
             default (str): Default value to return if node does not exist
 
         Returns:
             str: Node value
         """
         if self._running_config:
             try:
                 value = self._running_config.return_value(self._make_path(path))
             except vyos.configtree.ConfigTreeError:
                 value = None
         else:
             value = None
 
         if not value:
             return(default)
         else:
             return(value)
 
     def return_effective_values(self, path, default=[]):
         """
         Retrieve all values of a multi-value node in a running (effective) config
 
         Args:
             path (str): Configuration tree path
 
         Returns:
             str list: A list of values
         """
         if self._running_config:
             try:
                 values = self._running_config.return_values(self._make_path(path))
             except vyos.configtree.ConfigTreeError:
                 values = []
         else:
             values = []
 
         if not values:
             return(default.copy())
         else:
             return(values)
 
     def list_effective_nodes(self, path, default=[]):
         """
         Retrieve names of all children of a tag node in the running config
 
         Args:
             path (str): Configuration tree path
 
         Returns:
             str list: child node names
         """
         if self._running_config:
             try:
                 nodes = self._running_config.list_nodes(self._make_path(path))
             except vyos.configtree.ConfigTreeError:
                 nodes = []
         else:
             nodes = []
 
         if not nodes:
             return(default.copy())
         else:
             return(nodes)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 6a421485f..4111d7271 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -1,663 +1,668 @@
 # Copyright 2019-2022 VyOS maintainers and contributors <maintainers@vyos.io>
 #
 # This library is free software; you can redistribute it and/or
 # modify it under the terms of the GNU Lesser General Public
 # License as published by the Free Software Foundation; either
 # version 2.1 of the License, or (at your option) any later version.
 #
 # This library 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
 # Lesser General Public License for more details.
 #
 # You should have received a copy of the GNU Lesser General Public
 # License along with this library.  If not, see <http://www.gnu.org/licenses/>.
 
 """
 A library for retrieving value dicts from VyOS configs in a declarative fashion.
 """
 import os
 import json
 
 from vyos.utils.dict import dict_search
 from vyos.utils.process import cmd
 
 def retrieve_config(path_hash, base_path, config):
     """
     Retrieves a VyOS config as a dict according to a declarative description
 
     The description dict, passed in the first argument, must follow this format:
     ``field_name : <path, type, [inner_options_dict]>``.
 
     Supported types are: ``str`` (for normal nodes),
     ``list`` (returns a list of strings, for multi nodes),
     ``bool`` (returns True if valueless node exists),
     ``dict`` (for tag nodes, returns a dict indexed by node names,
     according to description in the third item of the tuple).
 
     Args:
         path_hash (dict): Declarative description of the config to retrieve
         base_path (list): A base path to prepend to all option paths
         config (vyos.config.Config): A VyOS config object
 
     Returns:
         dict: config dict
     """
     config_hash = {}
 
     for k in path_hash:
 
         if type(path_hash[k]) != tuple:
             raise ValueError("In field {0}: expected a tuple, got a value {1}".format(k, str(path_hash[k])))
         if len(path_hash[k]) < 2:
             raise ValueError("In field {0}: field description must be a tuple of at least two items, path (list) and type".format(k))
 
         path = path_hash[k][0]
         if type(path) != list:
             raise ValueError("In field {0}: path must be a list, not a {1}".format(k, type(path)))
 
         typ = path_hash[k][1]
         if type(typ) != type:
             raise ValueError("In field {0}: type must be a type, not a {1}".format(k, type(typ)))
 
         path = base_path + path
 
         path_str = " ".join(path)
 
         if typ == str:
             config_hash[k] = config.return_value(path_str)
         elif typ == list:
             config_hash[k] = config.return_values(path_str)
         elif typ == bool:
             config_hash[k] = config.exists(path_str)
         elif typ == dict:
             try:
                 inner_hash = path_hash[k][2]
             except IndexError:
                 raise ValueError("The type of the \'{0}\' field is dict, but inner options hash is missing from the tuple".format(k))
             config_hash[k] = {}
             nodes = config.list_nodes(path_str)
             for node in nodes:
                 config_hash[k][node] = retrieve_config(inner_hash, path + [node], config)
 
     return config_hash
 
 
 def dict_merge(source, destination):
     """ Merge two dictionaries. Only keys which are not present in destination
     will be copied from source, anything else will be kept untouched. Function
     will return a new dict which has the merged key/value pairs. """
     from copy import deepcopy
     tmp = deepcopy(destination)
 
     for key, value in source.items():
         if key not in tmp:
             tmp[key] = value
         elif isinstance(source[key], dict):
             tmp[key] = dict_merge(source[key], tmp[key])
 
     return tmp
 
 def list_diff(first, second):
     """ Diff two dictionaries and return only unique items """
     second = set(second)
     return [item for item in first if item not in second]
 
 def is_node_changed(conf, path):
    """
    Check if any key under path has been changed and return True.
    If nothing changed, return false
    """
    from vyos.configdiff import get_config_diff
    D = get_config_diff(conf, key_mangling=('-', '_'))
    return D.is_node_changed(path)
 
 def leaf_node_changed(conf, path):
     """
     Check if a leaf node was altered. If it has been altered - values has been
     changed, or it was added/removed, we will return a list containing the old
     value(s). If nothing has been changed, None is returned.
 
     NOTE: path must use the real CLI node name (e.g. with a hyphen!)
     """
     from vyos.configdiff import get_config_diff
     D = get_config_diff(conf, key_mangling=('-', '_'))
     (new, old) = D.get_value_diff(path)
     if new != old:
         if isinstance(old, dict):
             # valueLess nodes return {} if node is deleted
             return True
         if old is None and isinstance(new, dict):
             # valueLess nodes return {} if node was added
             return True
         if old is None:
             return []
         if isinstance(old, str):
             return [old]
         if isinstance(old, list):
             if isinstance(new, str):
                 new = [new]
             elif isinstance(new, type(None)):
                 new = []
             return list_diff(old, new)
 
     return None
 
 def node_changed(conf, path, key_mangling=None, recursive=False, expand_nodes=None) -> list:
     """
     Check if node under path (or anything under path if recursive=True) was changed. By default
     we only check if a node or subnode (recursive) was deleted from path. If expand_nodes
     is set to Diff.ADD we can also check if something was added to the path.
 
     If nothing changed, an empty list is returned.
     """
     from vyos.configdiff import get_config_diff
     from vyos.configdiff import Diff
     # to prevent circular dependencies we assign the default here
     if not expand_nodes: expand_nodes = Diff.DELETE
     D = get_config_diff(conf, key_mangling)
     # get_child_nodes_diff() will return dict_keys()
     tmp = D.get_child_nodes_diff(path, expand_nodes=expand_nodes, recursive=recursive)
     output = []
     if expand_nodes & Diff.DELETE:
         output.extend(list(tmp['delete'].keys()))
     if expand_nodes & Diff.ADD:
         output.extend(list(tmp['add'].keys()))
+
+    # remove duplicate keys from list, this happens when a node (e.g. description) is altered
+    output = list(dict.fromkeys(output))
     return output
 
 def get_removed_vlans(conf, path, dict):
     """
     Common function to parse a dictionary retrieved via get_config_dict() and
     determine any added/removed VLAN interfaces - be it 802.1q or Q-in-Q.
     """
     from vyos.configdiff import get_config_diff, Diff
 
     # Check vif, vif-s/vif-c VLAN interfaces for removal
     D = get_config_diff(conf, key_mangling=('-', '_'))
     D.set_level(conf.get_level())
 
     # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
     keys = D.get_child_nodes_diff(path + ['vif'], expand_nodes=Diff.DELETE)['delete'].keys()
     if keys: dict['vif_remove'] = [*keys]
 
     # get_child_nodes() will return dict_keys(), mangle this into a list with PEP448
     keys = D.get_child_nodes_diff(path + ['vif-s'], expand_nodes=Diff.DELETE)['delete'].keys()
     if keys: dict['vif_s_remove'] = [*keys]
 
     for vif in dict.get('vif_s', {}).keys():
         keys = D.get_child_nodes_diff(path + ['vif-s', vif, 'vif-c'], expand_nodes=Diff.DELETE)['delete'].keys()
         if keys: dict['vif_s'][vif]['vif_c_remove'] = [*keys]
 
     return dict
 
 def is_member(conf, interface, intftype=None):
     """
     Checks if passed interface is member of other interface of specified type.
     intftype is optional, if not passed it will search all known types
     (currently bridge and bonding)
 
     Returns: dict
     empty -> Interface is not a member
     key -> Interface is a member of this interface
     """
     from vyos.ifconfig import Section
 
     ret_val = {}
     intftypes = ['bonding', 'bridge']
 
     if intftype not in intftypes + [None]:
         raise ValueError((
             f'unknown interface type "{intftype}" or it cannot '
             f'have member interfaces'))
 
     intftype = intftypes if intftype == None else [intftype]
 
     for iftype in intftype:
         base = ['interfaces', iftype]
         for intf in conf.list_nodes(base):
             member = base + [intf, 'member', 'interface', interface]
             if conf.exists(member):
                 tmp = conf.get_config_dict(member, key_mangling=('-', '_'),
                                            get_first_key=True,
                                            no_tag_node_value_mangle=True)
                 ret_val.update({intf : tmp})
 
     return ret_val
 
 def is_mirror_intf(conf, interface, direction=None):
     """
     Check whether the passed interface is used for port mirroring. Direction
     is optional, if not passed it will search all known direction
     (currently ingress and egress)
 
     Returns:
     None -> Interface is not a monitor interface
     Array() -> This interface is a monitor interface of interfaces
     """
     from vyos.ifconfig import Section
 
     directions = ['ingress', 'egress']
     if direction not in directions + [None]:
         raise ValueError(f'Unknown interface mirror direction "{direction}"')
 
     direction = directions if direction == None else [direction]
 
     ret_val = None
     base = ['interfaces']
 
     for dir in direction:
         for iftype in conf.list_nodes(base):
             iftype_base = base + [iftype]
             for intf in conf.list_nodes(iftype_base):
                 mirror = iftype_base + [intf, 'mirror', dir, interface]
                 if conf.exists(mirror):
                     path = ['interfaces', Section.section(intf), intf]
                     tmp = conf.get_config_dict(path, key_mangling=('-', '_'),
                                                get_first_key=True)
                     ret_val = {intf : tmp}
 
     return ret_val
 
 def has_address_configured(conf, intf):
     """
     Checks if interface has an address configured.
     Checks the following config nodes:
     'address', 'ipv6 address eui64', 'ipv6 address autoconf'
 
     Returns True if interface has address configured, False if it doesn't.
     """
     from vyos.ifconfig import Section
     ret = False
 
     old_level = conf.get_level()
     conf.set_level([])
 
     intfpath = ['interfaces', Section.get_config_path(intf)]
     if (conf.exists([intfpath, 'address']) or
         conf.exists([intfpath, 'ipv6', 'address', 'autoconf']) or
         conf.exists([intfpath, 'ipv6', 'address', 'eui64'])):
         ret = True
 
     conf.set_level(old_level)
     return ret
 
 def has_vrf_configured(conf, intf):
     """
     Checks if interface has a VRF configured.
 
     Returns True if interface has VRF configured, False if it doesn't.
     """
     from vyos.ifconfig import Section
     ret = False
 
     old_level = conf.get_level()
     conf.set_level([])
 
     if conf.exists(['interfaces', Section.get_config_path(intf), 'vrf']):
         ret = True
 
     conf.set_level(old_level)
     return ret
 
 def has_vlan_subinterface_configured(conf, intf):
     """
     Checks if interface has an VLAN subinterface configured.
     Checks the following config nodes:
     'vif', 'vif-s'
 
     Return True if interface has VLAN subinterface configured.
     """
     from vyos.ifconfig import Section
     ret = False
 
     intfpath = ['interfaces', Section.section(intf), intf]
     if (conf.exists(intfpath + ['vif']) or conf.exists(intfpath + ['vif-s'])):
         ret = True
 
     return ret
 
 def is_source_interface(conf, interface, intftype=None):
     """
     Checks if passed interface is configured as source-interface of other
     interfaces of specified type. intftype is optional, if not passed it will
     search all known types (currently pppoe, macsec, pseudo-ethernet, tunnel
     and vxlan)
 
     Returns:
     None -> Interface is not a member
     interface name -> Interface is a member of this interface
     False -> interface type cannot have members
     """
     ret_val = None
     intftypes = ['macsec', 'pppoe', 'pseudo-ethernet', 'tunnel', 'vxlan']
     if not intftype:
         intftype = intftypes
 
     if isinstance(intftype, str):
         intftype = [intftype]
     elif not isinstance(intftype, list):
         raise ValueError(f'Interface type "{type(intftype)}" must be either str or list!')
 
     if not all(x in intftypes for x in intftype):
         raise ValueError(f'unknown interface type "{intftype}" or it can not '
             'have a source-interface')
 
     for it in intftype:
         base = ['interfaces', it]
         for intf in conf.list_nodes(base):
             src_intf = base + [intf, 'source-interface']
             if conf.exists(src_intf) and interface in conf.return_values(src_intf):
                 ret_val = intf
                 break
 
     return ret_val
 
 def get_dhcp_interfaces(conf, vrf=None):
     """ Common helper functions to retrieve all interfaces from current CLI
     sessions that have DHCP configured. """
     dhcp_interfaces = {}
     dict = conf.get_config_dict(['interfaces'], get_first_key=True)
     if not dict:
         return dhcp_interfaces
 
     def check_dhcp(config):
         ifname = config['ifname']
         tmp = {}
         if 'address' in config and 'dhcp' in config['address']:
             options = {}
             if dict_search('dhcp_options.default_route_distance', config) != None:
                 options.update({'dhcp_options' : config['dhcp_options']})
             if 'vrf' in config:
                 if vrf == config['vrf']: tmp.update({ifname : options})
             else:
                 if vrf is None: tmp.update({ifname : options})
 
         return tmp
 
     for section, interface in dict.items():
         for ifname in interface:
             # always reset config level, as get_interface_dict() will alter it
             conf.set_level([])
             # we already have a dict representation of the config from get_config_dict(),
             # but with the extended information from get_interface_dict() we also
             # get the DHCP client default-route-distance default option if not specified.
             _, ifconfig = get_interface_dict(conf, ['interfaces', section], ifname)
 
             tmp = check_dhcp(ifconfig)
             dhcp_interfaces.update(tmp)
             # check per VLAN interfaces
             for vif, vif_config in ifconfig.get('vif', {}).items():
                 tmp = check_dhcp(vif_config)
                 dhcp_interfaces.update(tmp)
             # check QinQ VLAN interfaces
             for vif_s, vif_s_config in ifconfig.get('vif_s', {}).items():
                 tmp = check_dhcp(vif_s_config)
                 dhcp_interfaces.update(tmp)
                 for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
                     tmp = check_dhcp(vif_c_config)
                     dhcp_interfaces.update(tmp)
 
     return dhcp_interfaces
 
 def get_pppoe_interfaces(conf, vrf=None):
     """ Common helper functions to retrieve all interfaces from current CLI
     sessions that have DHCP configured. """
     pppoe_interfaces = {}
     conf.set_level([])
     for ifname in conf.list_nodes(['interfaces', 'pppoe']):
         # always reset config level, as get_interface_dict() will alter it
         conf.set_level([])
         # we already have a dict representation of the config from get_config_dict(),
         # but with the extended information from get_interface_dict() we also
         # get the DHCP client default-route-distance default option if not specified.
         _, ifconfig = get_interface_dict(conf, ['interfaces', 'pppoe'], ifname)
 
         options = {}
         if 'default_route_distance' in ifconfig:
             options.update({'default_route_distance' : ifconfig['default_route_distance']})
         if 'no_default_route' in ifconfig:
             options.update({'no_default_route' : {}})
         if 'vrf' in ifconfig:
             if vrf == ifconfig['vrf']: pppoe_interfaces.update({ifname : options})
         else:
             if vrf is None: pppoe_interfaces.update({ifname : options})
 
     return pppoe_interfaces
 
-def get_interface_dict(config, base, ifname='', recursive_defaults=True):
+def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pki=False):
     """
     Common utility function to retrieve and mangle the interfaces configuration
     from the CLI input nodes. All interfaces have a common base where value
     retrival is identical. This function must be used whenever possible when
     working on the interfaces node!
 
     Return a dictionary with the necessary interface config keys.
     """
     if not ifname:
         from vyos import ConfigError
         # determine tagNode instance
         if 'VYOS_TAGNODE_VALUE' not in os.environ:
             raise ConfigError('Interface (VYOS_TAGNODE_VALUE) not specified')
         ifname = os.environ['VYOS_TAGNODE_VALUE']
 
     # Check if interface has been removed. We must use exists() as
     # get_config_dict() will always return {} - even when an empty interface
     # node like the following exists.
     # +macsec macsec1 {
     # +}
     if not config.exists(base + [ifname]):
         dict = config.get_config_dict(base + [ifname], key_mangling=('-', '_'),
                                       get_first_key=True,
                                       no_tag_node_value_mangle=True)
         dict.update({'deleted' : {}})
     else:
         # Get config_dict with default values
         dict = config.get_config_dict(base + [ifname], key_mangling=('-', '_'),
                                       get_first_key=True,
                                       no_tag_node_value_mangle=True,
                                       with_defaults=True,
-                                      with_recursive_defaults=recursive_defaults)
+                                      with_recursive_defaults=recursive_defaults,
+                                      with_pki=with_pki)
 
         # If interface does not request an IPv4 DHCP address there is no need
         # to keep the dhcp-options key
         if 'address' not in dict or 'dhcp' not in dict['address']:
             if 'dhcp_options' in dict:
                 del dict['dhcp_options']
 
     # Add interface instance name into dictionary
     dict.update({'ifname': ifname})
 
     # Check if QoS policy applied on this interface - See ifconfig.interface.set_mirror_redirect()
     if config.exists(['qos', 'interface', ifname]):
         dict.update({'traffic_policy': {}})
 
     address = leaf_node_changed(config, base + [ifname, 'address'])
     if address: dict.update({'address_old' : address})
 
     # Check if we are a member of a bridge device
     bridge = is_member(config, ifname, 'bridge')
     if bridge: dict.update({'is_bridge_member' : bridge})
 
     # Check if it is a monitor interface
     mirror = is_mirror_intf(config, ifname)
     if mirror: dict.update({'is_mirror_intf' : mirror})
 
     # Check if we are a member of a bond device
     bond = is_member(config, ifname, 'bonding')
     if bond: dict.update({'is_bond_member' : bond})
 
     # Check if any DHCP options changed which require a client restat
     dhcp = is_node_changed(config, base + [ifname, 'dhcp-options'])
     if dhcp: dict.update({'dhcp_options_changed' : {}})
 
     # Changine interface VRF assignemnts require a DHCP restart, too
     dhcp = is_node_changed(config, base + [ifname, 'vrf'])
     if dhcp: dict.update({'dhcp_options_changed' : {}})
 
     # Some interfaces come with a source_interface which must also not be part
     # of any other bond or bridge interface as it is exclusivly assigned as the
     # Kernels "lower" interface to this new "virtual/upper" interface.
     if 'source_interface' in dict:
         # Check if source interface is member of another bridge
         tmp = is_member(config, dict['source_interface'], 'bridge')
         if tmp: dict.update({'source_interface_is_bridge_member' : tmp})
 
         # Check if source interface is member of another bridge
         tmp = is_member(config, dict['source_interface'], 'bonding')
         if tmp: dict.update({'source_interface_is_bond_member' : tmp})
 
     mac = leaf_node_changed(config, base + [ifname, 'mac'])
     if mac: dict.update({'mac_old' : mac})
 
     eui64 = leaf_node_changed(config, base + [ifname, 'ipv6', 'address', 'eui64'])
     if eui64:
         tmp = dict_search('ipv6.address', dict)
         if not tmp:
             dict.update({'ipv6': {'address': {'eui64_old': eui64}}})
         else:
             dict['ipv6']['address'].update({'eui64_old': eui64})
 
     for vif, vif_config in dict.get('vif', {}).items():
         # Add subinterface name to dictionary
         dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'})
 
         if config.exists(['qos', 'interface', f'{ifname}.{vif}']):
             dict['vif'][vif].update({'traffic_policy': {}})
 
         if 'deleted' not in dict:
             address = leaf_node_changed(config, base + [ifname, 'vif', vif, 'address'])
             if address: dict['vif'][vif].update({'address_old' : address})
 
             # If interface does not request an IPv4 DHCP address there is no need
             # to keep the dhcp-options key
             if 'address' not in dict['vif'][vif] or 'dhcp' not in dict['vif'][vif]['address']:
                 if 'dhcp_options' in dict['vif'][vif]:
                     del dict['vif'][vif]['dhcp_options']
 
         # Check if we are a member of a bridge device
         bridge = is_member(config, f'{ifname}.{vif}', 'bridge')
         if bridge: dict['vif'][vif].update({'is_bridge_member' : bridge})
 
         # Check if any DHCP options changed which require a client restat
         dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options'])
         if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}})
 
     for vif_s, vif_s_config in dict.get('vif_s', {}).items():
         # Add subinterface name to dictionary
         dict['vif_s'][vif_s].update({'ifname' : f'{ifname}.{vif_s}'})
 
         if config.exists(['qos', 'interface', f'{ifname}.{vif_s}']):
             dict['vif_s'][vif_s].update({'traffic_policy': {}})
 
         if 'deleted' not in dict:
             address = leaf_node_changed(config, base + [ifname, 'vif-s', vif_s, 'address'])
             if address: dict['vif_s'][vif_s].update({'address_old' : address})
 
             # If interface does not request an IPv4 DHCP address there is no need
             # to keep the dhcp-options key
             if 'address' not in dict['vif_s'][vif_s] or 'dhcp' not in \
                 dict['vif_s'][vif_s]['address']:
                 if 'dhcp_options' in dict['vif_s'][vif_s]:
                     del dict['vif_s'][vif_s]['dhcp_options']
 
         # Check if we are a member of a bridge device
         bridge = is_member(config, f'{ifname}.{vif_s}', 'bridge')
         if bridge: dict['vif_s'][vif_s].update({'is_bridge_member' : bridge})
 
         # Check if any DHCP options changed which require a client restat
         dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options'])
         if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}})
 
         for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
             # Add subinterface name to dictionary
             dict['vif_s'][vif_s]['vif_c'][vif_c].update({'ifname' : f'{ifname}.{vif_s}.{vif_c}'})
 
             if config.exists(['qos', 'interface', f'{ifname}.{vif_s}.{vif_c}']):
                 dict['vif_s'][vif_s]['vif_c'][vif_c].update({'traffic_policy': {}})
 
             if 'deleted' not in dict:
                 address = leaf_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'address'])
                 if address: dict['vif_s'][vif_s]['vif_c'][vif_c].update(
                         {'address_old' : address})
 
                 # If interface does not request an IPv4 DHCP address there is no need
                 # to keep the dhcp-options key
                 if 'address' not in dict['vif_s'][vif_s]['vif_c'][vif_c] or 'dhcp' \
                     not in dict['vif_s'][vif_s]['vif_c'][vif_c]['address']:
                     if 'dhcp_options' in dict['vif_s'][vif_s]['vif_c'][vif_c]:
                         del dict['vif_s'][vif_s]['vif_c'][vif_c]['dhcp_options']
 
             # Check if we are a member of a bridge device
             bridge = is_member(config, f'{ifname}.{vif_s}.{vif_c}', 'bridge')
             if bridge: dict['vif_s'][vif_s]['vif_c'][vif_c].update(
                 {'is_bridge_member' : bridge})
 
             # Check if any DHCP options changed which require a client restat
             dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'])
             if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}})
 
     # Check vif, vif-s/vif-c VLAN interfaces for removal
     dict = get_removed_vlans(config, base + [ifname], dict)
     return ifname, dict
 
 def get_vlan_ids(interface):
     """
     Get the VLAN ID of the interface bound to the bridge
     """
     vlan_ids = set()
 
     bridge_status = cmd('bridge -j vlan show', shell=True)
     vlan_filter_status = json.loads(bridge_status)
 
     if vlan_filter_status is not None:
         for interface_status in vlan_filter_status:
             ifname = interface_status['ifname']
             if interface == ifname:
                 vlans_status = interface_status['vlans']
                 for vlan_status in vlans_status:
                     vlan_id = vlan_status['vlan']
                     vlan_ids.add(vlan_id)
 
     return vlan_ids
 
-def get_accel_dict(config, base, chap_secrets):
+def get_accel_dict(config, base, chap_secrets, with_pki=False):
     """
     Common utility function to retrieve and mangle the Accel-PPP configuration
     from different CLI input nodes. All Accel-PPP services have a common base
     where value retrival is identical. This function must be used whenever
     possible when working with Accel-PPP services!
 
     Return a dictionary with the necessary interface config keys.
     """
     from vyos.utils.system import get_half_cpus
     from vyos.template import is_ipv4
 
     dict = config.get_config_dict(base, key_mangling=('-', '_'),
                                   get_first_key=True,
                                   no_tag_node_value_mangle=True,
-                                  with_recursive_defaults=True)
+                                  with_recursive_defaults=True,
+                                  with_pki=with_pki)
 
     # set CPUs cores to process requests
     dict.update({'thread_count' : get_half_cpus()})
     # we need to store the path to the secrets file
     dict.update({'chap_secrets_file' : chap_secrets})
 
     # We can only have two IPv4 and three IPv6 nameservers - also they are
     # configured in a different way in the configuration, this is why we split
     # the configuration
     if 'name_server' in dict:
         ns_v4 = []
         ns_v6 = []
         for ns in dict['name_server']:
             if is_ipv4(ns): ns_v4.append(ns)
             else: ns_v6.append(ns)
 
         dict.update({'name_server_ipv4' : ns_v4, 'name_server_ipv6' : ns_v6})
         del dict['name_server']
 
     # Check option "disable-accounting" per server and replace default value from '1813' to '0'
     for server in (dict_search('authentication.radius.server', dict) or []):
         if 'disable_accounting' in dict['authentication']['radius']['server'][server]:
             dict['authentication']['radius']['server'][server]['acct_port'] = '0'
 
     return dict
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 703e3e8c4..280932fd7 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -1,449 +1,448 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2019-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/>.
 
 import unittest
 import json
 
 from requests import request
 from urllib3.exceptions import InsecureRequestWarning
 
 from base_vyostest_shim import VyOSUnitTestSHIM
 from base_vyostest_shim import ignore_warning
 from vyos.utils.file import read_file
 from vyos.utils.process import call
 from vyos.utils.process import process_named_running
 
 from vyos.configsession import ConfigSessionError
 
 base_path = ['service', 'https']
 pki_base = ['pki']
 
 cert_data = """
 MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIw
 WTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNv
 bWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIx
 MDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNV
 BAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlP
 UzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE
 01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3
 QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E
 BAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu
 +JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3Lftz
 ngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93
 +dm/LDnp7C0=
 """
 
 key_data = """
 MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx
 2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7
 u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww
 """
 
 # to test load config via HTTP URL
 nginx_conf_smoketest = """
 server {
     listen 8000;
     server_name localhost;
 
     root /tmp;
 
     index index.html;
 
     location / {
         try_files $uri $uri/ =404;
         autoindex on;
     }
 }
 """
 
 PROCESS_NAME = 'nginx'
 
 class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
     @classmethod
     def setUpClass(cls):
         super(TestHTTPSService, cls).setUpClass()
 
         # ensure we can also run this test on a live system - so lets clean
         # out the current configuration :)
         cls.cli_delete(cls, base_path)
         cls.cli_delete(cls, pki_base)
 
     def tearDown(self):
         self.cli_delete(base_path)
         self.cli_delete(pki_base)
         self.cli_commit()
 
         # Check for stopped  process
         self.assertFalse(process_named_running(PROCESS_NAME))
 
     def test_server_block(self):
         vhost_id = 'example'
         address = '0.0.0.0'
         port = '8443'
         name = 'example.org'
 
         test_path = base_path + ['virtual-host', vhost_id]
 
         self.cli_set(test_path + ['listen-address', address])
         self.cli_set(test_path + ['port', port])
         self.cli_set(test_path + ['server-name', name])
 
         self.cli_commit()
 
         nginx_config = read_file('/etc/nginx/sites-enabled/default')
         self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
         self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config)
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_certificate(self):
         self.cli_set(pki_base + ['certificate', 'test_https', 'certificate', cert_data.replace('\n','')])
         self.cli_set(pki_base + ['certificate', 'test_https', 'private', 'key', key_data.replace('\n','')])
 
         self.cli_set(base_path + ['certificates', 'certificate', 'test_https'])
 
         self.cli_commit()
         self.assertTrue(process_named_running(PROCESS_NAME))
 
     def test_api_missing_keys(self):
         self.cli_set(base_path + ['api'])
         self.assertRaises(ConfigSessionError, self.cli_commit)
 
     def test_api_incomplete_key(self):
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01'])
         self.assertRaises(ConfigSessionError, self.cli_commit)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_auth(self):
         vhost_id = 'example'
         address = '127.0.0.1'
         port = '443' # default value
         name = 'localhost'
 
         key = 'MySuperSecretVyOS'
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
 
         test_path = base_path + ['virtual-host', vhost_id]
         self.cli_set(test_path + ['listen-address', address])
         self.cli_set(test_path + ['server-name', name])
 
         self.cli_commit()
 
         nginx_config = read_file('/etc/nginx/sites-enabled/default')
         self.assertIn(f'listen {address}:{port} ssl;', nginx_config)
         self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config)
 
         url = f'https://{address}/retrieve'
         payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
         headers = {}
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # Must get HTTP code 200 on success
         self.assertEqual(r.status_code, 200)
 
         payload_invalid = {'data': '{"op": "showConfig", "path": []}', 'key': 'invalid'}
         r = request('POST', url, verify=False, headers=headers, data=payload_invalid)
         # Must get HTTP code 401 on invalid key (Unauthorized)
         self.assertEqual(r.status_code, 401)
 
         payload_no_key = {'data': '{"op": "showConfig", "path": []}'}
         r = request('POST', url, verify=False, headers=headers, data=payload_no_key)
         # Must get HTTP code 401 on missing key (Unauthorized)
         self.assertEqual(r.status_code, 401)
 
         # Check path config
         payload = {'data': '{"op": "showConfig", "path": ["system", "login"]}', 'key': f'{key}'}
         r = request('POST', url, verify=False, headers=headers, data=payload)
         response = r.json()
         vyos_user_exists = 'vyos' in response.get('data', {}).get('user', {})
         self.assertTrue(vyos_user_exists, "The 'vyos' user does not exist in the response.")
 
         # GraphQL auth test: a missing key will return status code 400, as
         # 'key' is a non-nullable field in the schema; an incorrect key is
         # caught by the resolver, and returns success 'False', so one must
         # check the return value.
 
         self.cli_set(base_path + ['api', 'graphql'])
         self.cli_commit()
 
         graphql_url = f'https://{address}/graphql'
 
         query_valid_key = f"""
         {{
           SystemStatus (data: {{key: "{key}"}}) {{
             success
             errors
             data {{
               result
             }}
           }}
         }}
         """
 
         r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_valid_key})
         success = r.json()['data']['SystemStatus']['success']
         self.assertTrue(success)
 
         query_invalid_key = """
         {
           SystemStatus (data: {key: "invalid"}) {
             success
             errors
             data {
               result
             }
           }
         }
         """
 
         r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_invalid_key})
         success = r.json()['data']['SystemStatus']['success']
         self.assertFalse(success)
 
         query_no_key = """
         {
           SystemStatus (data: {}) {
             success
             errors
             data {
               result
             }
           }
         }
         """
 
         r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query_no_key})
         success = r.json()['data']['SystemStatus']['success']
         self.assertFalse(success)
 
         # GraphQL token authentication test: request token; pass in header
         # of query.
 
         self.cli_set(base_path + ['api', 'graphql', 'authentication', 'type', 'token'])
         self.cli_commit()
 
         mutation = """
         mutation {
           AuthToken (data: {username: "vyos", password: "vyos"}) {
             success
             errors
             data {
               result
             }
           }
         }
         """
         r = request('POST', graphql_url, verify=False, headers=headers, json={'query': mutation})
 
         token = r.json()['data']['AuthToken']['data']['result']['token']
 
         headers = {'Authorization': f'Bearer {token}'}
 
         query = """
         {
           ShowVersion (data: {}) {
             success
             errors
             op_mode_error {
               name
               message
               vyos_code
             }
             data {
               result
             }
           }
         }
         """
 
         r = request('POST', graphql_url, verify=False, headers=headers, json={'query': query})
         success = r.json()['data']['ShowVersion']['success']
         self.assertTrue(success)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_add_delete(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/retrieve'
         payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'}
         headers = {}
 
         self.cli_set(base_path)
         self.cli_commit()
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # api not configured; expect 503
         self.assertEqual(r.status_code, 503)
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # api configured; expect 200
         self.assertEqual(r.status_code, 200)
 
         self.cli_delete(base_path + ['api'])
         self.cli_commit()
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         # api deleted; expect 503
         self.assertEqual(r.status_code, 503)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_show(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/show'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "show", "path": ["system", "image"]}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_generate(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/generate'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "generate", "path": ["macsec", "mka", "cak", "gcm-aes-256"]}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_configure(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/configure'
         headers = {}
         conf_interface = 'dum0'
         conf_address = '192.0.2.44/32'
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         payload_path = [
             "interfaces",
             "dummy",
             f"{conf_interface}",
             "address",
             f"{conf_address}",
         ]
 
         payload = {'data': json.dumps({"op": "set", "path": payload_path}), 'key': key}
 
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_config_file(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/config-file'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "save"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_reset(self):
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/reset'
         headers = {}
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         payload = {
             'data': '{"op": "reset", "path": ["ip", "arp", "table"]}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
     @ignore_warning(InsecureRequestWarning)
     def test_api_config_file_load_http(self):
-        """Test load config from HTTP URL
-        """
+        # Test load config from HTTP URL
         address = '127.0.0.1'
         key = 'VyOS-key'
         url = f'https://{address}/config-file'
         url_config = f'https://{address}/configure'
         headers = {}
         tmp_file = 'tmp-config.boot'
         nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest'
 
         self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
         self.cli_commit()
 
         # load config via HTTP requires nginx config
         call(f'sudo touch {nginx_tmp_site}')
         call(f'sudo chown vyos:vyattacfg {nginx_tmp_site}')
         call(f'sudo chmod +w {nginx_tmp_site}')
 
         with open(nginx_tmp_site, 'w') as f:
             f.write(nginx_conf_smoketest)
         call('sudo nginx -s reload')
 
         # save config
         payload = {
             'data': '{"op": "save", "file": "/tmp/tmp-config.boot"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
         # change config
         payload = {
             'data': '{"op": "set", "path": ["interfaces", "dummy", "dum1", "address", "192.0.2.31/32"]}',
             'key': f'{key}',
         }
         r = request('POST', url_config, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
         # load config from URL
         payload = {
             'data': '{"op": "load", "file": "http://localhost:8000/tmp-config.boot"}',
             'key': f'{key}',
         }
         r = request('POST', url, verify=False, headers=headers, data=payload)
         self.assertEqual(r.status_code, 200)
 
         # cleanup tmp nginx conf
         call(f'sudo rm -rf {nginx_tmp_site}')
         call('sudo nginx -s reload')
 
 if __name__ == '__main__':
     unittest.main(verbosity=5)
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
index 7374a29f7..2c0f846c3 100755
--- a/src/conf_mode/interfaces_ethernet.py
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -1,400 +1,391 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2021 VyOS maintainers and contributors
+# Copyright (C) 2019-2024 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/>.
 
 import os
 import pprint
 
 from glob import glob
 from sys import exit
 
 from vyos.base import Warning
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
 from vyos.configdict import is_node_changed
 from vyos.configverify import verify_address
 from vyos.configverify import verify_dhcpv6
 from vyos.configverify import verify_eapol
 from vyos.configverify import verify_interface_exists
 from vyos.configverify import verify_mirror_redirect
 from vyos.configverify import verify_mtu
 from vyos.configverify import verify_mtu_ipv6
 from vyos.configverify import verify_vlan_config
 from vyos.configverify import verify_vrf
 from vyos.configverify import verify_bond_bridge_member
 from vyos.ethtool import Ethtool
 from vyos.ifconfig import EthernetIf
 from vyos.ifconfig import BondIf
 from vyos.pki import find_chain
 from vyos.pki import encode_certificate
 from vyos.pki import load_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.utils.process import call
 from vyos.utils.dict import dict_search
 from vyos.utils.dict import dict_to_paths_values
 from vyos.utils.dict import dict_set
 from vyos.utils.dict import dict_delete
 from vyos.utils.file import write_file
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 # XXX: wpa_supplicant works on the source interface
 cfg_dir = '/run/wpa_supplicant'
 wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf'
 
 def update_bond_options(conf: Config, eth_conf: dict) -> list:
     """
     Return list of blocked options if interface is a bond member
     :param conf: Config object
     :type conf: Config
     :param eth_conf: Ethernet config dictionary
     :type eth_conf: dict
     :return: List of blocked options
     :rtype: list
     """
     blocked_list = []
     bond_name = list(eth_conf['is_bond_member'].keys())[0]
     config_without_defaults = conf.get_config_dict(
         ['interfaces', 'ethernet', eth_conf['ifname']],
         key_mangling=('-', '_'),
         get_first_key=True,
         no_tag_node_value_mangle=True,
         with_defaults=False,
         with_recursive_defaults=False)
     config_with_defaults = conf.get_config_dict(
         ['interfaces', 'ethernet', eth_conf['ifname']],
         key_mangling=('-', '_'),
         get_first_key=True,
         no_tag_node_value_mangle=True,
         with_defaults=True,
         with_recursive_defaults=True)
     bond_config_with_defaults = conf.get_config_dict(
         ['interfaces', 'bonding', bond_name],
         key_mangling=('-', '_'),
         get_first_key=True,
         no_tag_node_value_mangle=True,
         with_defaults=True,
         with_recursive_defaults=True)
     eth_dict_paths = dict_to_paths_values(config_without_defaults)
     eth_path_base = ['interfaces', 'ethernet', eth_conf['ifname']]
 
     #if option is configured under ethernet section
     for option_path, option_value in eth_dict_paths.items():
         bond_option_value = dict_search(option_path, bond_config_with_defaults)
 
         #If option is allowed for changing then continue
         if option_path in EthernetIf.get_bond_member_allowed_options():
             continue
         # if option is inherited from bond then set valued from bond interface
         if option_path in BondIf.get_inherit_bond_options():
             # If option equals to bond option then do nothing
             if option_value == bond_option_value:
                 continue
             else:
                 # if ethernet has option and bond interface has
                 # then copy it from bond
                 if bond_option_value is not None:
                     if is_node_changed(conf, eth_path_base + option_path.split('.')):
                         Warning(
                             f'Cannot apply "{option_path.replace(".", " ")}" to "{option_value}".' \
                             f' Interface "{eth_conf["ifname"]}" is a bond member.' \
                             f' Option is inherited from bond "{bond_name}"')
                     dict_set(option_path, bond_option_value, eth_conf)
                     continue
                 # if ethernet has option and bond interface does not have
                 # then delete it form dict and do not apply it
                 else:
                     if is_node_changed(conf, eth_path_base + option_path.split('.')):
                         Warning(
                             f'Cannot apply "{option_path.replace(".", " ")}".' \
                             f' Interface "{eth_conf["ifname"]}" is a bond member.' \
                             f' Option is inherited from bond "{bond_name}"')
                     dict_delete(option_path, eth_conf)
         blocked_list.append(option_path)
 
     # if inherited option is not configured under ethernet section but configured under bond section
     for option_path in BondIf.get_inherit_bond_options():
         bond_option_value = dict_search(option_path, bond_config_with_defaults)
         if bond_option_value is not None:
             if option_path not in eth_dict_paths:
                 if is_node_changed(conf, eth_path_base + option_path.split('.')):
                     Warning(
                         f'Cannot apply "{option_path.replace(".", " ")}" to "{dict_search(option_path, config_with_defaults)}".' \
                         f' Interface "{eth_conf["ifname"]}" is a bond member. ' \
                         f'Option is inherited from bond "{bond_name}"')
                 dict_set(option_path, bond_option_value, eth_conf)
     eth_conf['bond_blocked_changes'] = blocked_list
     return None
 
 def get_config(config=None):
     """
     Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
     interface name will be added or a deleted flag
     """
     if config:
         conf = config
     else:
         conf = Config()
 
-    # This must be called prior to get_interface_dict(), as this function will
-    # alter the config level (config.set_level())
-    pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                               get_first_key=True, no_tag_node_value_mangle=True)
-
     base = ['interfaces', 'ethernet']
-    ifname, ethernet = get_interface_dict(conf, base)
+    ifname, ethernet = get_interface_dict(conf, base, with_pki=True)
+
     if 'is_bond_member' in ethernet:
         update_bond_options(conf, ethernet)
 
-    if 'deleted' not in ethernet:
-       if pki: ethernet['pki'] = pki
-
     tmp = is_node_changed(conf, base + [ifname, 'speed'])
     if tmp: ethernet.update({'speed_duplex_changed': {}})
 
     tmp = is_node_changed(conf, base + [ifname, 'duplex'])
     if tmp: ethernet.update({'speed_duplex_changed': {}})
 
     return ethernet
 
-
-
 def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
     """
      Verify speed and duplex
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     :param ethtool: Ethernet object
     :type ethtool: Ethtool
     """
     if ((ethernet['speed'] == 'auto' and ethernet['duplex'] != 'auto') or
             (ethernet['speed'] != 'auto' and ethernet['duplex'] == 'auto')):
         raise ConfigError(
             'Speed/Duplex missmatch. Must be both auto or manually configured')
 
     if ethernet['speed'] != 'auto' and ethernet['duplex'] != 'auto':
         # We need to verify if the requested speed and duplex setting is
         # supported by the underlaying NIC.
         speed = ethernet['speed']
         duplex = ethernet['duplex']
         if not ethtool.check_speed_duplex(speed, duplex):
             raise ConfigError(
                 f'Adapter does not support changing speed ' \
                 f'and duplex settings to: {speed}/{duplex}!')
 
 
 def verify_flow_control(ethernet: dict, ethtool: Ethtool):
     """
      Verify flow control
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     :param ethtool: Ethernet object
     :type ethtool: Ethtool
     """
     if 'disable_flow_control' in ethernet:
         if not ethtool.check_flow_control():
             raise ConfigError(
                 'Adapter does not support changing flow-control settings!')
 
 
 def verify_ring_buffer(ethernet: dict, ethtool: Ethtool):
     """
      Verify ring buffer
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     :param ethtool: Ethernet object
     :type ethtool: Ethtool
     """
     if 'ring_buffer' in ethernet:
         max_rx = ethtool.get_ring_buffer_max('rx')
         if not max_rx:
             raise ConfigError(
                 'Driver does not support RX ring-buffer configuration!')
 
         max_tx = ethtool.get_ring_buffer_max('tx')
         if not max_tx:
             raise ConfigError(
                 'Driver does not support TX ring-buffer configuration!')
 
         rx = dict_search('ring_buffer.rx', ethernet)
         if rx and int(rx) > int(max_rx):
             raise ConfigError(f'Driver only supports a maximum RX ring-buffer ' \
                               f'size of "{max_rx}" bytes!')
 
         tx = dict_search('ring_buffer.tx', ethernet)
         if tx and int(tx) > int(max_tx):
             raise ConfigError(f'Driver only supports a maximum TX ring-buffer ' \
                               f'size of "{max_tx}" bytes!')
 
 
 def verify_offload(ethernet: dict, ethtool: Ethtool):
     """
      Verify offloading capabilities
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     :param ethtool: Ethernet object
     :type ethtool: Ethtool
     """
     if dict_search('offload.rps', ethernet) != None:
         if not os.path.exists(f'/sys/class/net/{ethernet["ifname"]}/queues/rx-0/rps_cpus'):
             raise ConfigError('Interface does not suport RPS!')
     driver = ethtool.get_driver_name()
     # T3342 - Xen driver requires special treatment
     if driver == 'vif':
         if int(ethernet['mtu']) > 1500 and dict_search('offload.sg', ethernet) == None:
             raise ConfigError('Xen netback drivers requires scatter-gatter offloading '\
                               'for MTU size larger then 1500 bytes')
 
 
 def verify_allowedbond_changes(ethernet: dict):
     """
      Verify changed options if interface is in bonding
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     """
     if 'bond_blocked_changes' in ethernet:
         for option in ethernet['bond_blocked_changes']:
             raise ConfigError(f'Cannot configure "{option.replace(".", " ")}"' \
                               f' on interface "{ethernet["ifname"]}".' \
                               f' Interface is a bond member')
 
 
 def verify(ethernet):
     if 'deleted' in ethernet:
         return None
     if 'is_bond_member' in ethernet:
         verify_bond_member(ethernet)
     else:
         verify_ethernet(ethernet)
 
 
 def verify_bond_member(ethernet):
     """
      Verification function for ethernet interface which is in bonding
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     """
     ifname = ethernet['ifname']
     verify_interface_exists(ifname)
     verify_eapol(ethernet)
     verify_mirror_redirect(ethernet)
     ethtool = Ethtool(ifname)
     verify_speed_duplex(ethernet, ethtool)
     verify_flow_control(ethernet, ethtool)
     verify_ring_buffer(ethernet, ethtool)
     verify_offload(ethernet, ethtool)
     verify_allowedbond_changes(ethernet)
 
 def verify_ethernet(ethernet):
     """
      Verification function for simple ethernet interface
     :param ethernet: dictionary which is received from get_interface_dict
     :type ethernet: dict
     """
     ifname = ethernet['ifname']
     verify_interface_exists(ifname)
     verify_mtu(ethernet)
     verify_mtu_ipv6(ethernet)
     verify_dhcpv6(ethernet)
     verify_address(ethernet)
     verify_vrf(ethernet)
     verify_bond_bridge_member(ethernet)
     verify_eapol(ethernet)
     verify_mirror_redirect(ethernet)
     ethtool = Ethtool(ifname)
     # No need to check speed and duplex keys as both have default values.
     verify_speed_duplex(ethernet, ethtool)
     verify_flow_control(ethernet, ethtool)
     verify_ring_buffer(ethernet, ethtool)
     verify_offload(ethernet, ethtool)
     # use common function to verify VLAN configuration
     verify_vlan_config(ethernet)
     return None
 
 
 def generate(ethernet):
     # render real configuration file once
     wpa_supplicant_conf = wpa_suppl_conf.format(**ethernet)
 
     if 'deleted' in ethernet:
         # delete configuration on interface removal
         if os.path.isfile(wpa_supplicant_conf):
             os.unlink(wpa_supplicant_conf)
         return None
 
     if 'eapol' in ethernet:
         ifname = ethernet['ifname']
 
         render(wpa_supplicant_conf, 'ethernet/wpa_supplicant.conf.j2', ethernet)
 
         cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem')
         cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key')
 
         cert_name = ethernet['eapol']['certificate']
         pki_cert = ethernet['pki']['certificate'][cert_name]
 
         loaded_pki_cert = load_certificate(pki_cert['certificate'])
         loaded_ca_certs = {load_certificate(c['certificate'])
             for c in ethernet['pki']['ca'].values()} if 'ca' in ethernet['pki'] else {}
 
         cert_full_chain = find_chain(loaded_pki_cert, loaded_ca_certs)
 
         write_file(cert_file_path,
                    '\n'.join(encode_certificate(c) for c in cert_full_chain))
         write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
 
         if 'ca_certificate' in ethernet['eapol']:
             ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem')
             ca_chains = []
 
             for ca_cert_name in ethernet['eapol']['ca_certificate']:
                 pki_ca_cert = ethernet['pki']['ca'][ca_cert_name]
                 loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
                 ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
                 ca_chains.append(
                     '\n'.join(encode_certificate(c) for c in ca_full_chain))
 
             write_file(ca_cert_file_path, '\n'.join(ca_chains))
 
     return None
 
 def apply(ethernet):
     ifname = ethernet['ifname']
     # take care about EAPoL supplicant daemon
     eapol_action='stop'
 
     e = EthernetIf(ifname)
     if 'deleted' in ethernet:
         # delete interface
         e.remove()
     else:
         e.update(ethernet)
         if 'eapol' in ethernet:
             eapol_action='reload-or-restart'
 
     call(f'systemctl {eapol_action} wpa_supplicant-wired@{ifname}')
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
 
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index bdeb44837..45569dd21 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.py
@@ -1,732 +1,728 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright (C) 2019-2024 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/>.
 
 import os
 import re
 import tempfile
 
 from cryptography.hazmat.primitives.asymmetric import ec
 from glob import glob
 from sys import exit
 from ipaddress import IPv4Address
 from ipaddress import IPv4Network
 from ipaddress import IPv6Address
 from ipaddress import IPv6Network
 from ipaddress import summarize_address_range
 from netifaces import interfaces
 from secrets import SystemRandom
 from shutil import rmtree
 
 from vyos.base import DeprecationWarning
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
 from vyos.configdict import is_node_changed
 from vyos.configverify import verify_vrf
 from vyos.configverify import verify_bridge_delete
 from vyos.configverify import verify_mirror_redirect
 from vyos.configverify import verify_bond_bridge_member
 from vyos.ifconfig import VTunIf
 from vyos.pki import load_dh_parameters
 from vyos.pki import load_private_key
 from vyos.pki import sort_ca_chain
 from vyos.pki import verify_ca_chain
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_crl
 from vyos.pki import wrap_dh_parameters
 from vyos.pki import wrap_openvpn_key
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.template import is_ipv4
 from vyos.template import is_ipv6
 from vyos.utils.dict import dict_search
 from vyos.utils.dict import dict_search_args
 from vyos.utils.list import is_list_equal
 from vyos.utils.file import makedir
 from vyos.utils.file import read_file
 from vyos.utils.file import write_file
 from vyos.utils.kernel import check_kmod
 from vyos.utils.kernel import unload_kmod
 from vyos.utils.process import call
 from vyos.utils.permission import chown
 from vyos.utils.process import cmd
 from vyos.utils.network import is_addr_assigned
 
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 user = 'openvpn'
 group = 'openvpn'
 
 cfg_dir = '/run/openvpn'
 cfg_file = '/run/openvpn/{ifname}.conf'
 otp_path = '/config/auth/openvpn'
 otp_file = '/config/auth/openvpn/{ifname}-otp-secrets'
 secret_chars = list('ABCDEFGHIJKLMNOPQRSTUVWXYZ234567')
 service_file = '/run/systemd/system/openvpn@{ifname}.service.d/20-override.conf'
 
 def get_config(config=None):
     """
     Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
     interface name will be added or a deleted flag
     """
     if config:
         conf = config
     else:
         conf = Config()
     base = ['interfaces', 'openvpn']
 
-    ifname, openvpn = get_interface_dict(conf, base)
+    ifname, openvpn = get_interface_dict(conf, base, with_pki=True)
     openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
 
     if 'deleted' in openvpn:
         return openvpn
 
-    openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                        get_first_key=True,
-                                        no_tag_node_value_mangle=True)
-
     if is_node_changed(conf, base + [ifname, 'openvpn-option']):
         openvpn.update({'restart_required': {}})
     if is_node_changed(conf, base + [ifname, 'enable-dco']):
         openvpn.update({'restart_required': {}})
 
     # We have to get the dict using 'get_config_dict' instead of 'get_interface_dict'
     # as 'get_interface_dict' merges the defaults in, so we can not check for defaults in there.
     tmp = conf.get_config_dict(base + [openvpn['ifname']], get_first_key=True)
 
     # We have to cleanup the config dict, as default values could enable features
     # which are not explicitly enabled on the CLI. Example: server mfa totp
     # originate comes with defaults, which will enable the
     # totp plugin, even when not set via CLI so we
     # need to check this first and drop those keys
     if dict_search('server.mfa.totp', tmp) == None:
         del openvpn['server']['mfa']
 
     # OpenVPN Data-Channel-Offload (DCO) is a Kernel module. If loaded it applies to all
     # OpenVPN interfaces. Check if DCO is used by any other interface instance.
     tmp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
     for interface, interface_config in tmp.items():
         # If one interface has DCO configured, enable it. No need to further check
         # all other OpenVPN interfaces. We must use a dedicated key to indicate
         # the Kernel module must be loaded or not. The per interface "offload.dco"
         # key is required per OpenVPN interface instance.
         if dict_search('offload.dco', interface_config) != None:
             openvpn['module_load_dco'] = {}
             break
 
     return openvpn
 
 def is_ec_private_key(pki, cert_name):
     if not pki or 'certificate' not in pki:
         return False
     if cert_name not in pki['certificate']:
         return False
 
     pki_cert = pki['certificate'][cert_name]
     if 'private' not in pki_cert or 'key' not in pki_cert['private']:
         return False
 
     key = load_private_key(pki_cert['private']['key'])
     return isinstance(key, ec.EllipticCurvePrivateKey)
 
 def verify_pki(openvpn):
     pki = openvpn['pki']
     interface = openvpn['ifname']
     mode = openvpn['mode']
     shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
     tls = dict_search_args(openvpn, 'tls')
 
     if not bool(shared_secret_key) ^ bool(tls): #  xor check if only one is set
         raise ConfigError('Must specify only one of "shared-secret-key" and "tls"')
 
     if mode in ['server', 'client'] and not tls:
         raise ConfigError('Must specify "tls" for server and client modes')
 
     if not pki:
         raise ConfigError('PKI is not configured')
 
     if shared_secret_key:
         if not dict_search_args(pki, 'openvpn', 'shared_secret'):
             raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
 
         if shared_secret_key not in pki['openvpn']['shared_secret']:
             raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
 
         # If PSK settings are correct, warn about its deprecation
-        DeprecationWarning("OpenVPN shared-secret support will be removed in future VyOS versions.\n\
-        Please migrate your site-to-site tunnels to TLS.\n\
-        You can use self-signed certificates with peer fingerprint verification, consult the documentation for details.")
+        DeprecationWarning('OpenVPN shared-secret support will be removed in future '\
+                           'VyOS versions. Please migrate your site-to-site tunnels to '\
+                           'TLS. You can use self-signed certificates with peer fingerprint '\
+                           'verification, consult the documentation for details.')
 
     if tls:
         if (mode in ['server', 'client']) and ('ca_certificate' not in tls):
             raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface},\
               it is required in server and client modes')
         else:
             if ('ca_certificate' not in tls) and ('peer_fingerprint' not in tls):
                 raise ConfigError('Either "tls ca-certificate" or "tls peer-fingerprint" is required\
                   on openvpn interface {interface} in site-to-site mode')
 
         if 'ca_certificate' in tls:
             for ca_name in tls['ca_certificate']:
                 if ca_name not in pki['ca']:
                     raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}')
 
             if len(tls['ca_certificate']) > 1:
                 sorted_chain = sort_ca_chain(tls['ca_certificate'], pki['ca'])
                 if not verify_ca_chain(sorted_chain, pki['ca']):
                     raise ConfigError(f'CA certificates are not a valid chain')
 
         if mode != 'client' and 'auth_key' not in tls:
             if 'certificate' not in tls:
                 raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}')
 
         if 'certificate' in tls:
             if tls['certificate'] not in pki['certificate']:
                 raise ConfigError(f'Invalid certificate on openvpn interface {interface}')
 
             if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected') is not None:
                 raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}')
 
         if 'dh_params' in tls:
             pki_dh = pki['dh'][tls['dh_params']]
             dh_params = load_dh_parameters(pki_dh['parameters'])
             dh_numbers = dh_params.parameter_numbers()
             dh_bits = dh_numbers.p.bit_length()
 
             if dh_bits < 2048:
                 raise ConfigError(f'Minimum DH key-size is 2048 bits')
 
 
         if 'auth_key' in tls or 'crypt_key' in tls:
             if not dict_search_args(pki, 'openvpn', 'shared_secret'):
                 raise ConfigError('There are no openvpn shared-secrets in PKI configuration')
 
         if 'auth_key' in tls:
             if tls['auth_key'] not in pki['openvpn']['shared_secret']:
                 raise ConfigError(f'Invalid auth-key on openvpn interface {interface}')
 
         if 'crypt_key' in tls:
             if tls['crypt_key'] not in pki['openvpn']['shared_secret']:
                 raise ConfigError(f'Invalid crypt-key on openvpn interface {interface}')
 
 def verify(openvpn):
     if 'deleted' in openvpn:
         # remove totp secrets file if totp is not configured
         if os.path.isfile(otp_file.format(**openvpn)):
             os.remove(otp_file.format(**openvpn))
 
         verify_bridge_delete(openvpn)
         return None
 
     if 'mode' not in openvpn:
         raise ConfigError('Must specify OpenVPN operation mode!')
 
     #
     # OpenVPN client mode - VERIFY
     #
     if openvpn['mode'] == 'client':
         if 'local_port' in openvpn:
             raise ConfigError('Cannot specify "local-port" in client mode')
 
         if 'local_host' in openvpn:
             raise ConfigError('Cannot specify "local-host" in client mode')
 
         if 'remote_host' not in openvpn:
             raise ConfigError('Must specify "remote-host" in client mode')
 
         if openvpn['protocol'] == 'tcp-passive':
             raise ConfigError('Protocol "tcp-passive" is not valid in client mode')
 
         if dict_search('tls.dh_params', openvpn):
             raise ConfigError('Cannot specify "tls dh-params" in client mode')
 
     #
     # OpenVPN site-to-site - VERIFY
     #
     elif openvpn['mode'] == 'site-to-site':
         if 'local_address' not in openvpn and 'is_bridge_member' not in openvpn:
             raise ConfigError('Must specify "local-address" or add interface to bridge')
 
         if 'local_address' in openvpn:
             if len([addr for addr in openvpn['local_address'] if is_ipv4(addr)]) > 1:
                 raise ConfigError('Only one IPv4 local-address can be specified')
 
             if len([addr for addr in openvpn['local_address'] if is_ipv6(addr)]) > 1:
                 raise ConfigError('Only one IPv6 local-address can be specified')
 
         if openvpn['device_type'] == 'tun':
             if 'remote_address' not in openvpn:
                 raise ConfigError('Must specify "remote-address"')
 
         if 'remote_address' in openvpn:
             if len([addr for addr in openvpn['remote_address'] if is_ipv4(addr)]) > 1:
                 raise ConfigError('Only one IPv4 remote-address can be specified')
 
             if len([addr for addr in openvpn['remote_address'] if is_ipv6(addr)]) > 1:
                 raise ConfigError('Only one IPv6 remote-address can be specified')
 
             if not 'local_address' in openvpn:
                 raise ConfigError('"remote-address" requires "local-address"')
 
             v4loAddr = [addr for addr in openvpn['local_address'] if is_ipv4(addr)]
             v4remAddr = [addr for addr in openvpn['remote_address'] if is_ipv4(addr)]
             if v4loAddr and not v4remAddr:
                 raise ConfigError('IPv4 "local-address" requires IPv4 "remote-address"')
             elif v4remAddr and not v4loAddr:
                 raise ConfigError('IPv4 "remote-address" requires IPv4 "local-address"')
 
             v6remAddr = [addr for addr in openvpn['remote_address'] if is_ipv6(addr)]
             v6loAddr = [addr for addr in openvpn['local_address'] if is_ipv6(addr)]
             if v6loAddr and not v6remAddr:
                 raise ConfigError('IPv6 "local-address" requires IPv6 "remote-address"')
             elif v6remAddr and not v6loAddr:
                 raise ConfigError('IPv6 "remote-address" requires IPv6 "local-address"')
 
             if is_list_equal(v4loAddr, v4remAddr) or is_list_equal(v6loAddr, v6remAddr):
                 raise ConfigError('"local-address" and "remote-address" cannot be the same')
 
             if dict_search('local_host', openvpn) in dict_search('local_address', openvpn):
                 raise ConfigError('"local-address" cannot be the same as "local-host"')
 
             if dict_search('remote_host', openvpn) in dict_search('remote_address', openvpn):
                 raise ConfigError('"remote-address" and "remote-host" can not be the same')
 
         if openvpn['device_type'] == 'tap' and 'local_address' in openvpn:
             # we can only have one local_address, this is ensured above
             v4addr = None
             for laddr in openvpn['local_address']:
                 if is_ipv4(laddr):
                     v4addr = laddr
                     break
 
             if v4addr in openvpn['local_address'] and 'subnet_mask' not in openvpn['local_address'][v4addr]:
                 raise ConfigError('Must specify IPv4 "subnet-mask" for local-address')
 
         if dict_search('encryption.ncp_ciphers', openvpn):
             raise ConfigError('NCP ciphers can only be used in client or server mode')
 
     else:
         # checks for client-server or site-to-site bridged
         if 'local_address' in openvpn or 'remote_address' in openvpn:
             raise ConfigError('Cannot specify "local-address" or "remote-address" ' \
                               'in client/server or bridge mode')
 
     #
     # OpenVPN server mode - VERIFY
     #
     if openvpn['mode'] == 'server':
         if openvpn['protocol'] == 'tcp-active':
             raise ConfigError('Protocol "tcp-active" is not valid in server mode')
 
         if dict_search('authentication.username', openvpn) or dict_search('authentication.password', openvpn):
             raise ConfigError('Cannot specify "authentication" in server mode')
 
         if 'remote_port' in openvpn:
             raise ConfigError('Cannot specify "remote-port" in server mode')
 
         if 'remote_host' in openvpn:
             raise ConfigError('Cannot specify "remote-host" in server mode')
 
         tmp = dict_search('server.subnet', openvpn)
         if tmp:
             v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)])
             v6_subnets = len([subnet for subnet in tmp if is_ipv6(subnet)])
             if v4_subnets > 1:
                 raise ConfigError('Cannot specify more than 1 IPv4 server subnet')
             if v6_subnets > 1:
                 raise ConfigError('Cannot specify more than 1 IPv6 server subnet')
 
             for subnet in tmp:
                 if is_ipv4(subnet):
                     subnet = IPv4Network(subnet)
 
                     if openvpn['device_type'] == 'tun' and subnet.prefixlen > 29:
                         raise ConfigError('Server subnets smaller than /29 with device type "tun" are not supported')
                     elif openvpn['device_type'] == 'tap' and subnet.prefixlen > 30:
                         raise ConfigError('Server subnets smaller than /30 with device type "tap" are not supported')
 
                     for client in (dict_search('client', openvpn) or []):
                         if client['ip'] and not IPv4Address(client['ip'][0]) in subnet:
                             raise ConfigError(f'Client "{client["name"]}" IP {client["ip"][0]} not in server subnet {subnet}')
 
         else:
             if 'is_bridge_member' not in openvpn:
                 raise ConfigError('Must specify "server subnet" or add interface to bridge in server mode')
 
         if hasattr(dict_search('server.client', openvpn), '__iter__'):
             for client_k, client_v in dict_search('server.client', openvpn).items():
                 if (client_v.get('ip') and len(client_v['ip']) > 1) or (client_v.get('ipv6_ip') and len(client_v['ipv6_ip']) > 1):
                     raise ConfigError(f'Server client "{client_k}": cannot specify more than 1 IPv4 and 1 IPv6 IP')
 
         if dict_search('server.client_ip_pool', openvpn):
             if not (dict_search('server.client_ip_pool.start', openvpn) and dict_search('server.client_ip_pool.stop', openvpn)):
                 raise ConfigError('Server client-ip-pool requires both start and stop addresses')
             else:
                 v4PoolStart = IPv4Address(dict_search('server.client_ip_pool.start', openvpn))
                 v4PoolStop = IPv4Address(dict_search('server.client_ip_pool.stop', openvpn))
                 if v4PoolStart > v4PoolStop:
                     raise ConfigError(f'Server client-ip-pool start address {v4PoolStart} is larger than stop address {v4PoolStop}')
 
                 v4PoolSize = int(v4PoolStop) - int(v4PoolStart)
                 if v4PoolSize >= 65536:
                     raise ConfigError(f'Server client-ip-pool is too large [{v4PoolStart} -> {v4PoolStop} = {v4PoolSize}], maximum is 65536 addresses.')
 
                 v4PoolNets = list(summarize_address_range(v4PoolStart, v4PoolStop))
                 for client in (dict_search('client', openvpn) or []):
                     if client['ip']:
                         for v4PoolNet in v4PoolNets:
                             if IPv4Address(client['ip'][0]) in v4PoolNet:
                                 print(f'Warning: Client "{client["name"]}" IP {client["ip"][0]} is in server IP pool, it is not reserved for this client.')
             # configuring a client_ip_pool will set 'server ... nopool' which is currently incompatible with 'server-ipv6' (probably to be fixed upstream)
             for subnet in (dict_search('server.subnet', openvpn) or []):
                 if is_ipv6(subnet):
                     raise ConfigError(f'Setting client-ip-pool is incompatible having an IPv6 server subnet.')
 
         for subnet in (dict_search('server.subnet', openvpn) or []):
             if is_ipv6(subnet):
                 tmp = dict_search('client_ipv6_pool.base', openvpn)
                 if tmp:
                     if not dict_search('server.client_ip_pool', openvpn):
                         raise ConfigError('IPv6 server pool requires an IPv4 server pool')
 
                     if int(tmp.split('/')[1]) >= 112:
                         raise ConfigError('IPv6 server pool must be larger than /112')
 
                     #
                     # todo - weird logic
                     #
                     v6PoolStart = IPv6Address(tmp)
                     v6PoolStop = IPv6Network((v6PoolStart, openvpn['server_ipv6_pool_prefixlen']), strict=False)[-1] # don't remove the parentheses, it's a 2-tuple
                     v6PoolSize = int(v6PoolStop) - int(v6PoolStart) if int(openvpn['server_ipv6_pool_prefixlen']) > 96 else 65536
                     if v6PoolSize < v4PoolSize:
                         raise ConfigError(f'IPv6 server pool must be at least as large as the IPv4 pool (current sizes: IPv6={v6PoolSize} IPv4={v4PoolSize})')
 
                     v6PoolNets = list(summarize_address_range(v6PoolStart, v6PoolStop))
                     for client in (dict_search('client', openvpn) or []):
                         if client['ipv6_ip']:
                             for v6PoolNet in v6PoolNets:
                                 if IPv6Address(client['ipv6_ip'][0]) in v6PoolNet:
                                     print(f'Warning: Client "{client["name"]}" IP {client["ipv6_ip"][0]} is in server IP pool, it is not reserved for this client.')
 
         # add mfa users to the file the mfa plugin uses
         if dict_search('server.mfa.totp', openvpn):
             user_data = ''
             if not os.path.isfile(otp_file.format(**openvpn)):
                 write_file(otp_file.format(**openvpn), user_data,
                            user=user, group=group, mode=0o644)
 
             ovpn_users = read_file(otp_file.format(**openvpn))
             for client in (dict_search('server.client', openvpn) or []):
                 exists = None
                 for ovpn_user in ovpn_users.split('\n'):
                     if re.search('^' + client + ' ', ovpn_user):
                         user_data += f'{ovpn_user}\n'
                         exists = 'true'
 
                 if not exists:
                     random = SystemRandom()
                     totp_secret = ''.join(random.choice(secret_chars) for _ in range(16))
                     user_data += f'{client} otp totp:sha1:base32:{totp_secret}::xxx *\n'
 
             write_file(otp_file.format(**openvpn), user_data,
                            user=user, group=group, mode=0o644)
 
     else:
         # checks for both client and site-to-site go here
         if dict_search('server.reject_unconfigured_clients', openvpn):
             raise ConfigError('Option reject-unconfigured-clients only supported in server mode')
 
         if 'replace_default_route' in openvpn and 'remote_host' not in openvpn:
             raise ConfigError('Cannot set "replace-default-route" without "remote-host"')
 
     #
     # OpenVPN common verification section
     # not depending on any operation mode
     #
 
     # verify specified IP address is present on any interface on this system
     if 'local_host' in openvpn:
         if not is_addr_assigned(openvpn['local_host']):
             print('local-host IP address "{local_host}" not assigned' \
                   ' to any interface'.format(**openvpn))
 
     # TCP active
     if openvpn['protocol'] == 'tcp-active':
         if 'local_port' in openvpn:
             raise ConfigError('Cannot specify "local-port" with "tcp-active"')
 
         if 'remote_host' not in openvpn:
             raise ConfigError('Must specify "remote-host" with "tcp-active"')
 
     #
     # TLS/encryption
     #
     if 'shared_secret_key' in openvpn:
         if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']:
             raise ConfigError('GCM encryption with shared-secret-key not supported')
 
     if 'tls' in openvpn:
         if {'auth_key', 'crypt_key'} <= set(openvpn['tls']):
             raise ConfigError('TLS auth and crypt keys are mutually exclusive')
 
         tmp = dict_search('tls.role', openvpn)
         if tmp:
             if openvpn['mode'] in ['client', 'server']:
                 if not dict_search('tls.auth_key', openvpn):
                     raise ConfigError('Cannot specify "tls role" in client-server mode')
 
             if tmp == 'active':
                 if openvpn['protocol'] == 'tcp-passive':
                     raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"')
 
                 if dict_search('tls.dh_params', openvpn):
                     raise ConfigError('Cannot specify "tls dh-params" when "tls role" is "active"')
 
             elif tmp == 'passive':
                 if openvpn['protocol'] == 'tcp-active':
                     raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"')
 
         if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']):
             if 'dh_params' in openvpn['tls']:
                 print('Warning: using dh-params and EC keys simultaneously will ' \
                       'lead to DH ciphers being used instead of ECDH')
 
     if dict_search('encryption.cipher', openvpn) == 'none':
         print('Warning: "encryption none" was specified!')
         print('No encryption will be performed and data is transmitted in ' \
               'plain text over the network!')
 
     verify_pki(openvpn)
 
     #
     # Auth user/pass
     #
     if (dict_search('authentication.username', openvpn) and not
         dict_search('authentication.password', openvpn)):
             raise ConfigError('Password for authentication is missing')
 
     if (dict_search('authentication.password', openvpn) and not
         dict_search('authentication.username', openvpn)):
             raise ConfigError('Username for authentication is missing')
 
     verify_vrf(openvpn)
     verify_bond_bridge_member(openvpn)
     verify_mirror_redirect(openvpn)
 
     return None
 
 def generate_pki_files(openvpn):
     pki = openvpn['pki']
     if not pki:
         return None
 
     interface = openvpn['ifname']
     shared_secret_key = dict_search_args(openvpn, 'shared_secret_key')
     tls = dict_search_args(openvpn, 'tls')
 
     if shared_secret_key:
         pki_key = pki['openvpn']['shared_secret'][shared_secret_key]
         key_path = os.path.join(cfg_dir, f'{interface}_shared.key')
         write_file(key_path, wrap_openvpn_key(pki_key['key']),
                    user=user, group=group)
 
     if tls:
         if 'ca_certificate' in tls:
             cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem')
             crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem')
 
             if os.path.exists(cert_path):
                 os.unlink(cert_path)
 
             if os.path.exists(crl_path):
                 os.unlink(crl_path)
 
             for cert_name in sort_ca_chain(tls['ca_certificate'], pki['ca']):
                 pki_ca = pki['ca'][cert_name]
 
                 if 'certificate' in pki_ca:
                     write_file(cert_path, wrap_certificate(pki_ca['certificate']) + "\n",
                                user=user, group=group, mode=0o600, append=True)
 
                 if 'crl' in pki_ca:
                     for crl in pki_ca['crl']:
                         write_file(crl_path, wrap_crl(crl) + "\n", user=user, group=group,
                                    mode=0o600, append=True)
 
                     openvpn['tls']['crl'] = True
 
         if 'certificate' in tls:
             cert_name = tls['certificate']
             pki_cert = pki['certificate'][cert_name]
 
             if 'certificate' in pki_cert:
                 cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem')
                 write_file(cert_path, wrap_certificate(pki_cert['certificate']),
                            user=user, group=group, mode=0o600)
 
             if 'private' in pki_cert and 'key' in pki_cert['private']:
                 key_path = os.path.join(cfg_dir, f'{interface}_cert.key')
                 write_file(key_path, wrap_private_key(pki_cert['private']['key']),
                            user=user, group=group, mode=0o600)
 
                 openvpn['tls']['private_key'] = True
 
         if 'dh_params' in tls:
             dh_name = tls['dh_params']
             pki_dh = pki['dh'][dh_name]
 
             if 'parameters' in pki_dh:
                 dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem')
                 write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']),
                            user=user, group=group, mode=0o600)
 
         if 'auth_key' in tls:
             key_name = tls['auth_key']
             pki_key = pki['openvpn']['shared_secret'][key_name]
 
             if 'key' in pki_key:
                 key_path = os.path.join(cfg_dir, f'{interface}_auth.key')
                 write_file(key_path, wrap_openvpn_key(pki_key['key']),
                            user=user, group=group, mode=0o600)
 
         if 'crypt_key' in tls:
             key_name = tls['crypt_key']
             pki_key = pki['openvpn']['shared_secret'][key_name]
 
             if 'key' in pki_key:
                 key_path = os.path.join(cfg_dir, f'{interface}_crypt.key')
                 write_file(key_path, wrap_openvpn_key(pki_key['key']),
                            user=user, group=group, mode=0o600)
 
 
 def generate(openvpn):
     interface = openvpn['ifname']
     directory = os.path.dirname(cfg_file.format(**openvpn))
     openvpn['plugin_dir'] = '/usr/lib/openvpn'
     # create base config directory on demand
     makedir(directory, user, group)
     # enforce proper permissions on /run/openvpn
     chown(directory, user, group)
 
     # we can't know in advance which clients have been removed,
     # thus all client configs will be removed and re-added on demand
     ccd_dir = os.path.join(directory, 'ccd', interface)
     if os.path.isdir(ccd_dir):
         rmtree(ccd_dir, ignore_errors=True)
 
     # Remove systemd directories with overrides
     service_dir = os.path.dirname(service_file.format(**openvpn))
     if os.path.isdir(service_dir):
         rmtree(service_dir, ignore_errors=True)
 
     if 'deleted' in openvpn or 'disable' in openvpn:
         return None
 
     # create client config directory on demand
     makedir(ccd_dir, user, group)
 
     # Fix file permissons for keys
     generate_pki_files(openvpn)
 
     # Generate User/Password authentication file
     if 'authentication' in openvpn:
         render(openvpn['auth_user_pass_file'], 'openvpn/auth.pw.j2', openvpn,
                user=user, group=group, permission=0o600)
     else:
         # delete old auth file if present
         if os.path.isfile(openvpn['auth_user_pass_file']):
             os.remove(openvpn['auth_user_pass_file'])
 
     # Generate client specific configuration
     server_client = dict_search_args(openvpn, 'server', 'client')
     if server_client:
         for client, client_config in server_client.items():
             client_file = os.path.join(ccd_dir, client)
 
             # Our client need's to know its subnet mask ...
             client_config['server_subnet'] = dict_search('server.subnet', openvpn)
 
             render(client_file, 'openvpn/client.conf.j2', client_config,
                    user=user, group=group)
 
     # we need to support quoting of raw parameters from OpenVPN CLI
     # see https://vyos.dev/T1632
     render(cfg_file.format(**openvpn), 'openvpn/server.conf.j2', openvpn,
            formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
 
     # Render 20-override.conf for OpenVPN service
     render(service_file.format(**openvpn), 'openvpn/service-override.conf.j2', openvpn,
            formater=lambda _: _.replace("&quot;", '"'), user=user, group=group)
     # Reload systemd services config to apply an override
     call(f'systemctl daemon-reload')
 
     return None
 
 def apply(openvpn):
     interface = openvpn['ifname']
 
     # Do some cleanup when OpenVPN is disabled/deleted
     if 'deleted' in openvpn or 'disable' in openvpn:
         call(f'systemctl stop openvpn@{interface}.service')
         for cleanup_file in glob(f'/run/openvpn/{interface}.*'):
             if os.path.isfile(cleanup_file):
                 os.unlink(cleanup_file)
 
         if interface in interfaces():
             VTunIf(interface).remove()
 
     # dynamically load/unload DCO Kernel extension if requested
     dco_module = 'ovpn_dco_v2'
     if 'module_load_dco' in openvpn:
         check_kmod(dco_module)
     else:
         unload_kmod(dco_module)
 
     # Now bail out early if interface is disabled or got deleted
     if 'deleted' in openvpn or 'disable' in openvpn:
         return None
 
     # verify specified IP address is present on any interface on this system
     # Allow to bind service to nonlocal address, if it virtaual-vrrp address
     # or if address will be assign later
     if 'local_host' in openvpn:
         if not is_addr_assigned(openvpn['local_host']):
             cmd('sysctl -w net.ipv4.ip_nonlocal_bind=1')
 
     # No matching OpenVPN process running - maybe it got killed or none
     # existed - nevertheless, spawn new OpenVPN process
     action = 'reload-or-restart'
     if 'restart_required' in openvpn:
         action = 'restart'
     call(f'systemctl {action} openvpn@{interface}.service')
 
     o = VTunIf(**openvpn)
     o.update(openvpn)
 
     return None
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
-
diff --git a/src/conf_mode/interfaces_sstpc.py b/src/conf_mode/interfaces_sstpc.py
index b588910dc..b9d7a74fb 100755
--- a/src/conf_mode/interfaces_sstpc.py
+++ b/src/conf_mode/interfaces_sstpc.py
@@ -1,145 +1,141 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2022 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/>.
 
 import os
 from sys import exit
 
 from vyos.config import Config
 from vyos.configdict import get_interface_dict
 from vyos.configdict import is_node_changed
 from vyos.configverify import verify_authentication
 from vyos.configverify import verify_vrf
 from vyos.ifconfig import SSTPCIf
 from vyos.pki import encode_certificate
 from vyos.pki import find_chain
 from vyos.pki import load_certificate
 from vyos.template import render
 from vyos.utils.process import call
 from vyos.utils.dict import dict_search
 from vyos.utils.process import is_systemd_service_running
 from vyos.utils.file import write_file
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 def get_config(config=None):
     """
     Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
     interface name will be added or a deleted flag
     """
     if config:
         conf = config
     else:
         conf = Config()
     base = ['interfaces', 'sstpc']
-    ifname, sstpc = get_interface_dict(conf, base)
+    ifname, sstpc = get_interface_dict(conf, base, with_pki=True)
 
     # We should only terminate the SSTP client session if critical parameters
     # change. All parameters that can be changed on-the-fly (like interface
     # description) should not lead to a reconnect!
     for options in ['authentication', 'no_peer_dns', 'no_default_route',
                     'server', 'ssl']:
         if is_node_changed(conf, base + [ifname, options]):
             sstpc.update({'shutdown_required': {}})
             # bail out early - no need to further process other nodes
             break
 
-    # Load PKI certificates for later processing
-    sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                        get_first_key=True,
-                                        no_tag_node_value_mangle=True)
     return sstpc
 
 def verify(sstpc):
     if 'deleted' in sstpc:
         return None
 
     verify_authentication(sstpc)
     verify_vrf(sstpc)
 
     if not dict_search('server', sstpc):
         raise ConfigError('Remote SSTP server must be specified!')
 
     if not dict_search('ssl.ca_certificate', sstpc):
         raise ConfigError('Missing mandatory CA certificate!')
 
     return None
 
 def generate(sstpc):
     ifname = sstpc['ifname']
     config_sstpc = f'/etc/ppp/peers/{ifname}'
 
     sstpc['ca_file_path'] = f'/run/sstpc/{ifname}_ca-cert.pem'
 
     if 'deleted' in sstpc:
         for file in [sstpc['ca_file_path'], config_sstpc]:
             if os.path.exists(file):
                 os.unlink(file)
         return None
 
     ca_name = sstpc['ssl']['ca_certificate']
     pki_ca_cert = sstpc['pki']['ca'][ca_name]
 
     loaded_ca_cert = load_certificate(pki_ca_cert['certificate'])
     loaded_ca_certs = {load_certificate(c['certificate'])
             for c in sstpc['pki']['ca'].values()} if 'ca' in sstpc['pki'] else {}
 
     ca_full_chain = find_chain(loaded_ca_cert, loaded_ca_certs)
 
     write_file(sstpc['ca_file_path'], '\n'.join(encode_certificate(c) for c in ca_full_chain))
     render(config_sstpc, 'sstp-client/peer.j2', sstpc, permission=0o640)
 
     return None
 
 def apply(sstpc):
     ifname = sstpc['ifname']
     if 'deleted' in sstpc or 'disable' in sstpc:
         if os.path.isdir(f'/sys/class/net/{ifname}'):
             p = SSTPCIf(ifname)
             p.remove()
         call(f'systemctl stop ppp@{ifname}.service')
         return None
 
     # reconnect should only be necessary when specific options change,
     # like server, authentication ... (see get_config() for details)
     if ((not is_systemd_service_running(f'ppp@{ifname}.service')) or
         'shutdown_required' in sstpc):
 
         # cleanup system (e.g. FRR routes first)
         if os.path.isdir(f'/sys/class/net/{ifname}'):
             p = SSTPCIf(ifname)
             p.remove()
 
         call(f'systemctl restart ppp@{ifname}.service')
         # When interface comes "live" a hook is called:
         # /etc/ppp/ip-up.d/96-vyos-sstpc-callback
         # which triggers SSTPCIf.update()
     else:
         if os.path.isdir(f'/sys/class/net/{ifname}'):
             p = SSTPCIf(ifname)
             p.update(sstpc)
 
     return None
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py
index 333ebc66c..7338fe573 100755
--- a/src/conf_mode/load-balancing_reverse-proxy.py
+++ b/src/conf_mode/load-balancing_reverse-proxy.py
@@ -1,169 +1,166 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2023-2024 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/>.
 
 import os
 
 from sys import exit
 from shutil import rmtree
 
 from vyos.config import Config
 from vyos.utils.process import call
 from vyos.utils.network import check_port_availability
 from vyos.utils.network import is_listen_port_bind_service
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 load_balancing_dir = '/run/haproxy'
 load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
 systemd_service = 'haproxy.service'
 systemd_override = r'/run/systemd/system/haproxy.service.d/10-override.conf'
 
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
 
     base = ['load-balancing', 'reverse-proxy']
+    if not conf.exists(base):
+        return None
     lb = conf.get_config_dict(base,
                               get_first_key=True,
                               key_mangling=('-', '_'),
-                              no_tag_node_value_mangle=True)
-
-    if lb:
-        lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                    get_first_key=True, no_tag_node_value_mangle=True)
-
-    if lb:
-        lb = conf.merge_defaults(lb, recursive=True)
+                              no_tag_node_value_mangle=True,
+                              with_recursive_defaults=True,
+                              with_pki=True)
 
     return lb
 
 
 def verify(lb):
     if not lb:
         return None
 
     if 'backend' not in lb or 'service' not in lb:
         raise ConfigError(f'"service" and "backend" must be configured!')
 
     for front, front_config in lb['service'].items():
         if 'port' not in front_config:
             raise ConfigError(f'"{front} service port" must be configured!')
 
         # Check if bind address:port are used by another service
         tmp_address = front_config.get('address', '0.0.0.0')
         tmp_port = front_config['port']
         if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
                 not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
             raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
 
     for back, back_config in lb['backend'].items():
         if 'server' not in back_config:
             raise ConfigError(f'"{back} server" must be configured!')
         for bk_server, bk_server_conf in back_config['server'].items():
             if 'address' not in bk_server_conf or 'port' not in bk_server_conf:
                 raise ConfigError(f'"backend {back} server {bk_server} address and port" must be configured!')
 
             if {'send_proxy', 'send_proxy_v2'} <= set(bk_server_conf):
                 raise ConfigError(f'Cannot use both "send-proxy" and "send-proxy-v2" for server "{bk_server}"')
 
 def generate(lb):
     if not lb:
         # Delete /run/haproxy/haproxy.cfg
         config_files = [load_balancing_conf_file, systemd_override]
         for file in config_files:
             if os.path.isfile(file):
                 os.unlink(file)
         # Delete old directories
         if os.path.isdir(load_balancing_dir):
             rmtree(load_balancing_dir, ignore_errors=True)
 
         return None
 
     # Create load-balance dir
     if not os.path.isdir(load_balancing_dir):
         os.mkdir(load_balancing_dir)
 
     # SSL Certificates for frontend
     for front, front_config in lb['service'].items():
         if 'ssl' in front_config:
 
             if 'certificate' in front_config['ssl']:
                 cert_names = front_config['ssl']['certificate']
 
                 for cert_name in cert_names:
                     pki_cert = lb['pki']['certificate'][cert_name]
                     cert_file_path = os.path.join(load_balancing_dir, f'{cert_name}.pem')
                     cert_key_path = os.path.join(load_balancing_dir, f'{cert_name}.pem.key')
 
                     with open(cert_file_path, 'w') as f:
                         f.write(wrap_certificate(pki_cert['certificate']))
 
                     if 'private' in pki_cert and 'key' in pki_cert['private']:
                         with open(cert_key_path, 'w') as f:
                             f.write(wrap_private_key(pki_cert['private']['key']))
 
             if 'ca_certificate' in front_config['ssl']:
                 ca_name = front_config['ssl']['ca_certificate']
                 pki_ca_cert = lb['pki']['ca'][ca_name]
                 ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
 
                 with open(ca_cert_file_path, 'w') as f:
                     f.write(wrap_certificate(pki_ca_cert['certificate']))
 
     # SSL Certificates for backend
     for back, back_config in lb['backend'].items():
         if 'ssl' in back_config:
 
             if 'ca_certificate' in back_config['ssl']:
                 ca_name = back_config['ssl']['ca_certificate']
                 pki_ca_cert = lb['pki']['ca'][ca_name]
                 ca_cert_file_path = os.path.join(load_balancing_dir, f'{ca_name}.pem')
 
                 with open(ca_cert_file_path, 'w') as f:
                     f.write(wrap_certificate(pki_ca_cert['certificate']))
 
     render(load_balancing_conf_file, 'load-balancing/haproxy.cfg.j2', lb)
     render(systemd_override, 'load-balancing/override_haproxy.conf.j2', lb)
 
     return None
 
 
 def apply(lb):
     call('systemctl daemon-reload')
     if not lb:
         call(f'systemctl stop {systemd_service}')
     else:
         call(f'systemctl reload-or-restart {systemd_service}')
 
     return None
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py
index 3dc5dfc01..cb40acc9f 100755
--- a/src/conf_mode/service_https.py
+++ b/src/conf_mode/service_https.py
@@ -1,335 +1,330 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2019-2023 VyOS maintainers and contributors
+# Copyright (C) 2019-2024 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/>.
 
 import os
 import sys
 import json
 
 from copy import deepcopy
 from time import sleep
 
 import vyos.defaults
 import vyos.certbot_util
 
 from vyos.base import Warning
 from vyos.config import Config
 from vyos.configdiff import get_config_diff
 from vyos.configverify import verify_vrf
 from vyos import ConfigError
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.utils.process import call
 from vyos.utils.process import is_systemd_service_running
 from vyos.utils.process import is_systemd_service_active
 from vyos.utils.network import check_port_availability
 from vyos.utils.network import is_listen_port_bind_service
 from vyos.utils.file import write_file
 
 from vyos import airbag
 airbag.enable()
 
 config_file = '/etc/nginx/sites-available/default'
 systemd_override = r'/run/systemd/system/nginx.service.d/override.conf'
 cert_dir = '/etc/ssl/certs'
 key_dir = '/etc/ssl/private'
 certbot_dir = vyos.defaults.directories['certbot']
 
 api_config_state = '/run/http-api-state'
 systemd_service = '/run/systemd/system/vyos-http-api.service'
 
 # https config needs to coordinate several subsystems: api, certbot,
 # self-signed certificate, as well as the virtual hosts defined within the
 # https config definition itself. Consequently, one needs a general dict,
 # encompassing the https and other configs, and a list of such virtual hosts
 # (server blocks in nginx terminology) to pass to the jinja2 template.
 default_server_block = {
     'id'        : '',
     'address'   : '*',
     'port'      : '443',
     'name'      : ['_'],
     'api'       : False,
     'vyos_cert' : {},
     'certbot'   : False
 }
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
 
     base = ['service', 'https']
     if not conf.exists(base):
         return None
 
     diff = get_config_diff(conf)
 
-    https = conf.get_config_dict(base, get_first_key=True)
-
-    if https:
-        https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                            no_tag_node_value_mangle=True,
-                                            get_first_key=True)
+    https = conf.get_config_dict(base, get_first_key=True, with_pki=True)
 
     https['children_changed'] = diff.node_changed_children(base)
     https['api_add_or_delete'] = diff.node_changed_presence(base + ['api'])
 
     if 'api' not in https:
         return https
 
     http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'),
                                     no_tag_node_value_mangle=True,
                                     get_first_key=True,
                                     with_recursive_defaults=True)
 
     if http_api.from_defaults(['graphql']):
         del http_api['graphql']
 
     # Do we run inside a VRF context?
     vrf_path = ['service', 'https', 'vrf']
     if conf.exists(vrf_path):
         http_api['vrf'] = conf.return_value(vrf_path)
 
     https['api'] = http_api
 
     return https
 
 def verify(https):
     from vyos.utils.dict import dict_search
 
     if https is None:
         return None
 
     if 'certificates' in https:
         certificates = https['certificates']
 
         if 'certificate' in certificates:
             if not https['pki']:
-                raise ConfigError("PKI is not configured")
+                raise ConfigError('PKI is not configured')
 
             cert_name = certificates['certificate']
 
             if cert_name not in https['pki']['certificate']:
                 raise ConfigError("Invalid certificate on https configuration")
 
             pki_cert = https['pki']['certificate'][cert_name]
 
             if 'certificate' not in pki_cert:
                 raise ConfigError("Missing certificate on https configuration")
 
             if 'private' not in pki_cert or 'key' not in pki_cert['private']:
                 raise ConfigError("Missing certificate private key on https configuration")
 
         if 'certbot' in https['certificates']:
             vhost_names = []
             for _, vh_conf in https.get('virtual-host', {}).items():
                 vhost_names += vh_conf.get('server-name', [])
             domains = https['certificates']['certbot'].get('domain-name', [])
             domains_found = [domain for domain in domains if domain in vhost_names]
             if not domains_found:
                 raise ConfigError("At least one 'virtual-host <id> server-name' "
                               "matching the 'certbot domain-name' is required.")
 
     server_block_list = []
 
     # organize by vhosts
     vhost_dict = https.get('virtual-host', {})
 
     if not vhost_dict:
         # no specified virtual hosts (server blocks); use default
         server_block_list.append(default_server_block)
     else:
         for vhost in list(vhost_dict):
             server_block = deepcopy(default_server_block)
             data = vhost_dict.get(vhost, {})
             server_block['address'] = data.get('listen-address', '*')
             server_block['port'] = data.get('port', '443')
             server_block_list.append(server_block)
 
     for entry in server_block_list:
         _address = entry.get('address')
         _address = '0.0.0.0' if _address == '*' else _address
         _port = entry.get('port')
         proto = 'tcp'
         if check_port_availability(_address, int(_port), proto) is not True and \
                 not is_listen_port_bind_service(int(_port), 'nginx'):
             raise ConfigError(f'"{proto}" port "{_port}" is used by another service')
 
     verify_vrf(https)
 
     # Verify API server settings, if present
     if 'api' in https:
         keys = dict_search('api.keys.id', https)
         gql_auth_type = dict_search('api.graphql.authentication.type', https)
 
         # If "api graphql" is not defined and `gql_auth_type` is None,
         # there's certainly no JWT auth option, and keys are required
         jwt_auth = (gql_auth_type == "token")
 
         # Check for incomplete key configurations in every case
         valid_keys_exist = False
         if keys:
             for k in keys:
                 if 'key' not in keys[k]:
                     raise ConfigError(f'Missing HTTPS API key string for key id "{k}"')
                 else:
                     valid_keys_exist = True
 
         # If only key-based methods are enabled,
         # fail the commit if no valid key configurations are found
         if (not valid_keys_exist) and (not jwt_auth):
             raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled')
 
         if (not valid_keys_exist) and jwt_auth:
             Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.')
 
     return None
 
 def generate(https):
     if https is None:
         return None
 
     if 'api' not in https:
         if os.path.exists(systemd_service):
             os.unlink(systemd_service)
     else:
         render(systemd_service, 'https/vyos-http-api.service.j2', https['api'])
         with open(api_config_state, 'w') as f:
             json.dump(https['api'], f, indent=2)
 
     server_block_list = []
 
     # organize by vhosts
 
     vhost_dict = https.get('virtual-host', {})
 
     if not vhost_dict:
         # no specified virtual hosts (server blocks); use default
         server_block_list.append(default_server_block)
     else:
         for vhost in list(vhost_dict):
             server_block = deepcopy(default_server_block)
             server_block['id'] = vhost
             data = vhost_dict.get(vhost, {})
             server_block['address'] = data.get('listen-address', '*')
             server_block['port'] = data.get('port', '443')
             name = data.get('server-name', ['_'])
             server_block['name'] = name
             allow_client = data.get('allow-client', {})
             server_block['allow_client'] = allow_client.get('address', [])
             server_block_list.append(server_block)
 
     # get certificate data
 
     cert_dict = https.get('certificates', {})
 
     if 'certificate' in cert_dict:
         cert_name = cert_dict['certificate']
         pki_cert = https['pki']['certificate'][cert_name]
 
         cert_path = os.path.join(cert_dir, f'{cert_name}.pem')
         key_path = os.path.join(key_dir, f'{cert_name}.pem')
 
         server_cert = str(wrap_certificate(pki_cert['certificate']))
         if 'ca-certificate' in cert_dict:
             ca_cert = cert_dict['ca-certificate']
             server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate']))
 
         write_file(cert_path, server_cert)
         write_file(key_path, wrap_private_key(pki_cert['private']['key']))
 
         vyos_cert_data = {
             'crt': cert_path,
             'key': key_path
         }
 
         for block in server_block_list:
             block['vyos_cert'] = vyos_cert_data
 
     # letsencrypt certificate using certbot
 
     certbot = False
     cert_domains = cert_dict.get('certbot', {}).get('domain-name', [])
     if cert_domains:
         certbot = True
         for domain in cert_domains:
             sub_list = vyos.certbot_util.choose_server_block(server_block_list,
                                                              domain)
             if sub_list:
                 for sb in sub_list:
                     sb['certbot'] = True
                     sb['certbot_dir'] = certbot_dir
                     # certbot organizes certificates by first domain
                     sb['certbot_domain_dir'] = cert_domains[0]
 
     if 'api' in list(https):
         vhost_list = https.get('api-restrict', {}).get('virtual-host', [])
         if not vhost_list:
             for block in server_block_list:
                 block['api'] = True
         else:
             for block in server_block_list:
                 if block['id'] in vhost_list:
                     block['api'] = True
 
     data = {
         'server_block_list': server_block_list,
         'certbot': certbot
     }
 
     render(config_file, 'https/nginx.default.j2', data)
     render(systemd_override, 'https/override.conf.j2', https)
     return None
 
 def apply(https):
     # Reload systemd manager configuration
     call('systemctl daemon-reload')
     http_api_service_name = 'vyos-http-api.service'
     https_service_name = 'nginx.service'
 
     if https is None:
         if is_systemd_service_active(f'{http_api_service_name}'):
             call(f'systemctl stop {http_api_service_name}')
         call(f'systemctl stop {https_service_name}')
         return
 
     if 'api' in https['children_changed']:
         if 'api' in https:
             if is_systemd_service_running(f'{http_api_service_name}'):
                 call(f'systemctl reload {http_api_service_name}')
             else:
                 call(f'systemctl restart {http_api_service_name}')
             # Let uvicorn settle before (possibly) restarting nginx
             sleep(1)
         else:
             if is_systemd_service_active(f'{http_api_service_name}'):
                 call(f'systemctl stop {http_api_service_name}')
 
     if (not is_systemd_service_running(f'{https_service_name}') or
         https['api_add_or_delete'] or
         set(https['children_changed']) - set(['api'])):
         call(f'systemctl restart {https_service_name}')
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         sys.exit(1)
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 9e9385ddb..7fd32c230 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -1,596 +1,594 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2024 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/>.
 
 import ipaddress
 import os
 import re
 import jmespath
 
 from sys import exit
 from time import sleep
 from time import time
 
 from vyos.base import Warning
 from vyos.config import Config
 from vyos.configdict import leaf_node_changed
 from vyos.configverify import verify_interface_exists
 from vyos.defaults import directories
 from vyos.ifconfig import Interface
 from vyos.pki import encode_certificate
 from vyos.pki import encode_public_key
 from vyos.pki import find_chain
 from vyos.pki import load_certificate
 from vyos.pki import load_private_key
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_crl
 from vyos.pki import wrap_public_key
 from vyos.pki import wrap_private_key
 from vyos.template import ip_from_cidr
 from vyos.template import is_ipv4
 from vyos.template import is_ipv6
 from vyos.template import render
 from vyos.utils.network import is_ipv6_link_local
 from vyos.utils.dict import dict_search
 from vyos.utils.dict import dict_search_args
 from vyos.utils.process import call
 from vyos.utils.process import run
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 dhcp_wait_attempts = 2
 dhcp_wait_sleep = 1
 
 swanctl_dir        = '/etc/swanctl'
 charon_conf        = '/etc/strongswan.d/charon.conf'
 charon_dhcp_conf   = '/etc/strongswan.d/charon/dhcp.conf'
 charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
 interface_conf     = '/etc/strongswan.d/interfaces_use.conf'
 swanctl_conf       = f'{swanctl_dir}/swanctl.conf'
 
 default_install_routes = 'yes'
 
 vici_socket = '/var/run/charon.vici'
 
 CERT_PATH = f'{swanctl_dir}/x509/'
 PUBKEY_PATH = f'{swanctl_dir}/pubkey/'
 KEY_PATH  = f'{swanctl_dir}/private/'
 CA_PATH   = f'{swanctl_dir}/x509ca/'
 CRL_PATH  = f'{swanctl_dir}/x509crl/'
 
 DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting'
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['vpn', 'ipsec']
     l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings']
     if not conf.exists(base):
         return None
 
     # retrieve common dictionary keys
     ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
                                  no_tag_node_value_mangle=True,
                                  get_first_key=True,
-                                 with_recursive_defaults=True)
+                                 with_recursive_defaults=True,
+                                 with_pki=True)
 
     ipsec['dhcp_no_address'] = {}
     ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
     ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
     ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
-    ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                        no_tag_node_value_mangle=True,
-                                        get_first_key=True)
 
     tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
                                no_tag_node_value_mangle=True,
                                get_first_key=True)
     if tmp:
         ipsec['l2tp'] = conf.merge_defaults(tmp, recursive=True)
         ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address'])
         ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024'
         ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1'
 
     return ipsec
 
 def get_dhcp_address(iface):
     addresses = Interface(iface).get_addr()
     if not addresses:
         return None
     for address in addresses:
         if not is_ipv6_link_local(address):
             return ip_from_cidr(address)
     return None
 
 def verify_pki_x509(pki, x509_conf):
     if not pki or 'ca' not in pki or 'certificate' not in pki:
         raise ConfigError(f'PKI is not configured')
 
     ca_cert_name = x509_conf['ca_certificate']
     cert_name = x509_conf['certificate']
 
     if not dict_search_args(pki, 'ca', ca_cert_name, 'certificate'):
         raise ConfigError(f'Missing CA certificate on specified PKI CA certificate "{ca_cert_name}"')
 
     if not dict_search_args(pki, 'certificate', cert_name, 'certificate'):
         raise ConfigError(f'Missing certificate on specified PKI certificate "{cert_name}"')
 
     if not dict_search_args(pki, 'certificate', cert_name, 'private', 'key'):
         raise ConfigError(f'Missing private key on specified PKI certificate "{cert_name}"')
 
     return True
 
 def verify_pki_rsa(pki, rsa_conf):
     if not pki or 'key_pair' not in pki:
         raise ConfigError(f'PKI is not configured')
 
     local_key = rsa_conf['local_key']
     remote_key = rsa_conf['remote_key']
 
     if not dict_search_args(pki, 'key_pair', local_key, 'private', 'key'):
         raise ConfigError(f'Missing private key on specified local-key "{local_key}"')
 
     if not dict_search_args(pki, 'key_pair', remote_key, 'public', 'key'):
         raise ConfigError(f'Missing public key on specified remote-key "{remote_key}"')
 
     return True
 
 def verify(ipsec):
     if not ipsec:
         return None
 
     if 'authentication' in ipsec:
         if 'psk' in ipsec['authentication']:
             for psk, psk_config in ipsec['authentication']['psk'].items():
                 if 'id' not in psk_config or 'secret' not in psk_config:
                     raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"')
 
     if 'interfaces' in ipsec :
         for ifname in ipsec['interface']:
             verify_interface_exists(ifname)
 
     if 'l2tp' in ipsec:
         if 'esp_group' in ipsec['l2tp']:
             if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']:
                 raise ConfigError(f"Invalid esp-group on L2TP remote-access config")
 
         if 'ike_group' in ipsec['l2tp']:
             if 'ike_group' not in ipsec or ipsec['l2tp']['ike_group'] not in ipsec['ike_group']:
                 raise ConfigError(f"Invalid ike-group on L2TP remote-access config")
 
         if 'authentication' not in ipsec['l2tp']:
             raise ConfigError(f'Missing authentication settings on L2TP remote-access config')
 
         if 'mode' not in ipsec['l2tp']['authentication']:
             raise ConfigError(f'Missing authentication mode on L2TP remote-access config')
 
         if not ipsec['l2tp_outside_address']:
             raise ConfigError(f'Missing outside-address on L2TP remote-access config')
 
         if ipsec['l2tp']['authentication']['mode'] == 'pre-shared-secret':
             if 'pre_shared_secret' not in ipsec['l2tp']['authentication']:
                 raise ConfigError(f'Missing pre shared secret on L2TP remote-access config')
 
         if ipsec['l2tp']['authentication']['mode'] == 'x509':
             if 'x509' not in ipsec['l2tp']['authentication']:
                 raise ConfigError(f'Missing x509 settings on L2TP remote-access config')
 
             x509 = ipsec['l2tp']['authentication']['x509']
 
             if 'ca_certificate' not in x509 or 'certificate' not in x509:
                 raise ConfigError(f'Missing x509 certificates on L2TP remote-access config')
 
             verify_pki_x509(ipsec['pki'], x509)
 
     if 'profile' in ipsec:
         for profile, profile_conf in ipsec['profile'].items():
             if 'esp_group' in profile_conf:
                 if 'esp_group' not in ipsec or profile_conf['esp_group'] not in ipsec['esp_group']:
                     raise ConfigError(f"Invalid esp-group on {profile} profile")
             else:
                 raise ConfigError(f"Missing esp-group on {profile} profile")
 
             if 'ike_group' in profile_conf:
                 if 'ike_group' not in ipsec or profile_conf['ike_group'] not in ipsec['ike_group']:
                     raise ConfigError(f"Invalid ike-group on {profile} profile")
             else:
                 raise ConfigError(f"Missing ike-group on {profile} profile")
 
             if 'authentication' not in profile_conf:
                 raise ConfigError(f"Missing authentication on {profile} profile")
 
     if 'remote_access' in ipsec:
         if 'connection' in ipsec['remote_access']:
             for name, ra_conf in ipsec['remote_access']['connection'].items():
                 if 'esp_group' in ra_conf:
                     if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']:
                         raise ConfigError(f"Invalid esp-group on {name} remote-access config")
                 else:
                     raise ConfigError(f"Missing esp-group on {name} remote-access config")
 
                 if 'ike_group' in ra_conf:
                     if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']:
                         raise ConfigError(f"Invalid ike-group on {name} remote-access config")
 
                     ike = ra_conf['ike_group']
                     if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2':
                         raise ConfigError('IPsec remote-access connections requires IKEv2!')
 
                 else:
                     raise ConfigError(f"Missing ike-group on {name} remote-access config")
 
                 if 'authentication' not in ra_conf:
                     raise ConfigError(f"Missing authentication on {name} remote-access config")
 
                 if ra_conf['authentication']['server_mode'] == 'x509':
                     if 'x509' not in ra_conf['authentication']:
                         raise ConfigError(f"Missing x509 settings on {name} remote-access config")
 
                     x509 = ra_conf['authentication']['x509']
 
                     if 'ca_certificate' not in x509 or 'certificate' not in x509:
                         raise ConfigError(f"Missing x509 certificates on {name} remote-access config")
 
                     verify_pki_x509(ipsec['pki'], x509)
                 elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret':
                     if 'pre_shared_secret' not in ra_conf['authentication']:
                         raise ConfigError(f"Missing pre-shared-key on {name} remote-access config")
 
                 if 'client_mode' not in ra_conf['authentication']:
                     raise ConfigError('Client authentication method is required!')
 
                 if dict_search('authentication.client_mode', ra_conf) == 'eap-radius':
                     if dict_search('remote_access.radius.server', ipsec) == None:
                         raise ConfigError('RADIUS authentication requires at least one server')
 
                 if 'pool' in ra_conf:
                     if {'dhcp', 'radius'} <= set(ra_conf['pool']):
                         raise ConfigError(f'Can not use both DHCP and RADIUS for address allocation '\
                                           f'at the same time for "{name}"!')
 
                     if 'dhcp' in ra_conf['pool'] and len(ra_conf['pool']) > 1:
                         raise ConfigError(f'Can not use DHCP and a predefined address pool for "{name}"!')
 
                     if 'radius' in ra_conf['pool'] and len(ra_conf['pool']) > 1:
                         raise ConfigError(f'Can not use RADIUS and a predefined address pool for "{name}"!')
 
                     for pool in ra_conf['pool']:
                         if pool == 'dhcp':
                             if dict_search('remote_access.dhcp.server', ipsec) == None:
                                 raise ConfigError('IPsec DHCP server is not configured!')
                         elif pool == 'radius':
                             if dict_search('remote_access.radius.server', ipsec) == None:
                                 raise ConfigError('IPsec RADIUS server is not configured!')
 
                             if dict_search('authentication.client_mode', ra_conf) != 'eap-radius':
                                 raise ConfigError('RADIUS IP pool requires eap-radius client authentication!')
 
                         elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']:
                             raise ConfigError(f'Requested pool "{pool}" does not exist!')
 
         if 'pool' in ipsec['remote_access']:
             for pool, pool_config in ipsec['remote_access']['pool'].items():
                 if 'prefix' not in pool_config:
                     raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!')
 
                 if 'name_server' in pool_config:
                     if len(pool_config['name_server']) > 2:
                         raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!')
 
                     for ns in pool_config['name_server']:
                         v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix'])
                         v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix'])
                         if v4_addr_and_ns or v6_addr_and_ns:
                            raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!')
 
                 if 'exclude' in pool_config:
                     for exclude in pool_config['exclude']:
                         v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix'])
                         v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix'])
                         if v4_addr_and_exclude or v6_addr_and_exclude:
                            raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!')
 
         if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']:
             for server, server_config in ipsec['remote_access']['radius']['server'].items():
                 if 'key' not in server_config:
                     raise ConfigError(f'Missing RADIUS secret key for server "{server}"')
 
     if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
         for peer, peer_conf in ipsec['site_to_site']['peer'].items():
             has_default_esp = False
             # Peer name it is swanctl connection name and shouldn't contain dots or colons, T4118
             if bool(re.search(':|\.', peer)):
                 raise ConfigError(f'Incorrect peer name "{peer}" '
                                   f'Peer name can contain alpha-numeric letters, hyphen and underscore')
 
             if 'remote_address' not in peer_conf:
                 print(f'You should set correct remote-address "peer {peer} remote-address x.x.x.x"\n')
 
             if 'default_esp_group' in peer_conf:
                 has_default_esp = True
                 if 'esp_group' not in ipsec or peer_conf['default_esp_group'] not in ipsec['esp_group']:
                     raise ConfigError(f"Invalid esp-group on site-to-site peer {peer}")
 
             if 'ike_group' in peer_conf:
                 if 'ike_group' not in ipsec or peer_conf['ike_group'] not in ipsec['ike_group']:
                     raise ConfigError(f"Invalid ike-group on site-to-site peer {peer}")
             else:
                 raise ConfigError(f"Missing ike-group on site-to-site peer {peer}")
 
             if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
                 raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
 
             if {'id', 'use_x509_id'} <= set(peer_conf['authentication']):
                 raise ConfigError(f"Manually set peer id and use-x509-id are mutually exclusive!")
 
             if peer_conf['authentication']['mode'] == 'x509':
                 if 'x509' not in peer_conf['authentication']:
                     raise ConfigError(f"Missing x509 settings on site-to-site peer {peer}")
 
                 x509 = peer_conf['authentication']['x509']
 
                 if 'ca_certificate' not in x509 or 'certificate' not in x509:
                     raise ConfigError(f"Missing x509 certificates on site-to-site peer {peer}")
 
                 verify_pki_x509(ipsec['pki'], x509)
             elif peer_conf['authentication']['mode'] == 'rsa':
                 if 'rsa' not in peer_conf['authentication']:
                     raise ConfigError(f"Missing RSA settings on site-to-site peer {peer}")
 
                 rsa = peer_conf['authentication']['rsa']
 
                 if 'local_key' not in rsa:
                     raise ConfigError(f"Missing RSA local-key on site-to-site peer {peer}")
 
                 if 'remote_key' not in rsa:
                     raise ConfigError(f"Missing RSA remote-key on site-to-site peer {peer}")
 
                 verify_pki_rsa(ipsec['pki'], rsa)
 
             if 'local_address' not in peer_conf and 'dhcp_interface' not in peer_conf:
                 raise ConfigError(f"Missing local-address or dhcp-interface on site-to-site peer {peer}")
 
             if 'dhcp_interface' in peer_conf:
                 dhcp_interface = peer_conf['dhcp_interface']
 
                 verify_interface_exists(dhcp_interface)
                 dhcp_base = directories['isc_dhclient_dir']
 
                 if not os.path.exists(f'{dhcp_base}/dhclient_{dhcp_interface}.conf'):
                     raise ConfigError(f"Invalid dhcp-interface on site-to-site peer {peer}")
 
                 address = get_dhcp_address(dhcp_interface)
                 count = 0
                 while not address and count < dhcp_wait_attempts:
                     address = get_dhcp_address(dhcp_interface)
                     count += 1
                     sleep(dhcp_wait_sleep)
 
                 if not address:
                     ipsec['dhcp_no_address'][peer] = dhcp_interface
                     print(f"Failed to get address from dhcp-interface on site-to-site peer {peer} -- skipped")
                     continue
 
             if 'vti' in peer_conf:
                 if 'local_address' in peer_conf and 'dhcp_interface' in peer_conf:
                     raise ConfigError(f"A single local-address or dhcp-interface is required when using VTI on site-to-site peer {peer}")
 
                 if dict_search('options.disable_route_autoinstall',
                                ipsec) == None:
                     Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]')
 
                 if 'bind' in peer_conf['vti']:
                     vti_interface = peer_conf['vti']['bind']
                     if not os.path.exists(f'/sys/class/net/{vti_interface}'):
                         raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!')
 
             if 'vti' not in peer_conf and 'tunnel' not in peer_conf:
                 raise ConfigError(f"No VTI or tunnel specified on site-to-site peer {peer}")
 
             if 'tunnel' in peer_conf:
                 for tunnel, tunnel_conf in peer_conf['tunnel'].items():
                     if 'esp_group' not in tunnel_conf and not has_default_esp:
                         raise ConfigError(f"Missing esp-group on tunnel {tunnel} for site-to-site peer {peer}")
 
                     esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group']
 
                     if esp_group_name not in ipsec['esp_group']:
                         raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}")
 
                     esp_group = ipsec['esp_group'][esp_group_name]
 
                     if 'mode' in esp_group and esp_group['mode'] == 'transport':
                         if 'protocol' in tunnel_conf and ((peer in ['any', '0.0.0.0']) or ('local_address' not in peer_conf or peer_conf['local_address'] in ['any', '0.0.0.0'])):
                             raise ConfigError(f"Fixed local-address or peer required when a protocol is defined with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
 
                         if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
                             raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
 
 def cleanup_pki_files():
     for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]:
         if not os.path.exists(path):
             continue
         for file in os.listdir(path):
             file_path = os.path.join(path, file)
             if os.path.isfile(file_path):
                 os.unlink(file_path)
 
 def generate_pki_files_x509(pki, x509_conf):
     ca_cert_name = x509_conf['ca_certificate']
     ca_cert_data = dict_search_args(pki, 'ca', ca_cert_name, 'certificate')
     ca_cert_crls = dict_search_args(pki, 'ca', ca_cert_name, 'crl') or []
     ca_index = 1
     crl_index = 1
 
     ca_cert = load_certificate(ca_cert_data)
     pki_ca_certs = [load_certificate(ca['certificate']) for ca in pki['ca'].values()]
 
     ca_cert_chain = find_chain(ca_cert, pki_ca_certs)
 
     cert_name = x509_conf['certificate']
     cert_data = dict_search_args(pki, 'certificate', cert_name, 'certificate')
     key_data = dict_search_args(pki, 'certificate', cert_name, 'private', 'key')
     protected = 'passphrase' in x509_conf
 
     for ca_cert_obj in ca_cert_chain:
         with open(os.path.join(CA_PATH, f'{ca_cert_name}_{ca_index}.pem'), 'w') as f:
             f.write(encode_certificate(ca_cert_obj))
         ca_index += 1
 
     for crl in ca_cert_crls:
         with open(os.path.join(CRL_PATH, f'{ca_cert_name}_{crl_index}.pem'), 'w') as f:
             f.write(wrap_crl(crl))
         crl_index += 1
 
     with open(os.path.join(CERT_PATH, f'{cert_name}.pem'), 'w') as f:
         f.write(wrap_certificate(cert_data))
 
     with open(os.path.join(KEY_PATH, f'x509_{cert_name}.pem'), 'w') as f:
         f.write(wrap_private_key(key_data, protected))
 
 def generate_pki_files_rsa(pki, rsa_conf):
     local_key_name = rsa_conf['local_key']
     local_key_data = dict_search_args(pki, 'key_pair', local_key_name, 'private', 'key')
     protected = 'passphrase' in rsa_conf
     remote_key_name = rsa_conf['remote_key']
     remote_key_data = dict_search_args(pki, 'key_pair', remote_key_name, 'public', 'key')
 
     local_key = load_private_key(local_key_data, rsa_conf['passphrase'] if protected else None)
 
     with open(os.path.join(KEY_PATH, f'rsa_{local_key_name}.pem'), 'w') as f:
         f.write(wrap_private_key(local_key_data, protected))
 
     with open(os.path.join(PUBKEY_PATH, f'{local_key_name}.pem'), 'w') as f:
         f.write(encode_public_key(local_key.public_key()))
 
     with open(os.path.join(PUBKEY_PATH, f'{remote_key_name}.pem'), 'w') as f:
         f.write(wrap_public_key(remote_key_data))
 
 def generate(ipsec):
     cleanup_pki_files()
 
     if not ipsec:
         for config_file in [charon_dhcp_conf, charon_radius_conf, interface_conf, swanctl_conf]:
             if os.path.isfile(config_file):
                 os.unlink(config_file)
         render(charon_conf, 'ipsec/charon.j2', {'install_routes': default_install_routes})
         return
 
     if ipsec['dhcp_no_address']:
         with open(DHCP_HOOK_IFLIST, 'w') as f:
             f.write(" ".join(ipsec['dhcp_no_address'].values()))
     elif os.path.exists(DHCP_HOOK_IFLIST):
         os.unlink(DHCP_HOOK_IFLIST)
 
     for path in [swanctl_dir, CERT_PATH, CA_PATH, CRL_PATH, PUBKEY_PATH]:
         if not os.path.exists(path):
             os.mkdir(path, mode=0o755)
 
     if not os.path.exists(KEY_PATH):
         os.mkdir(KEY_PATH, mode=0o700)
 
     if 'l2tp' in ipsec:
         if 'authentication' in ipsec['l2tp'] and 'x509' in ipsec['l2tp']['authentication']:
             generate_pki_files_x509(ipsec['pki'], ipsec['l2tp']['authentication']['x509'])
 
     if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']:
         for rw, rw_conf in ipsec['remote_access']['connection'].items():
 
             if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']:
                 generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509'])
 
     if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']:
         for peer, peer_conf in ipsec['site_to_site']['peer'].items():
             if peer in ipsec['dhcp_no_address']:
                 continue
 
             if peer_conf['authentication']['mode'] == 'x509':
                 generate_pki_files_x509(ipsec['pki'], peer_conf['authentication']['x509'])
             elif peer_conf['authentication']['mode'] == 'rsa':
                 generate_pki_files_rsa(ipsec['pki'], peer_conf['authentication']['rsa'])
 
             local_ip = ''
             if 'local_address' in peer_conf:
                 local_ip = peer_conf['local_address']
             elif 'dhcp_interface' in peer_conf:
                 local_ip = get_dhcp_address(peer_conf['dhcp_interface'])
 
             ipsec['site_to_site']['peer'][peer]['local_address'] = local_ip
 
             if 'tunnel' in peer_conf:
                 for tunnel, tunnel_conf in peer_conf['tunnel'].items():
                     local_prefixes = dict_search_args(tunnel_conf, 'local', 'prefix')
                     remote_prefixes = dict_search_args(tunnel_conf, 'remote', 'prefix')
 
                     if not local_prefixes or not remote_prefixes:
                         continue
 
                     passthrough = None
 
                     for local_prefix in local_prefixes:
                         for remote_prefix in remote_prefixes:
                             local_net = ipaddress.ip_network(local_prefix)
                             remote_net = ipaddress.ip_network(remote_prefix)
                             if local_net.overlaps(remote_net):
                                 if passthrough is None:
                                     passthrough = []
                                 passthrough.append(local_prefix)
 
                     ipsec['site_to_site']['peer'][peer]['tunnel'][tunnel]['passthrough'] = passthrough
 
         # auth psk <tag> dhcp-interface <xxx>
         if jmespath.search('authentication.psk.*.dhcp_interface', ipsec):
             for psk, psk_config in ipsec['authentication']['psk'].items():
                 if 'dhcp_interface' in psk_config:
                     for iface in psk_config['dhcp_interface']:
                         id = get_dhcp_address(iface)
                         if id:
                             ipsec['authentication']['psk'][psk]['id'].append(id)
 
     render(charon_conf, 'ipsec/charon.j2', ipsec)
     render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
     render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
     render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
     render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
 
 def resync_nhrp(ipsec):
     if ipsec and not ipsec['nhrp_exists']:
         return
 
     tmp = run('/usr/libexec/vyos/conf_mode/protocols_nhrp.py')
     if tmp > 0:
         print('ERROR: failed to reapply NHRP settings!')
 
 def apply(ipsec):
     systemd_service = 'strongswan.service'
     if not ipsec:
         call(f'systemctl stop {systemd_service}')
     else:
         call(f'systemctl reload-or-restart {systemd_service}')
 
     resync_nhrp(ipsec)
 
 if __name__ == '__main__':
     try:
         ipsec = get_config()
         verify(ipsec)
         generate(ipsec)
         apply(ipsec)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py
index a039172c4..421ac6997 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -1,296 +1,292 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2018-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/>.
 
 import os
 from sys import exit
 
 from vyos.base import Warning
 from vyos.config import Config
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.utils.process import call
 from vyos.utils.network import check_port_availability
 from vyos.utils.process import is_systemd_service_running
 from vyos.utils.network import is_listen_port_bind_service
 from vyos.utils.dict import dict_search
 from vyos import ConfigError
 from passlib.hash import sha512_crypt
 from time import sleep
 
 from vyos import airbag
 airbag.enable()
 
 cfg_dir        = '/run/ocserv'
 ocserv_conf    = cfg_dir + '/ocserv.conf'
 ocserv_passwd  = cfg_dir + '/ocpasswd'
 ocserv_otp_usr = cfg_dir + '/users.oath'
 radius_cfg     = cfg_dir + '/radiusclient.conf'
 radius_servers = cfg_dir + '/radius_servers'
 
 # Generate hash from user cleartext password
 def get_hash(password):
     return sha512_crypt.hash(password)
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['vpn', 'openconnect']
     if not conf.exists(base):
         return None
 
     ocserv = conf.get_config_dict(base, key_mangling=('-', '_'),
                                   get_first_key=True,
-                                  with_recursive_defaults=True)
-
-    if ocserv:
-        ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                             no_tag_node_value_mangle=True,
-                                             get_first_key=True)
+                                  with_recursive_defaults=True,
+                                  with_pki=True)
 
     return ocserv
 
 def verify(ocserv):
     if ocserv is None:
         return None
     # Check if listen-ports not binded other services
     # It can be only listen by 'ocserv-main'
     for proto, port in ocserv.get('listen_ports').items():
         if check_port_availability(ocserv['listen_address'], int(port), proto) is not True and \
                 not is_listen_port_bind_service(int(port), 'ocserv-main'):
             raise ConfigError(f'"{proto}" port "{port}" is used by another service')
 
     # Check accounting
     if "accounting" in ocserv:
         if "mode" in ocserv["accounting"] and "radius" in ocserv["accounting"]["mode"]:
             if not origin["accounting"]['radius']['server']:
                 raise ConfigError('Openconnect accounting mode radius requires at least one RADIUS server')
             if "authentication" not in ocserv or "mode" not in ocserv["authentication"]:
                 raise ConfigError('Accounting depends on OpenConnect authentication configuration')
             elif "radius" not in ocserv["authentication"]["mode"]:
                 raise ConfigError('RADIUS accounting must be used with RADIUS authentication')
 
     # Check authentication
     if "authentication" in ocserv:
         if "mode" in ocserv["authentication"]:
             if ("local" in ocserv["authentication"]["mode"] and
                 "radius" in ocserv["authentication"]["mode"]):
                     raise ConfigError('OpenConnect authentication modes are mutually-exclusive, remove either local or radius from your configuration')
             if "radius" in ocserv["authentication"]["mode"]:
                 if not ocserv["authentication"]['radius']['server']:
                     raise ConfigError('Openconnect authentication mode radius requires at least one RADIUS server')
             if "local" in ocserv["authentication"]["mode"]:
                 if not ocserv["authentication"]["local_users"]:
                     raise ConfigError('openconnect mode local required at least one user')
                 if not ocserv["authentication"]["local_users"]["username"]:
                     raise ConfigError('openconnect mode local required at least one user')
                 else:
                     # For OTP mode: verify that each local user has an OTP key
                     if "otp" in ocserv["authentication"]["mode"]["local"]:
                         users_wo_key = []
                         for user, user_config in ocserv["authentication"]["local_users"]["username"].items():
                             # User has no OTP key defined
                             if dict_search('otp.key', user_config) == None:
                                 users_wo_key.append(user)
                         if users_wo_key:
                             raise ConfigError(f'OTP enabled, but no OTP key is configured for these users:\n{users_wo_key}')
                     # For password (and default) mode: verify that each local user has password
                     if "password" in ocserv["authentication"]["mode"]["local"] or "otp" not in ocserv["authentication"]["mode"]["local"]:
                         users_wo_pswd = []
                         for user in ocserv["authentication"]["local_users"]["username"]:
                             if not "password" in ocserv["authentication"]["local_users"]["username"][user]:
                                 users_wo_pswd.append(user)
                         if users_wo_pswd:
                             raise ConfigError(f'password required for users:\n{users_wo_pswd}')
 
             # Validate that if identity-based-config is configured all child config nodes are set
             if 'identity_based_config' in ocserv["authentication"]:
                 if 'disabled' not in ocserv["authentication"]["identity_based_config"]:
                     Warning("Identity based configuration files is a 3rd party addition. Use at your own risk, this might break the ocserv daemon!")
                     if 'mode' not in ocserv["authentication"]["identity_based_config"]:
                         raise ConfigError('OpenConnect radius identity-based-config enabled but mode not selected')
                     elif 'group' in ocserv["authentication"]["identity_based_config"]["mode"] and "radius" not in ocserv["authentication"]["mode"]:
                         raise ConfigError('OpenConnect config-per-group must be used with radius authentication')
                     if 'directory' not in ocserv["authentication"]["identity_based_config"]:
                         raise ConfigError('OpenConnect identity-based-config enabled but directory not set')
                     if 'default_config' not in ocserv["authentication"]["identity_based_config"]:
                         raise ConfigError('OpenConnect identity-based-config enabled but default-config not set')
         else:
             raise ConfigError('openconnect authentication mode required')
     else:
         raise ConfigError('openconnect authentication credentials required')
 
     # Check ssl
     if 'ssl' not in ocserv:
         raise ConfigError('openconnect ssl required')
 
     if not ocserv['pki'] or 'certificate' not in ocserv['pki']:
         raise ConfigError('PKI not configured')
 
     ssl = ocserv['ssl']
     if 'certificate' not in ssl:
         raise ConfigError('openconnect ssl certificate required')
 
     cert_name = ssl['certificate']
 
     if cert_name not in ocserv['pki']['certificate']:
         raise ConfigError('Invalid openconnect ssl certificate')
 
     cert = ocserv['pki']['certificate'][cert_name]
 
     if 'certificate' not in cert:
         raise ConfigError('Missing certificate in PKI')
 
     if 'private' not in cert or 'key' not in cert['private']:
         raise ConfigError('Missing private key in PKI')
 
     if 'ca_certificate' in ssl:
         if 'ca' not in ocserv['pki']:
             raise ConfigError('PKI not configured')
 
         if ssl['ca_certificate'] not in ocserv['pki']['ca']:
             raise ConfigError('Invalid openconnect ssl CA certificate')
 
     # Check network settings
     if "network_settings" in ocserv:
         if "push_route" in ocserv["network_settings"]:
             # Replace default route
             if "0.0.0.0/0" in ocserv["network_settings"]["push_route"]:
                 ocserv["network_settings"]["push_route"].remove("0.0.0.0/0")
                 ocserv["network_settings"]["push_route"].append("default")
         else:
             ocserv["network_settings"]["push_route"] = ["default"]
     else:
         raise ConfigError('openconnect network settings required')
 
 def generate(ocserv):
     if not ocserv:
         return None
 
     if "radius" in ocserv["authentication"]["mode"]:
         if dict_search(ocserv, 'accounting.mode.radius'):
             # Render radius client configuration
             render(radius_cfg, 'ocserv/radius_conf.j2', ocserv)
             merged_servers = ocserv["accounting"]["radius"]["server"] | ocserv["authentication"]["radius"]["server"]
             # Render radius servers
             # Merge the accounting and authentication servers into a single dictionary
             render(radius_servers, 'ocserv/radius_servers.j2', {'server': merged_servers})
         else:
             # Render radius client configuration
             render(radius_cfg, 'ocserv/radius_conf.j2', ocserv)
             # Render radius servers
             render(radius_servers, 'ocserv/radius_servers.j2', ocserv["authentication"]["radius"])
     elif "local" in ocserv["authentication"]["mode"]:
         # if mode "OTP", generate OTP users file parameters
         if "otp" in ocserv["authentication"]["mode"]["local"]:
             if "local_users" in ocserv["authentication"]:
                 for user in ocserv["authentication"]["local_users"]["username"]:
                     # OTP token type from CLI parameters:
                     otp_interval = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("interval"))
                     token_type = ocserv["authentication"]["local_users"]["username"][user]["otp"].get("token_type")
                     otp_length = str(ocserv["authentication"]["local_users"]["username"][user]["otp"].get("otp_length"))
                     if token_type == "hotp-time":
                         otp_type = "HOTP/T" + otp_interval
                     elif token_type == "hotp-event":
                         otp_type = "HOTP/E"
                     else:
                         otp_type = "HOTP/T" + otp_interval
                     ocserv["authentication"]["local_users"]["username"][user]["otp"]["token_tmpl"] = otp_type + "/" + otp_length
         # if there is a password, generate hash
         if "password" in ocserv["authentication"]["mode"]["local"] or not "otp" in ocserv["authentication"]["mode"]["local"]:
             if "local_users" in ocserv["authentication"]:
                 for user in ocserv["authentication"]["local_users"]["username"]:
                     ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
 
         if "password-otp" in ocserv["authentication"]["mode"]["local"]:
             # Render local users ocpasswd
             render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
             # Render local users OTP keys
             render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"])
         elif "password" in ocserv["authentication"]["mode"]["local"]:
             # Render local users ocpasswd
             render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
         elif "otp" in ocserv["authentication"]["mode"]["local"]:
             # Render local users OTP keys
             render(ocserv_otp_usr, 'ocserv/ocserv_otp_usr.j2', ocserv["authentication"]["local_users"])
         else:
             # Render local users ocpasswd
             render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
     else:
         if "local_users" in ocserv["authentication"]:
             for user in ocserv["authentication"]["local_users"]["username"]:
                 ocserv["authentication"]["local_users"]["username"][user]["hash"] = get_hash(ocserv["authentication"]["local_users"]["username"][user]["password"])
             # Render local users
             render(ocserv_passwd, 'ocserv/ocserv_passwd.j2', ocserv["authentication"]["local_users"])
 
     if "ssl" in ocserv:
         cert_file_path = os.path.join(cfg_dir, 'cert.pem')
         cert_key_path = os.path.join(cfg_dir, 'cert.key')
         ca_cert_file_path = os.path.join(cfg_dir, 'ca.pem')
 
         if 'certificate' in ocserv['ssl']:
             cert_name = ocserv['ssl']['certificate']
             pki_cert = ocserv['pki']['certificate'][cert_name]
 
             with open(cert_file_path, 'w') as f:
                 f.write(wrap_certificate(pki_cert['certificate']))
 
             if 'private' in pki_cert and 'key' in pki_cert['private']:
                 with open(cert_key_path, 'w') as f:
                     f.write(wrap_private_key(pki_cert['private']['key']))
 
         if 'ca_certificate' in ocserv['ssl']:
             ca_name = ocserv['ssl']['ca_certificate']
             pki_ca_cert = ocserv['pki']['ca'][ca_name]
 
             with open(ca_cert_file_path, 'w') as f:
                 f.write(wrap_certificate(pki_ca_cert['certificate']))
 
     # Render config
     render(ocserv_conf, 'ocserv/ocserv_config.j2', ocserv)
 
 
 def apply(ocserv):
     if not ocserv:
         call('systemctl stop ocserv.service')
         for file in [ocserv_conf, ocserv_passwd, ocserv_otp_usr]:
             if os.path.exists(file):
                 os.unlink(file)
     else:
         call('systemctl reload-or-restart ocserv.service')
         counter = 0
         while True:
             # exit early when service runs
             if is_systemd_service_running("ocserv.service"):
                 break
             sleep(0.250)
             if counter > 5:
                 raise ConfigError('openconnect failed to start, check the logs for details')
                 break
             counter += 1
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index ac053cc76..6bf9307e1 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -1,173 +1,170 @@
 #!/usr/bin/env python3
 #
-# Copyright (C) 2018-2023 VyOS maintainers and contributors
+# Copyright (C) 2018-2024 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/>.
 
 import os
 
 from sys import exit
 
 from vyos.config import Config
 from vyos.configdict import get_accel_dict
 from vyos.configdict import dict_merge
 from vyos.pki import wrap_certificate
 from vyos.pki import wrap_private_key
 from vyos.template import render
 from vyos.utils.process import call
 from vyos.utils.network import check_port_availability
 from vyos.utils.dict import dict_search
 from vyos.accel_ppp_util import verify_accel_ppp_base_service
 from vyos.accel_ppp_util import verify_accel_ppp_ip_pool
 from vyos.accel_ppp_util import get_pools_in_order
 from vyos.utils.network import is_listen_port_bind_service
 from vyos.utils.file import write_file
 from vyos import ConfigError
 from vyos import airbag
 airbag.enable()
 
 cfg_dir = '/run/accel-pppd'
 sstp_conf = '/run/accel-pppd/sstp.conf'
 sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets'
 
 cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem')
 cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key')
 ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem')
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
     base = ['vpn', 'sstp']
     if not conf.exists(base):
         return None
 
     # retrieve common dictionary keys
-    sstp = get_accel_dict(conf, base, sstp_chap_secrets)
+    sstp = get_accel_dict(conf, base, sstp_chap_secrets, with_pki=True)
     if dict_search('client_ip_pool', sstp):
         # Multiple named pools require ordered values T5099
         sstp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', sstp))
-    if sstp:
-        sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
-                                           get_first_key=True,
-                                           no_tag_node_value_mangle=True)
+
     sstp['server_type'] = 'sstp'
     return sstp
 
 
 def verify(sstp):
     if not sstp:
         return None
 
     port = sstp.get('port')
     proto = 'tcp'
     if check_port_availability('0.0.0.0', int(port), proto) is not True and \
             not is_listen_port_bind_service(int(port), 'accel-pppd'):
         raise ConfigError(f'"{proto}" port "{port}" is used by another service')
 
     verify_accel_ppp_base_service(sstp)
 
     if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp:
         raise ConfigError('Client IP subnet required')
 
     verify_accel_ppp_ip_pool(sstp)
     #
     # SSL certificate checks
     #
     if not sstp['pki']:
         raise ConfigError('PKI is not configured')
 
     if 'ssl' not in sstp:
         raise ConfigError('SSL missing on SSTP config')
 
     ssl = sstp['ssl']
 
     # CA
     if 'ca_certificate' not in ssl:
         raise ConfigError('SSL CA certificate missing on SSTP config')
 
     ca_name = ssl['ca_certificate']
 
     if ca_name not in sstp['pki']['ca']:
         raise ConfigError('Invalid CA certificate on SSTP config')
 
     if 'certificate' not in sstp['pki']['ca'][ca_name]:
         raise ConfigError('Missing certificate data for CA certificate on SSTP config')
 
     # Certificate
     if 'certificate' not in ssl:
         raise ConfigError('SSL certificate missing on SSTP config')
 
     cert_name = ssl['certificate']
 
     if cert_name not in sstp['pki']['certificate']:
         raise ConfigError('Invalid certificate on SSTP config')
 
     pki_cert = sstp['pki']['certificate'][cert_name]
 
     if 'certificate' not in pki_cert:
         raise ConfigError('Missing certificate data for certificate on SSTP config')
 
     if 'private' not in pki_cert or 'key' not in pki_cert['private']:
         raise ConfigError('Missing private key for certificate on SSTP config')
 
     if 'password_protected' in pki_cert['private']:
         raise ConfigError('Encrypted private key is not supported on SSTP config')
 
 def generate(sstp):
     if not sstp:
         return None
 
     # accel-cmd reload doesn't work so any change results in a restart of the daemon
     render(sstp_conf, 'accel-ppp/sstp.config.j2', sstp)
 
     cert_name = sstp['ssl']['certificate']
     pki_cert = sstp['pki']['certificate'][cert_name]
 
     ca_cert_name = sstp['ssl']['ca_certificate']
     pki_ca = sstp['pki']['ca'][ca_cert_name]
     write_file(cert_file_path, wrap_certificate(pki_cert['certificate']))
     write_file(cert_key_path, wrap_private_key(pki_cert['private']['key']))
     write_file(ca_cert_file_path, wrap_certificate(pki_ca['certificate']))
 
     if dict_search('authentication.mode', sstp) == 'local':
         render(sstp_chap_secrets, 'accel-ppp/chap-secrets.config_dict.j2',
                sstp, permission=0o640)
     else:
         if os.path.exists(sstp_chap_secrets):
              os.unlink(sstp_chap_secrets)
 
     return sstp
 
 def apply(sstp):
     if not sstp:
         call('systemctl stop accel-ppp@sstp.service')
         for file in [sstp_chap_secrets, sstp_conf]:
             if os.path.exists(file):
                 os.unlink(file)
 
         return None
 
     call('systemctl restart accel-ppp@sstp.service')
 
 
 if __name__ == '__main__':
     try:
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)