From c7074d3c84750d782c28eee78aa2295af6fb4879 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:44:00 +0200 Subject: [PATCH 01/10] tpm: T7727: Prompt to overwrite an existing backup Move encrypted volume check before key input Unmount any conflicting config bind mounts --- src/helpers/vyos-config-encrypt.py | 38 +++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 86c98d07a2..2e68638e96 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -58,9 +58,6 @@ def load_config(key): 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 @@ -71,6 +68,7 @@ def load_config(key): cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') + run(f'umount {mount_path}') cmd(f'mount /dev/mapper/vyos_config {mount_path}') cmd(f'chgrp -R vyattacfg {mount_path}') @@ -79,9 +77,6 @@ def load_config(key): return True def encrypt_config(key, recovery_key=None, is_tpm=True): - if is_opened(): - raise Exception('An encrypted config volume is already mapped') - # Clear and write key to TPM if is_tpm: try: @@ -137,11 +132,22 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): if recovery_key: os.unlink(recovery_key_file) + run(f'umount {mount_path}') cmd(f'mount /dev/mapper/vyos_config {mount_path}') cmd(f'chgrp vyattacfg {mount_path}') return True +def config_backup_folder(base): + # Get next available backup folder + if not os.path.exists(base): + return base + + idx = 1 + while os.path.exists(f'{base}.{idx}'): + idx += 1 + return f'{base}.{idx}' + def decrypt_config(key): if not key: return @@ -150,9 +156,6 @@ def decrypt_config(key): 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(): @@ -168,8 +171,9 @@ def decrypt_config(key): # If /opt/vyatta/etc/config is populated, move to /opt/vyatta/etc/config.old if len(os.listdir(mount_path)) > 0: - print(f'Moving existing {mount_path} folder to {mount_path_old}') - shutil.move(mount_path, mount_path_old) + backup_path = config_backup_folder(mount_path_old) + print(f'Moving existing {mount_path} folder to {backup_path}') + shutil.move(mount_path, backup_path) # Temporarily mount encrypted volume and migrate files to # /opt/vyatta/etc/config on rootfs @@ -212,6 +216,18 @@ def decrypt_config(key): parser.add_argument('--load', help='Load encrypted config volume', action="store_true") args = parser.parse_args() + if args.disable or args.load: + 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): + print('Encrypted config volume does not exist, aborting.') + sys.exit(0) + elif args.enable and is_opened(): + print('An encrypted config volume is already mapped, aborting.') + sys.exit(0) + tpm_exists = os.path.exists('/sys/class/tpm/tpm0') key = None From 0476b6be11473a36e8e88d8ae3d983bb8168084b Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 18 Aug 2025 21:57:08 +0200 Subject: [PATCH 02/10] tpm: T7720: Handle encrypt failure and gracefully abort --- src/helpers/vyos-config-encrypt.py | 47 ++++++++++++++++++------------ 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 2e68638e96..71ec7bb73c 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -28,7 +28,7 @@ from vyos.tpm import read_tpm_key from vyos.tpm import write_tpm_key from vyos.utils.io import ask_input, ask_yes_no -from vyos.utils.process import cmd +from vyos.utils.process import cmd, run from vyos.defaults import directories persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath' @@ -96,28 +96,39 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): 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}') + try: + # 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 + # 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}') + # 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 + 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}') + 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') + # 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') + except Exception as e: + print('An error occurred while creating the encrypted config volume, aborting.') + + if os.path.exists('/dev/mapper/vyos_config'): + run('cryptsetup -q close vyos_config') + + if os.path.exists(image_path): + os.unlink(image_path) + + raise e with TemporaryDirectory() as d: cmd(f'mount /dev/mapper/vyos_config {d}') From e5f06c475d01e540b57cca996c1d92ed1d2864a4 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Mon, 18 Aug 2025 22:19:03 +0200 Subject: [PATCH 03/10] tpm: T7717: Preserve group on config and archives --- src/helpers/vyos-config-encrypt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 71ec7bb73c..cbe9b75432 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -135,7 +135,7 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): # Move mount_path to encrypted volume shutil.copytree(mount_path, d, copy_function=shutil.move, dirs_exist_ok=True) - + cmd(f'chgrp -R vyattacfg {d}') cmd(f'umount {d}') os.unlink(key_file) From b7c8955e3a96f9b016bc3c595d33e6ace2872af4 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 19 Aug 2025 11:07:57 +0200 Subject: [PATCH 04/10] tpm: T7713: Restore original config mounts when decrypting --- src/helpers/vyos-config-encrypt.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index cbe9b75432..d76baa87cb 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -68,7 +68,7 @@ def load_config(key): cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') - run(f'umount {mount_path}') + run(f'umount -l {mount_path}') cmd(f'mount /dev/mapper/vyos_config {mount_path}') cmd(f'chgrp -R vyattacfg {mount_path}') @@ -143,7 +143,7 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): if recovery_key: os.unlink(recovery_key_file) - run(f'umount {mount_path}') + run(f'umount -l {mount_path}') cmd(f'mount /dev/mapper/vyos_config {mount_path}') cmd(f'chgrp vyattacfg {mount_path}') @@ -166,6 +166,7 @@ def decrypt_config(key): persist_path = cmd(persistpath_cmd).strip() image_name = get_current_image() image_path = os.path.join(persist_path, 'luks', image_name) + original_config_path = os.path.join(persist_path, 'boot', image_name, 'rw', 'opt', 'vyatta', 'etc', 'config') key_file = None @@ -176,9 +177,8 @@ def decrypt_config(key): cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') - # unmount encrypted volume mount point - if os.path.ismount(mount_path): - cmd(f'umount {mount_path}') + # unmount encrypted volume mount points + run(f'umount -Alq /dev/mapper/vyos_config') # If /opt/vyatta/etc/config is populated, move to /opt/vyatta/etc/config.old if len(os.listdir(mount_path)) > 0: @@ -186,8 +186,12 @@ def decrypt_config(key): print(f'Moving existing {mount_path} folder to {backup_path}') shutil.move(mount_path, backup_path) - # Temporarily mount encrypted volume and migrate files to - # /opt/vyatta/etc/config on rootfs + # Mount original persistence config path + if not os.path.exists(mount_path): + os.mkdir(mount_path) + cmd(f'mount --bind {original_config_path} {mount_path}') + + # Temporarily mount encrypted volume and migrate files to /config on rootfs with TemporaryDirectory() as d: cmd(f'mount /dev/mapper/vyos_config {d}') From 645f92140138623c81fe7922978e8298301e7be1 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 19 Aug 2025 12:53:47 +0200 Subject: [PATCH 05/10] tpm: T7735: Only require key/recovery if unmapped --- src/helpers/vyos-config-encrypt.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index d76baa87cb..d9cf98cb93 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -251,18 +251,19 @@ def decrypt_config(key): 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 not is_opened(): + 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') From e98ae3e5d50ecadd6fd988d0745e49868323bde1 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 19 Aug 2025 13:06:00 +0200 Subject: [PATCH 06/10] tpm: T4919: Use vyos module function for running image, single-line imports --- src/helpers/vyos-config-encrypt.py | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index d9cf98cb93..64e39f7001 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -20,13 +20,10 @@ from argparse import ArgumentParser from cryptography.fernet import Fernet -from tempfile import NamedTemporaryFile -from tempfile import TemporaryDirectory +from tempfile import NamedTemporaryFile, TemporaryDirectory -from vyos.system.image import is_live_boot -from vyos.tpm import clear_tpm_key -from vyos.tpm import read_tpm_key -from vyos.tpm import write_tpm_key +from vyos.system.image import is_live_boot, get_running_image +from vyos.tpm import clear_tpm_key, read_tpm_key, write_tpm_key from vyos.utils.io import ask_input, ask_yes_no from vyos.utils.process import cmd, run from vyos.defaults import directories @@ -40,22 +37,12 @@ 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_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) if is_opened(): @@ -93,7 +80,7 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): if not os.path.isdir(luks_folder): os.mkdir(luks_folder) - image_name = get_current_image() + image_name = get_running_image() image_path = os.path.join(luks_folder, image_name) try: @@ -164,7 +151,7 @@ def decrypt_config(key): return persist_path = cmd(persistpath_cmd).strip() - image_name = get_current_image() + image_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) original_config_path = os.path.join(persist_path, 'boot', image_name, 'rw', 'opt', 'vyatta', 'etc', 'config') @@ -233,7 +220,7 @@ def decrypt_config(key): if args.disable or args.load: persist_path = cmd(persistpath_cmd).strip() - image_name = get_current_image() + image_name = get_running_image() image_path = os.path.join(persist_path, 'luks', image_name) if not os.path.exists(image_path): From 0f1430ff44c91b4e1dd152f0cf960a62dcb4d032 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 19 Aug 2025 15:47:03 +0200 Subject: [PATCH 07/10] tpm: T7726: Copy encrypted volume when adding system images --- src/op_mode/image_installer.py | 57 ++++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 13 deletions(-) diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 6dc2dc4905..a5afd26f4f 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -26,6 +26,7 @@ from os import readlink from os import getpid from os import getppid +from os import sync from json import loads from json import dumps from typing import Union @@ -1086,30 +1087,60 @@ def add_image(image_path: str, vrf: str = None, username: str = '', # find target directory root_dir: str = disk.find_persistence() + cmdline_options = [] + # a config dir. It is the deepest one, so the comand will # create all the rest in a single step target_config_dir: str = f'{root_dir}/boot/{image_name}/rw{DIR_CONFIG}/' # copy config if no_prompt or migrate_config(): - print('Copying configuration directory') - # copytree preserves perms but not ownership: - Path(target_config_dir).mkdir(parents=True) - chown(target_config_dir, group='vyattacfg') - chmod_2775(target_config_dir) - copytree(f'{DIR_CONFIG}/', target_config_dir, symlinks=True, - copy_function=copy_preserve_owner, dirs_exist_ok=True) - - # Record information from which image we upgraded to the new one. - # This can be used for a future automatic rollback into the old image. - tmp = {'previous_image' : image.get_running_image()} - write_file(f'{target_config_dir}/first_boot', dumps(tmp)) - + if Path('/dev/mapper/vyos_config').exists(): + print('Copying encrypted configuration volume') + + # Record information from which image we upgraded to the new one. + # This can be used for a future automatic rollback into the old image. + # + # For encrypted config, we need to copy, sync filesystems and remove from current image + tmp = {'previous_image' : image.get_running_image()} + write_file('/opt/vyatta/etc/config/first_boot', dumps(tmp)) + sync() + + # Copy encrypteed volumes + current_name = image.get_running_image() + current_config_path = f'{root_dir}/luks/{current_name}' + target_config_path = f'{root_dir}/luks/{image_name}' + copy(current_config_path, target_config_path) + + # Now remove from current image + Path('/opt/vyatta/etc/config/first_boot').unlink() + + cmdline_options = get_cli_kernel_options( + f'/opt/vyatta/etc/config/config.boot') + else: + print('Copying configuration directory') + # copytree preserves perms but not ownership: + Path(target_config_dir).mkdir(parents=True) + chown(target_config_dir, group='vyattacfg') + chmod_2775(target_config_dir) + copytree(f'{DIR_CONFIG}/', target_config_dir, symlinks=True, + copy_function=copy_preserve_owner, dirs_exist_ok=True) + + # Record information from which image we upgraded to the new one. + # This can be used for a future automatic rollback into the old image. + tmp = {'previous_image' : image.get_running_image()} + write_file(f'{target_config_dir}/first_boot', dumps(tmp)) + + cmdline_options = get_cli_kernel_options( + f'{target_config_dir}/config.boot') else: Path(target_config_dir).mkdir(parents=True) chown(target_config_dir, group='vyattacfg') chmod_2775(target_config_dir) Path(f'{target_config_dir}/.vyatta_config').touch() + cmdline_options = get_cli_kernel_options( + f'{target_config_dir}/config.boot') + target_ssh_dir: str = f'{root_dir}/boot/{image_name}/rw/etc/ssh/' if no_prompt or copy_ssh_host_keys(): print('Copying SSH host keys') From 5865c42e37262da5a6ba67ae0785cb1cc4a2b15b Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 19 Aug 2025 16:25:34 +0200 Subject: [PATCH 08/10] tpm: T7726: Prompt user before clearing TPM key --- src/helpers/vyos-config-encrypt.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 64e39f7001..3fa651abff 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -197,7 +197,8 @@ def decrypt_config(key): os.unlink(image_path) try: - clear_tpm_key() + if ask_yes_no('Do you want to clear the TPM? This will cause issues if other system images use the key'): + clear_tpm_key() except: pass From a080cfae6523768f1d5dea7ed0ab3178622495ef Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 9 Sep 2025 15:22:18 +0200 Subject: [PATCH 09/10] tpm: T7726: Prompt before overwriting existing TPM key --- src/helpers/vyos-config-encrypt.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 3fa651abff..e2888089f2 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -241,13 +241,25 @@ def decrypt_config(key): if not is_opened(): if tpm_exists: + existing_key = None + + try: + existing_key = read_tpm_key() + except: pass + if args.enable: - key = Fernet.generate_key() + if existing_key: + print('WARNING: An encryption key already exists in the TPM.') + print('If you choose not to use the existing key, any system image') + print('using the old key will need the recovery key.') + if existing_key and ask_yes_no('Do you want to use the existing TPM key?'): + key = existing_key + else: + key = Fernet.generate_key() elif args.disable or args.load: - try: - key = read_tpm_key() + if existing_key: need_recovery = False - except: + else: print('Failed to read key from TPM, recovery key required') need_recovery = True else: From 36d9d7068cc0c437f17f85c8fbd05c79f7d99627 Mon Sep 17 00:00:00 2001 From: sarthurdev <965089+sarthurdev@users.noreply.github.com> Date: Tue, 23 Sep 2025 10:14:10 +0200 Subject: [PATCH 10/10] tpm: T7726: Test TPM key or prompt recovery key --- src/helpers/vyos-config-encrypt.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index e2888089f2..c983e60652 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -34,6 +34,7 @@ mount_path_old = f'{mount_path}.old' dm_device = '/dev/mapper/vyos_config' + def is_opened(): return os.path.exists(dm_device) @@ -146,6 +147,29 @@ def config_backup_folder(base): idx += 1 return f'{base}.{idx}' +def test_decrypt(key): + if not key: + return + + persist_path = cmd(persistpath_cmd).strip() + image_name = get_running_image() + image_path = os.path.join(persist_path, 'luks', image_name) + + key_file = None + + if not is_opened(): + with NamedTemporaryFile(dir='/dev/shm', delete=False) as f: + f.write(key) + key_file = f.name + + try: + cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') + os.unlink(key_file) + return True + except: + return False + return False + def decrypt_config(key): if not key: return @@ -257,10 +281,10 @@ def decrypt_config(key): else: key = Fernet.generate_key() elif args.disable or args.load: - if existing_key: + if existing_key and test_decrypt(existing_key): need_recovery = False else: - print('Failed to read key from TPM, recovery key required') + print('TPM key invalid or not found, recovery key required') need_recovery = True else: need_recovery = True