diff --git a/src/helpers/vyos-config-encrypt.py b/src/helpers/vyos-config-encrypt.py index 86c98d07a2..c983e60652 100755 --- a/src/helpers/vyos-config-encrypt.py +++ b/src/helpers/vyos-config-encrypt.py @@ -20,15 +20,12 @@ 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 +from vyos.utils.process import cmd, run from vyos.defaults import directories persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath' @@ -37,30 +34,18 @@ mount_path_old = f'{mount_path}.old' 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_name = get_running_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 +56,7 @@ def load_config(key): cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}') + run(f'umount -l {mount_path}') cmd(f'mount /dev/mapper/vyos_config {mount_path}') cmd(f'chgrp -R vyattacfg {mount_path}') @@ -79,9 +65,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: @@ -98,38 +81,49 @@ 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) - # 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}') # 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) @@ -137,21 +131,53 @@ def encrypt_config(key, recovery_key=None, is_tpm=True): if recovery_key: os.unlink(recovery_key_file) + run(f'umount -l {mount_path}') cmd(f'mount /dev/mapper/vyos_config {mount_path}') cmd(f'chgrp vyattacfg {mount_path}') return True -def decrypt_config(key): +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 test_decrypt(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 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 + + 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 + + persist_path = cmd(persistpath_cmd).strip() + 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') key_file = None @@ -162,17 +188,21 @@ 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: - 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) + + # 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 - # /opt/vyatta/etc/config on rootfs + # Temporarily mount encrypted volume and migrate files to /config on rootfs with TemporaryDirectory() as d: cmd(f'mount /dev/mapper/vyos_config {d}') @@ -191,7 +221,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 @@ -212,6 +243,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_running_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 @@ -220,18 +263,31 @@ 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: + if not is_opened(): + if tpm_exists: + existing_key = None + 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 + existing_key = read_tpm_key() + except: pass + + if args.enable: + 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: + if existing_key and test_decrypt(existing_key): + need_recovery = False + else: + print('TPM key invalid or not found, 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') 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')