diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py
index 7ca82f86f..b82805661 100755
--- a/smoketest/scripts/cli/test_nat.py
+++ b/smoketest/scripts/cli/test_nat.py
@@ -1,160 +1,170 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020 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 jmespath
 import json
 import unittest
 
 from vyos.configsession import ConfigSession
 from vyos.configsession import ConfigSessionError
 from vyos.util import cmd
 from vyos.util import dict_search
 
 base_path = ['nat']
 src_path = base_path + ['source']
 dst_path = base_path + ['destination']
 
 class TestNAT(unittest.TestCase):
     def setUp(self):
         # ensure we can also run this test on a live system - so lets clean
         # out the current configuration :)
         self.session = ConfigSession(os.getpid())
         self.session.delete(base_path)
 
     def tearDown(self):
         self.session.delete(base_path)
         self.session.commit()
 
     def test_snat(self):
         rules = ['100', '110', '120', '130', '200', '210', '220', '230']
         outbound_iface_100 = 'eth0'
         outbound_iface_200 = 'eth1'
         for rule in rules:
             network = f'192.168.{rule}.0/24'
             # depending of rule order we check either for source address for NAT
             # or configured destination address for NAT
             if int(rule) < 200:
                 self.session.set(src_path + ['rule', rule, 'source', 'address', network])
                 self.session.set(src_path + ['rule', rule, 'outbound-interface', outbound_iface_100])
                 self.session.set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])
             else:
                 self.session.set(src_path + ['rule', rule, 'destination', 'address', network])
                 self.session.set(src_path + ['rule', rule, 'outbound-interface', outbound_iface_200])
                 self.session.set(src_path + ['rule', rule, 'exclude'])
 
         self.session.commit()
 
         tmp = cmd('sudo nft -j list table nat')
         data_json = jmespath.search('nftables[?rule].rule[?chain]', json.loads(tmp))
 
         for idx in range(0, len(data_json)):
             rule = str(rules[idx])
             data = data_json[idx]
             network = f'192.168.{rule}.0/24'
 
             self.assertEqual(data['chain'], 'POSTROUTING')
             self.assertEqual(data['comment'], f'SRC-NAT-{rule}')
             self.assertEqual(data['family'], 'ip')
             self.assertEqual(data['table'], 'nat')
 
             iface = dict_search('match.right', data['expr'][0])
             direction = dict_search('match.left.payload.field', data['expr'][1])
             address = dict_search('match.right.prefix.addr', data['expr'][1])
             mask = dict_search('match.right.prefix.len', data['expr'][1])
 
             if int(rule) < 200:
                 self.assertEqual(direction, 'saddr')
                 self.assertEqual(iface, outbound_iface_100)
                 # check for masquerade keyword
                 self.assertIn('masquerade', data['expr'][3])
             else:
                 self.assertEqual(direction, 'daddr')
                 self.assertEqual(iface, outbound_iface_200)
                 # check for return keyword due to 'exclude'
                 self.assertIn('return', data['expr'][3])
 
             self.assertEqual(f'{address}/{mask}', network)
 
     def test_dnat(self):
         rules = ['100', '110', '120', '130', '200', '210', '220', '230']
         inbound_iface_100 = 'eth0'
         inbound_iface_200 = 'eth1'
         inbound_proto_100 = 'udp'
         inbound_proto_200 = 'tcp'
 
         for rule in rules:
             port = f'10{rule}'
             self.session.set(dst_path + ['rule', rule, 'source', 'port', port])
             self.session.set(dst_path + ['rule', rule, 'translation', 'address', '192.0.2.1'])
             self.session.set(dst_path + ['rule', rule, 'translation', 'port', port])
             if int(rule) < 200:
                 self.session.set(dst_path + ['rule', rule, 'protocol', inbound_proto_100])
                 self.session.set(dst_path + ['rule', rule, 'inbound-interface', inbound_iface_100])
             else:
                 self.session.set(dst_path + ['rule', rule, 'protocol', inbound_proto_200])
                 self.session.set(dst_path + ['rule', rule, 'inbound-interface', inbound_iface_200])
 
         self.session.commit()
 
         tmp = cmd('sudo nft -j list table nat')
         data_json = jmespath.search('nftables[?rule].rule[?chain]', json.loads(tmp))
 
         for idx in range(0, len(data_json)):
             rule = str(rules[idx])
             data = data_json[idx]
             port = int(f'10{rule}')
 
             self.assertEqual(data['chain'], 'PREROUTING')
             self.assertEqual(data['comment'].split()[0], f'DST-NAT-{rule}')
             self.assertEqual(data['family'], 'ip')
             self.assertEqual(data['table'], 'nat')
 
             iface = dict_search('match.right', data['expr'][0])
             direction = dict_search('match.left.payload.field', data['expr'][1])
             protocol = dict_search('match.left.payload.protocol', data['expr'][1])
             dnat_addr = dict_search('dnat.addr', data['expr'][3])
             dnat_port = dict_search('dnat.port', data['expr'][3])
 
             self.assertEqual(direction, 'sport')
             self.assertEqual(dnat_addr, '192.0.2.1')
             self.assertEqual(dnat_port, port)
             if int(rule) < 200:
                 self.assertEqual(iface, inbound_iface_100)
                 self.assertEqual(protocol, inbound_proto_100)
             else:
                 self.assertEqual(iface, inbound_iface_200)
 
-
     def test_snat_required_translation_address(self):
         # T2813: Ensure translation address is specified
         rule = '5'
         self.session.set(src_path + ['rule', rule, 'source', 'address', '192.0.2.0/24'])
 
         # check validate() - outbound-interface must be defined
         with self.assertRaises(ConfigSessionError):
             self.session.commit()
         self.session.set(src_path + ['rule', rule, 'outbound-interface', 'eth0'])
 
         # check validate() - translation address not specified
         with self.assertRaises(ConfigSessionError):
             self.session.commit()
 
         self.session.set(src_path + ['rule', rule, 'translation', 'address', 'masquerade'])
         self.session.commit()
 
+    def test_nat_no_rules(self):
+        # T3206: deleting all rules but keep the direction 'destination' or
+        # 'source' resulteds in KeyError: 'rule'.
+        #
+        # Test that both 'nat destination' and 'nat source' nodes can exist
+        # without any rule
+        self.session.set(src_path)
+        self.session.set(dst_path)
+        self.session.commit()
+
+
 if __name__ == '__main__':
     unittest.main(verbosity=2)
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 1ccec3d2e..2d98cb11b 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -1,204 +1,204 @@
 #!/usr/bin/env python3
 #
 # Copyright (C) 2020 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 jmespath
 import json
 import os
 
 from distutils.version import LooseVersion
 from platform import release as kernel_version
 from sys import exit
 from netifaces import interfaces
 
 from vyos.config import Config
 from vyos.configdict import dict_merge
 from vyos.template import render
 from vyos.util import cmd
 from vyos.util import check_kmod
 from vyos.util import dict_search
 from vyos.validate import is_addr_assigned
 from vyos.xml import defaults
 from vyos import ConfigError
 
 from vyos import airbag
 airbag.enable()
 
 if LooseVersion(kernel_version()) > LooseVersion('5.1'):
     k_mod = ['nft_nat', 'nft_chain_nat']
 else:
     k_mod = ['nft_nat', 'nft_chain_nat_ipv4']
 
 iptables_nat_config = '/tmp/vyos-nat-rules.nft'
 
 def get_handler(json, chain, target):
     """ Get nftable rule handler number of given chain/target combination.
     Handler is required when adding NAT/Conntrack helper targets """
     for x in json:
         if x['chain'] != chain:
             continue
         if x['target'] != target:
             continue
         return x['handle']
 
     return None
 
 
 def verify_rule(config, err_msg):
     """ Common verify steps used for both source and destination NAT """
 
     if (dict_search('translation.port', config) != None or
         dict_search('destination.port', config) != None or
         dict_search('source.port', config)):
 
         if config['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
             raise ConfigError(f'{err_msg}\n' \
                               'ports can only be specified when protocol is '\
                               'either tcp, udp or tcp_udp!')
 
         if '/' in (dict_search('translation.address', config) or []):
             raise ConfigError(f'{err_msg}\n' \
                              'Cannot use ports with an IPv4net type translation address as it\n' \
                              'statically maps a whole network of addresses onto another\n' \
                              'network of addresses')
 
 def get_config(config=None):
     if config:
         conf = config
     else:
         conf = Config()
 
     base = ['nat']
     nat = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True)
 
     # T2665: we must add the tagNode defaults individually until this is
     # moved to the base class
     for direction in ['source', 'destination']:
         if direction in nat:
             default_values = defaults(base + [direction, 'rule'])
-            for rule in nat[direction]['rule']:
+            for rule in dict_search(f'{direction}.rule', nat) or []:
                 nat[direction]['rule'][rule] = dict_merge(default_values,
                     nat[direction]['rule'][rule])
 
 
     # read in current nftable (once) for further processing
     tmp = cmd('nft -j list table raw')
     nftable_json = json.loads(tmp)
 
     # condense the full JSON table into a list with only relevand informations
     pattern = 'nftables[?rule].rule[?expr[].jump].{chain: chain, handle: handle, target: expr[].jump.target | [0]}'
     condensed_json = jmespath.search(pattern, nftable_json)
 
     if not conf.exists(base):
         nat['helper_functions'] = 'remove'
 
         # Retrieve current table handler positions
         nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_HELPER')
         nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK')
         nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_HELPER')
         nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'NAT_CONNTRACK')
         nat['deleted'] = ''
         return nat
 
     # check if NAT connection tracking helpers need to be set up - this has to
     # be done only once
     if not get_handler(condensed_json, 'PREROUTING', 'NAT_CONNTRACK'):
         nat['helper_functions'] = 'add'
 
         # Retrieve current table handler positions
         nat['pre_ct_ignore'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_IGNORE')
         nat['pre_ct_conntrack'] = get_handler(condensed_json, 'PREROUTING', 'VYATTA_CT_PREROUTING_HOOK')
         nat['out_ct_ignore'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_IGNORE')
         nat['out_ct_conntrack'] = get_handler(condensed_json, 'OUTPUT', 'VYATTA_CT_OUTPUT_HOOK')
 
     return nat
 
 def verify(nat):
     if not nat or 'deleted' in nat:
         # no need to verify the CLI as NAT is going to be deactivated
         return None
 
     if 'helper_functions' in nat:
         if not (nat['pre_ct_ignore'] or nat['pre_ct_conntrack'] or nat['out_ct_ignore'] or nat['out_ct_conntrack']):
             raise Exception('could not determine nftable ruleset handlers')
 
     if dict_search('source.rule', nat):
         for rule, config in dict_search('source.rule', nat).items():
             err_msg = f'Source NAT configuration error in rule {rule}:'
             if 'outbound_interface' not in config:
                 raise ConfigError(f'{err_msg}\n' \
                                   'outbound-interface not specified')
             else:
                 if config['outbound_interface'] not in 'any' and config['outbound_interface'] not in interfaces():
                     print(f'WARNING: rule "{rule}" interface "{config["outbound_interface"]}" does not exist on this system')
 
 
             addr = dict_search('translation.address', config)
             if addr != None:
                 if addr != 'masquerade':
                     for ip in addr.split('-'):
                         if not is_addr_assigned(ip):
                             print(f'WARNING: IP address {ip} does not exist on the system!')
             elif 'exclude' not in config:
                 raise ConfigError(f'{err_msg}\n' \
                                   'translation address not specified')
 
             # common rule verification
             verify_rule(config, err_msg)
 
 
     if dict_search('destination.rule', nat):
         for rule, config in dict_search('destination.rule', nat).items():
             err_msg = f'Destination NAT configuration error in rule {rule}:'
 
             if 'inbound_interface' not in config:
                 raise ConfigError(f'{err_msg}\n' \
                                   'inbound-interface not specified')
             else:
                 if config['inbound_interface'] not in 'any' and config['inbound_interface'] not in interfaces():
                     print(f'WARNING: rule "{rule}" interface "{config["inbound_interface"]}" does not exist on this system')
 
 
             if dict_search('translation.address', config) == None and 'exclude' not in config:
                 raise ConfigError(f'{err_msg}\n' \
                                   'translation address not specified')
 
             # common rule verification
             verify_rule(config, err_msg)
 
     return None
 
 def generate(nat):
     render(iptables_nat_config, 'firewall/nftables-nat.tmpl', nat,
            permission=0o755)
     return None
 
 def apply(nat):
     cmd(f'{iptables_nat_config}')
     if os.path.isfile(iptables_nat_config):
         os.unlink(iptables_nat_config)
 
     return None
 
 if __name__ == '__main__':
     try:
         check_kmod(k_mod)
         c = get_config()
         verify(c)
         generate(c)
         apply(c)
     except ConfigError as e:
         print(e)
         exit(1)