diff --git a/debian/control b/debian/control
index 726a083f2..dddc4e14c 100644
--- a/debian/control
+++ b/debian/control
@@ -1,332 +1,336 @@
 Source: vyos-1x
 Section: contrib/net
 Priority: extra
 Maintainer: VyOS Package Maintainers <maintainers@vyos.net>
 Build-Depends:
   debhelper (>= 9),
   dh-python,
   fakeroot,
   gcc,
   iproute2,
   libvyosconfig0 (>= 0.0.7),
   libzmq3-dev,
   python3 (>= 3.10),
 # For generating command definitions
   python3-lxml,
   python3-xmltodict,
 # For running tests
   python3-coverage,
   python3-netifaces,
   python3-nose,
   python3-jinja2,
   python3-psutil,
   python3-setuptools,
   python3-sphinx,
   quilt,
   whois
 Standards-Version: 3.9.6
 
 Package: vyos-1x
 Architecture: amd64 arm64
 Pre-Depends:
   libnss-tacplus [amd64],
   libpam-tacplus [amd64],
   libpam-radius-auth [amd64]
 Depends:
 ## Fundamentals
   ${python3:Depends} (>= 3.10),
   libvyosconfig0,
   vyatta-bash,
   vyatta-cfg,
   vyos-http-api-tools,
   vyos-utils,
 ## End of Fundamentals
 ## Python libraries used in multiple modules and scripts
   python3,
   python3-cryptography,
   python3-hurry.filesize,
   python3-inotify,
   python3-jinja2,
   python3-jmespath,
   python3-netaddr,
   python3-netifaces,
   python3-paramiko,
   python3-passlib,
   python3-psutil,
   python3-pyhumps,
   python3-pystache,
   python3-pyudev,
   python3-six,
   python3-tabulate,
   python3-voluptuous,
   python3-xmltodict,
   python3-zmq,
 ## End of Python libraries
 ## Basic System services and utilities
   sudo,
   systemd,
   bsdmainutils,
   openssl,
   curl,
   dbus,
   file,
   iproute2 (>= 6.0.0),
   linux-cpupower,
 # ipaddrcheck is widely used in IP value validators
   ipaddrcheck,
   ethtool,
   fdisk,
   lm-sensors,
   procps,
   netplug,
   sed,
   ssl-cert,
   tuned,
   beep,
   wide-dhcpv6-client,
 # Generic colorizer
   grc,
 ## End of System services and utilities
 ## For the installer
 # Image signature verification tool
   minisign,
 # Live filesystem tools
   squashfs-tools,
   fuse-overlayfs,
 ## End installer
   auditd,
   iputils-arping,
   isc-dhcp-client,
 # For "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server"
   accel-ppp,
 # End "vpn pptp", "vpn l2tp", "vpn sstp", "service ipoe-server"
   avahi-daemon,
   conntrack,
   conntrackd,
 ## Conf mode features
 # For "interfaces wireless"
   hostapd,
   hsflowd,
   iw,
   wireless-regdb,
   wpasupplicant (>= 0.6.7),
 # End "interfaces wireless"
 # For "interfaces wwan"
   modemmanager,
   usb-modeswitch,
   libqmi-utils,
 # End "interfaces wwan"
 # For "interfaces openvpn"
   openvpn,
   openvpn-auth-ldap,
   openvpn-auth-radius,
   openvpn-otp,
   libpam-google-authenticator,
 # End "interfaces openvpn"
 # For "interfaces wireguard"
   wireguard-tools,
   qrencode,
 # End "interfaces wireguard"
 # For "interfaces pppoe"
   pppoe,
 # End "interfaces pppoe"
 # For "interfaces sstpc"
   sstp-client,
 # End "interfaces sstpc"
 # For "protocols *"
   frr (>= 7.5),
   frr-pythontools,
   frr-rpki-rtrlib,
   frr-snmp,
 # End "protocols *"
 # For "protocols nhrp" (part of DMVPN)
   opennhrp,
 # End "protocols nhrp"
 # For "protocols igmp-proxy"
   igmpproxy,
 # End "protocols igmp-proxy"
 # For "pki"
   certbot,
 # End "pki"
 # For "service console-server"
   conserver-client,
   conserver-server,
   console-data,
   dropbear,
 # End "service console-server"
 # For "service aws glb"
   aws-gwlbtun,
 # For "service dns dynamic"
   ddclient (>= 3.11.1),
 # End "service dns dynamic"
 # # For "service ids"
   fastnetmon [amd64],
 # End "service ids"
 # # For "service ndp-proxy"
   ndppd,
 # End "service ndp-proxy"
 # For "service router-advert"
   radvd,
 # End "service route-advert"
 # For "high-availability reverse-proxy"
   haproxy,
 # End "high-availability reverse-proxy"
 # For "service dhcp-relay"
   isc-dhcp-relay,
 # For "service dhcp-server"
   kea,
 # End "service dhcp-server"
 # For "service lldp"
   lldpd,
 # End "service lldp"
 # For "service https"
   nginx-light,
 # End "service https"
 # For "service ssh"
   openssh-server,
   sshguard,
 # End "service ssh"
 # For "service salt-minion"
   salt-minion,
 # End "service salt-minion"
 # For "service snmp"
   snmp,
   snmpd,
 # End "service snmp"
 # For "service upnp"
   miniupnpd-nftables,
 # End "service upnp"
 # For "service webproxy"
   squid,
   squidclient,
   squidguard,
 # End "service webproxy"
 # For "service monitoring telegraf"
   telegraf (>= 1.20),
 # End "service monitoring telegraf"
 # For "service monitoring zabbix-agent"
   zabbix-agent2,
 # End "service monitoring zabbix-agent"
 # For "service tftp-server"
   tftpd-hpa,
 # End "service tftp-server"
 # For "service dns forwarding"
   pdns-recursor,
 # End "service dns forwarding"
 # For "service sla owamp"
   owamp-client,
   owamp-server,
 # End "service sla owamp"
 # For "service sla twamp"
   twamp-client,
   twamp-server,
 # End "service sla twamp"
 # For "service broadcast-relay"
   udp-broadcast-relay,
 # End "service broadcast-relay"
 # For "high-availability vrrp"
   keepalived (>=2.0.5),
 # End "high-availability-vrrp"
 # For "system task-scheduler"
   cron,
 # End "system task-scheduler"
 # For "system lcd"
   lcdproc,
   lcdproc-extra-drivers,
 # End "system lcd"
 # For "system config-management commit-archive"
   git,
 # End "system config-management commit-archive"
 # For firewall
   libndp-tools,
   libnetfilter-conntrack3,
   libnfnetlink0,
   nfct,
   nftables (>= 0.9.3),
 # For "vpn ipsec"
   strongswan (>= 5.9),
   strongswan-swanctl (>= 5.9),
   charon-systemd,
   libcharon-extra-plugins (>=5.9),
   libcharon-extauth-plugins (>=5.9),
   libstrongswan-extra-plugins (>=5.9),
   libstrongswan-standard-plugins (>=5.9),
   python3-vici (>= 5.7.2),
 # End "vpn ipsec"
 # For "nat64"
   jool,
 # End "nat64"
 # For "system ntp"
   chrony,
 # End "system ntp"
 # For "vpn openconnect"
   ocserv,
 # End "vpn openconnect"
 # For "system flow-accounting"
   pmacct (>= 1.6.0),
 # End "system flow-accounting"
 # For container
   podman,
   netavark,
   aardvark-dns,
 # iptables is only used for containers now, not the the firewall CLI
   iptables,
 # End container
 ## End Configuration mode
 ## Operational mode
 # Used for hypervisor model in "run show version"
   hvinfo,
 # For "run traceroute"
   traceroute,
 # For "run monitor traffic"
   tcpdump,
 # End "run monitor traffic"
 # For "show hardware dmi"
   dmidecode,
 # For "run show hardware storage smart"
   smartmontools,
 # For "run show hardware scsi"
   lsscsi,
 # For "run show hardware pci"
   pciutils,
 # For "show hardware usb"
   usbutils,
 # For "run show hardware storage nvme"
   nvme-cli,
 # For "run monitor bandwidth-test"
   iperf,
   iperf3,
 # End "run monitor bandwidth-test"
 # For "run wake-on-lan"
   etherwake,
 # For "run force ipv6-nd"
   ndisc6,
 # For "run monitor bandwidth"
   bmon,
 # End Operational mode
+## TPM tools
+  cryptsetup,
+  tpm2-tools,
+## End TPM tools
 ## Optional utilities
   easy-rsa,
   tcptraceroute,
   mtr-tiny,
   telnet,
   stunnel4,
   uidmap
 ## End optional utilities
 Description: VyOS configuration scripts and data
  VyOS configuration scripts, interface definitions, and everything
 
 Package: vyos-1x-vmware
 Architecture: amd64
 Depends:
  vyos-1x,
  open-vm-tools
 Description: VyOS configuration scripts and data for VMware
  Adds configuration files required for VyOS running on VMware hosts.
 
 Package: vyos-1x-smoketest
 Architecture: all
 Depends:
  skopeo,
  snmp,
  vyos-1x
 Description: VyOS build sanity checking toolkit
diff --git a/op-mode-definitions/crypt.xml.in b/op-mode-definitions/crypt.xml.in
new file mode 100644
index 000000000..105592a1a
--- /dev/null
+++ b/op-mode-definitions/crypt.xml.in
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+<interfaceDefinition>
+  <node name="encryption">
+    <properties>
+      <help>Manage config encryption</help>
+    </properties>
+    <children>
+      <node name="disable">
+        <properties>
+          <help>Disable config encryption using TPM or recovery key</help>
+        </properties>
+        <command>sudo ${vyos_libexec_dir}/vyos-config-encrypt.py --disable</command>
+      </node>
+      <node name="enable">
+        <properties>
+          <help>Enable config encryption using TPM</help>
+        </properties>
+        <command>sudo ${vyos_libexec_dir}/vyos-config-encrypt.py --enable</command>
+      </node>
+      <node name="load">
+        <properties>
+          <help>Load encrypted config volume using TPM or recovery key</help>
+        </properties>
+        <command>sudo ${vyos_libexec_dir}/vyos-config-encrypt.py --load</command>
+      </node>
+    </children>
+  </node>
+</interfaceDefinition>
diff --git a/python/vyos/tpm.py b/python/vyos/tpm.py
new file mode 100644
index 000000000..f120e10c4
--- /dev/null
+++ b/python/vyos/tpm.py
@@ -0,0 +1,98 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 tempfile
+
+from vyos.util import rc_cmd
+
+default_pcrs = ['0','2','4','7']
+tpm_handle = 0x81000000
+
+def init_tpm(clear=False):
+    """
+    Initialize TPM
+    """
+    code, output = rc_cmd('tpm2_startup' + (' -c' if clear else ''))
+    if code != 0:
+        raise Exception('init_tpm: Failed to initialize TPM')
+
+def clear_tpm_key():
+    """
+    Clear existing key on TPM
+    """
+    code, output = rc_cmd(f'tpm2_evictcontrol -C o -c {tpm_handle}')
+    if code != 0:
+        raise Exception('clear_tpm_key: Failed to clear TPM key')
+
+def read_tpm_key(index=0, pcrs=default_pcrs):
+    """
+    Read existing key on TPM
+    """
+    with tempfile.TemporaryDirectory() as tpm_dir:
+        pcr_str = ",".join(pcrs)
+
+        tpm_key_file = os.path.join(tpm_dir, 'tpm_key.key')
+        code, output = rc_cmd(f'tpm2_unseal -c {tpm_handle + index} -p pcr:sha256:{pcr_str} -o {tpm_key_file}')
+        if code != 0:
+            raise Exception('read_tpm_key: Failed to read key from TPM')
+
+        with open(tpm_key_file, 'rb') as f:
+            tpm_key = f.read()
+
+        return tpm_key
+
+def write_tpm_key(key, index=0, pcrs=default_pcrs):
+    """
+    Saves key to TPM
+    """
+    with tempfile.TemporaryDirectory() as tpm_dir:
+        pcr_str = ",".join(pcrs)
+
+        policy_file = os.path.join(tpm_dir, 'policy.digest')
+        code, output = rc_cmd(f'tpm2_createpolicy --policy-pcr -l sha256:{pcr_str} -L {policy_file}')
+        if code != 0:
+            raise Exception('write_tpm_key: Failed to create policy digest')
+
+        primary_context_file = os.path.join(tpm_dir, 'primary.ctx')
+        code, output = rc_cmd(f'tpm2_createprimary -C e -g sha256 -G rsa -c {primary_context_file}')
+        if code != 0:
+            raise Exception('write_tpm_key: Failed to create primary key')
+
+        key_file = os.path.join(tpm_dir, 'crypt.key')
+        with open(key_file, 'wb') as f:
+            f.write(key)
+
+        public_obj = os.path.join(tpm_dir, 'obj.pub')
+        private_obj = os.path.join(tpm_dir, 'obj.key')
+        code, output = rc_cmd(
+            f'tpm2_create -g sha256 \
+            -u {public_obj} -r {private_obj} \
+            -C {primary_context_file} -L {policy_file} -i {key_file}')
+
+        if code != 0:
+            raise Exception('write_tpm_key: Failed to create object')
+
+        load_context_file = os.path.join(tpm_dir, 'load.ctx')
+        code, output = rc_cmd(f'tpm2_load -C {primary_context_file} -u {public_obj} -r {private_obj} -c {load_context_file}')
+
+        if code != 0:
+            raise Exception('write_tpm_key: Failed to load object')
+
+        code, output = rc_cmd(f'tpm2_evictcontrol -c {load_context_file} -C o {tpm_handle + index}')
+
+        if code != 0:
+            raise Exception('write_tpm_key: Failed to write object to TPM')
diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py
new file mode 100755
index 000000000..8f7359767
--- /dev/null
+++ b/src/helpers/vyos-config-encrypt.py
@@ -0,0 +1,276 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 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 shutil
+import sys
+
+from argparse import ArgumentParser
+from cryptography.fernet import Fernet
+from tempfile import NamedTemporaryFile
+from tempfile import TemporaryDirectory
+
+from vyos.tpm import clear_tpm_key
+from vyos.tpm import init_tpm
+from vyos.tpm import read_tpm_key
+from vyos.tpm import write_tpm_key
+from vyos.util import ask_input
+from vyos.util import ask_yes_no
+from vyos.util import cmd
+
+persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
+mount_paths = ['/config', '/opt/vyatta/etc/config']
+dm_device = '/dev/mapper/vyos_config'
+
+def is_opened():
+    return os.path.exists(dm_device)
+
+def get_current_image():
+    with open('/proc/cmdline', 'r') as f:
+        args = f.read().split(" ")
+        for arg in args:
+            if 'vyos-union' in arg:
+                k, v = arg.split("=")
+                path_split = v.split("/")
+                return path_split[-1]
+    return None
+
+def load_config(key):
+    if not key:
+        return
+
+    persist_path = cmd(persistpath_cmd).strip()
+    image_name = get_current_image()
+    image_path = os.path.join(persist_path, 'luks', image_name)
+
+    if not os.path.exists(image_path):
+        raise Exception("Encrypted config volume doesn't exist")
+
+    if is_opened():
+        print('Encrypted config volume is already mounted')
+        return
+
+    with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+        f.write(key)
+        key_file = f.name
+
+    cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+    for path in mount_paths:
+        cmd(f'mount /dev/mapper/vyos_config {path}')
+        cmd(f'chgrp -R vyattacfg {path}')
+
+    os.unlink(key_file)
+
+    return True
+
+def encrypt_config(key, recovery_key):
+    if is_opened():
+        raise Exception('An encrypted config volume is already mapped')
+
+    # Clear and write key to TPM
+    try:
+        clear_tpm_key()
+    except:
+        pass
+    write_tpm_key(key)
+
+    persist_path = cmd(persistpath_cmd).strip()
+    size = ask_input('Enter size of encrypted config partition (MB): ', numeric_only=True, default=512)
+
+    luks_folder = os.path.join(persist_path, 'luks')
+
+    if not os.path.isdir(luks_folder):
+        os.mkdir(luks_folder)
+
+    image_name = get_current_image()
+    image_path = os.path.join(luks_folder, image_name)
+
+    # Create file for encrypted config
+    cmd(f'fallocate -l {size}M {image_path}')
+
+    # Write TPM key for slot #1
+    with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+        f.write(key)
+        key_file = f.name
+
+    # Format and add main key to volume
+    cmd(f'cryptsetup -q luksFormat {image_path} {key_file}')
+
+    if recovery_key:
+        # Write recovery key for slot 2
+        with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+            f.write(recovery_key)
+            recovery_key_file = f.name
+
+        cmd(f'cryptsetup -q luksAddKey {image_path} {recovery_key_file} --key-file={key_file}')
+
+    # Open encrypted volume and format with ext4
+    cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+    cmd('mkfs.ext4 /dev/mapper/vyos_config')
+
+    with TemporaryDirectory() as d:
+        cmd(f'mount /dev/mapper/vyos_config {d}')
+
+        # Move /config to encrypted volume
+        shutil.copytree('/config', d, copy_function=shutil.move, dirs_exist_ok=True)
+
+        cmd(f'umount {d}')
+
+    os.unlink(key_file)
+
+    if recovery_key:
+        os.unlink(recovery_key_file)
+
+    for path in mount_paths:
+        cmd(f'mount /dev/mapper/vyos_config {path}')
+        cmd(f'chgrp vyattacfg {path}')
+
+    return True
+
+def decrypt_config(key):
+    if not key:
+        return
+
+    persist_path = cmd(persistpath_cmd).strip()
+    image_name = get_current_image()
+    image_path = os.path.join(persist_path, 'luks', image_name)
+
+    if not os.path.exists(image_path):
+        raise Exception("Encrypted config volume doesn't exist")
+
+    key_file = None
+
+    if not is_opened():
+        with NamedTemporaryFile(dir='/dev/shm', delete=False) as f:
+            f.write(key)
+            key_file = f.name
+
+        cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')
+
+    # unmount encrypted volume mount points
+    for path in mount_paths:
+        if os.path.ismount(path):
+            cmd(f'umount {path}')
+
+    # If /config is populated, move to /config.old
+    if len(os.listdir('/config')) > 0:
+        print('Moving existing /config folder to /config.old')
+        shutil.move('/config', '/config.old')
+
+    # Temporarily mount encrypted volume and migrate files to /config on rootfs
+    with TemporaryDirectory() as d:
+        cmd(f'mount /dev/mapper/vyos_config {d}')
+
+        # Move encrypted volume to /config
+        shutil.copytree(d, '/config', copy_function=shutil.move, dirs_exist_ok=True)
+        cmd(f'chgrp -R vyattacfg /config')
+
+        cmd(f'umount {d}')
+
+    # Close encrypted volume
+    cmd('cryptsetup -q close vyos_config')
+
+    # Remove encrypted volume image file and key
+    if key_file:
+        os.unlink(key_file)
+    os.unlink(image_path)
+
+    try:
+        clear_tpm_key()
+    except:
+        pass
+
+    return True
+
+if __name__ == '__main__':
+    if len(sys.argv) < 2:
+        print("Must specify action.")
+        sys.exit(1)
+
+    parser = ArgumentParser(description='Config encryption')
+    parser.add_argument('--disable', help='Disable encryption', action="store_true")
+    parser.add_argument('--enable', help='Enable encryption', action="store_true")
+    parser.add_argument('--load', help='Load encrypted config volume', action="store_true")
+    args = parser.parse_args()
+
+    tpm_exists = os.path.exists('/sys/class/tpm/tpm0')
+
+    key = None
+    recovery_key = None
+    need_recovery = False
+
+    question_key_str = 'recovery key' if tpm_exists else 'key'
+
+    if tpm_exists:
+        if args.enable:
+            key = Fernet.generate_key()
+        elif args.disable or args.load:
+            try:
+                key = read_tpm_key()
+                need_recovery = False
+            except:
+                print('Failed to read key from TPM, recovery key required')
+                need_recovery = True
+    else:
+        need_recovery = True
+
+    if args.enable and not tpm_exists:
+        print('WARNING: VyOS will boot into a default config when encrypted without a TPM')
+        print('You will need to manually login with default credentials and use "encryption load"')
+        print('to mount the encrypted volume and use "load /config/config.boot"')
+
+        if not ask_yes_no('Are you sure you want to proceed?'):
+            sys.exit(0)
+
+    if need_recovery or (args.enable and not ask_yes_no(f'Automatically generate a {question_key_str}?', default=True)):
+        while True:
+            recovery_key = ask_input(f'Enter {question_key_str}:', default=None).encode()
+
+            if len(recovery_key) >= 32:
+                break
+
+            print('Invalid key - must be at least 32 characters, try again.')
+    else:
+        recovery_key = Fernet.generate_key()
+
+    try:
+        if args.disable:
+            decrypt_config(key or recovery_key)
+
+            print('Encrypted config volume has been disabled')
+            print('Contents have been migrated to /config on rootfs')
+        elif args.load:
+            load_config(key or recovery_key)
+
+            print('Encrypted config volume has been mounted')
+            print('Use "load /config/config.boot" to load configuration')
+        elif args.enable and tpm_exists:
+            encrypt_config(key, recovery_key)
+
+            print('Encrypted config volume has been enabled with TPM')
+            print('Backup the recovery key in a safe place!')
+            print('Recovery key: ' + recovery_key.decode())
+        elif args.enable:
+            encrypt_config(recovery_key)
+
+            print('Encrypted config volume has been enabled without TPM')
+            print('Backup the key in a safe place!')
+            print('Key: ' + recovery_key.decode())
+    except Exception as e:
+        word = 'decrypt' if args.disable or args.load else 'encrypt'
+        print(f'Failed to {word} config: {e}')