Skip to content
159 changes: 98 additions & 61 deletions src/helpers/vyos-config-encrypt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

persistpath_cmd = '/opt/vyatta/sbin/vyos-persistpath'
mount_paths = ['/config', '/opt/vyatta/etc/config']
Expand All @@ -37,27 +34,14 @@
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
Expand All @@ -69,6 +53,7 @@ def load_config(key):
cmd(f'cryptsetup -q open {image_path} vyos_config --key-file={key_file}')

for path in mount_paths:
run(f'umount -l {path}')
cmd(f'mount /dev/mapper/vyos_config {path}')
cmd(f'chgrp -R vyattacfg {path}')

Expand All @@ -77,9 +62,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:
Expand All @@ -96,37 +78,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 /config to encrypted volume
shutil.copytree('/config', d, copy_function=shutil.move, dirs_exist_ok=True)
cmd(f'chgrp -R vyattacfg {d}')

cmd(f'umount {d}')

Expand All @@ -136,21 +130,30 @@ def encrypt_config(key, recovery_key=None, is_tpm=True):
os.unlink(recovery_key_file)

for path in mount_paths:
run(f'umount -l {path}')
cmd(f'mount /dev/mapper/vyos_config {path}')
cmd(f'chgrp vyattacfg {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

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")
original_config_path = os.path.join(persist_path, 'boot', image_name, 'rw', 'config')

key_file = None

Expand All @@ -164,12 +167,20 @@ def decrypt_config(key):
# unmount encrypted volume mount points
for path in mount_paths:
if os.path.ismount(path):
cmd(f'umount {path}')
cmd(f'umount -l {path}')

config_backup_path = config_backup_folder('/config.old')

# 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')
print(f'Moving existing /config folder to {config_backup_path}')
shutil.move('/config', config_backup_path)

# Mount original persistence config path
for path in mount_paths:
if not os.path.exists(path):
os.mkdir(path)
cmd(f'mount --bind {original_config_path} {path}')

# Temporarily mount encrypted volume and migrate files to /config on rootfs
with TemporaryDirectory() as d:
Expand All @@ -190,7 +201,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

Expand All @@ -211,6 +223,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
Expand All @@ -219,18 +243,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:
need_recovery = False
else:
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')
Expand Down
59 changes: 44 additions & 15 deletions src/op_mode/image_installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -1083,30 +1084,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/opt/vyatta/etc/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('/opt/vyatta/etc/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('/opt/vyatta/etc/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')
Expand All @@ -1132,8 +1163,6 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
if set_as_default:
grub.set_default(image_name, root_dir)

cmdline_options = get_cli_kernel_options(
f'{target_config_dir}/config.boot')
grub_util.update_kernel_cmdline_options(' '.join(cmdline_options),
root_dir=root_dir,
version=image_name)
Expand Down
Loading