Skip to content

Commit

Permalink
Merge pull request #100 from dwlehman/luks
Browse files Browse the repository at this point in the history
Encrypted Volumes
  • Loading branch information
dwlehman authored Jun 11, 2020
2 parents 8c4f818 + 178c4d6 commit 98fd8bd
Show file tree
Hide file tree
Showing 15 changed files with 533 additions and 14 deletions.
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ The `mount_point` specifies the directory on which the file system will be mount
##### `mount_options`
The `mount_options` specifies custom mount options as a string, e.g.: 'ro'.

##### `encryption`
This specifies whether or not the volume will be encrypted using LUKS.
__WARNING__: Toggling encryption for a volume is a destructive operation, meaning
all data on that volume will be removed as part of the process of
adding/removing the encryption layer.

##### `encryption_passphrase`
This string specifies a passphrase used to unlock/open the LUKS volume.

##### `encryption_key_file`
This string specifies the full path to the key file used to unlock the LUKS volume.

##### `encryption_cipher`
This string specifies a non-default cipher to be used by LUKS.

##### `encryption_key_size`
This integer specifies the LUKS key size (in bits).

##### `encryption_luks_version`
This integer specifies the LUKS version to use.

#### `storage_safe_mode`
When true (the default), an error will occur instead of automatically removing existing devices and/or formatting.

Expand Down
7 changes: 7 additions & 0 deletions defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,10 @@ storage_volume_defaults:
mount_check: 0
mount_passno: 0
mount_device_identifier: "uuid" # uuid|label|path

encryption: false
encryption_passphrase: null
encryption_key_file: null
encryption_cipher: null
encryption_key_size: null
encryption_luks_version: null
107 changes: 100 additions & 7 deletions library/blivet.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@
mounts:
description: list of dicts describing mounts to set up
type: list of dict
crypts:
description: list of dicts describing crypttab entries to set up
type: list of dict
pools:
description: list of dicts describing the pools w/ device path for each volume
type: list of dict
Expand Down Expand Up @@ -190,6 +193,20 @@ def _look_up_device(self):
if device is None:
return

if device.format.type == 'luks':
# XXX If we have no key we will always re-encrypt.
device.format._key_file = self._volume.get('encryption_key_file')
device.format.passphrase = self._volume.get('encryption_passphrase')

# set up the original format as well since it'll get used for processing
device.original_format._key_file = self._volume.get('encryption_key_file')
device.original_format.passphrase = self._volume.get('encryption_passphrase')
if device.isleaf:
self._blivet.populate()

if not device.isleaf:
device = device.children[0]

self._device = device

# check that the type is correct, raising an exception if there is a name conflict
Expand Down Expand Up @@ -219,10 +236,53 @@ def _destroy(self):

# save device identifiers for use by the role
self._volume['_device'] = self._device.path
self._volume['_raw_device'] = self._device.raw_device.path
self._volume['_mount_id'] = self._device.fstab_spec

# schedule removal of this device and any descendant devices
self._blivet.devicetree.recursive_remove(self._device)
self._blivet.devicetree.recursive_remove(self._device.raw_device)

def _manage_encryption(self):
# Make sure to handle adjusting both existing stacks and future stacks.
if self._device == self._device.raw_device and self._volume['encryption']:
# add luks
luks_name = "luks-%s" % self._device._name
if not self._device.format.exists:
fmt = self._device.format
else:
fmt = get_format(None)

self._blivet.format_device(self._device,
get_format("luks",
name=luks_name,
cipher=self._volume.get('encryption_cipher'),
key_size=self._volume.get('encryption_key_size'),
luks_version=self._volume.get('encryption_luks_version'),
passphrase=self._volume.get('encryption_passphrase') or None,
key_file=self._volume.get('encryption_key_file') or None))

if not self._device.format.has_key:
raise BlivetAnsibleError("encrypted volume '%s' missing key/passphrase" % self._volume['name'])

luks_device = devices.LUKSDevice(luks_name,
fmt=fmt,
parents=[self._device])
self._blivet.create_device(luks_device)
self._device = luks_device
elif self._device != self._device.raw_device and not self._volume['encryption']:
# remove luks
if not self._device.format.exists:
fmt = self._device.format
else:
fmt = get_format(None)

self._device = self._device.raw_device
self._blivet.destroy_device(self._device.children[0])
if fmt.type is not None:
self._blivet.format_device(self._device, fmt)

# XXX: blivet has to store cipher, key_size, luks_version for existing before we
# can support re-encrypting based on changes to those parameters

def _resize(self):
""" Schedule actions as needed to ensure the device has the desired size. """
Expand Down Expand Up @@ -254,7 +314,7 @@ def _reformat(self):
if safe_mode and (self._device.format.type is not None or self._device.format.name != get_format(None).name):
raise BlivetAnsibleError("cannot remove existing formatting on volume '%s' in safe mode" % self._volume['name'])

if self._device.format.status:
if self._device.format.status and (self._device.format.mountable or self._device.format.type == "swap"):
self._device.format.teardown()
self._blivet.format_device(self._device, fmt)

Expand All @@ -275,16 +335,19 @@ def manage(self):
if self._device is None:
raise BlivetAnsibleError("failed to look up or create device '%s'" % self._volume['name'])

self._manage_encryption()

# schedule reformat if appropriate
if self._device.exists:
if self._device.raw_device.exists:
self._reformat()

# schedule resize if appropriate
if self._device.exists and self._volume['size']:
if self._device.raw_device.exists and self._volume['size']:
self._resize()

# save device identifiers for use by the role
self._volume['_device'] = self._device.path
self._volume['_raw_device'] = self._device.raw_device.path
self._volume['_mount_id'] = self._device.fstab_spec


Expand All @@ -295,7 +358,10 @@ def _get_device_id(self):
return self._volume['disks'][0]

def _type_check(self):
return self._device.is_disk
return self._device.raw_device.is_disk

def _create(self):
self._reformat()

def _look_up_device(self):
super(BlivetDiskVolume, self)._look_up_device()
Expand All @@ -313,7 +379,7 @@ class BlivetPartitionVolume(BlivetVolume):
blivet_device_class = devices.PartitionDevice

def _type_check(self):
return self._device.type == 'partition'
return self._device.raw_device.type == 'partition'

def _get_device_id(self):
return self._blivet_pool._disks[0].name + '1'
Expand Down Expand Up @@ -635,6 +701,7 @@ def manage_volume(b, volume):
bvolume = _get_blivet_volume(b, volume)
bvolume.manage()
volume['_device'] = bvolume._volume.get('_device', '')
volume['_raw_device'] = bvolume._volume.get('_raw_device', '')
volume['_mount_id'] = bvolume._volume.get('_mount_id', '')


Expand All @@ -644,6 +711,7 @@ def manage_pool(b, pool):
bpool.manage()
for (volume, bvolume) in zip(pool['volumes'], bpool._blivet_volumes):
volume['_device'] = bvolume._volume.get('_device', '')
volume['_raw_device'] = bvolume._volume.get('_raw_device', '')
volume['_mount_id'] = bvolume._volume.get('_mount_id', '')


Expand Down Expand Up @@ -749,6 +817,20 @@ def handle_new_mount(volume, fstab):
return mount_info


def get_crypt_info(actions):
info = list()
for action in actions:
if not (action.is_format and action.format.type == 'luks'):
continue

info.append(dict(backing_device=action.device.path,
name=action.format.map_name,
password=action.format.key_file or '-',
state='present' if action.is_create else 'absent'))

return sorted(info, key=lambda e: e['state'])


def get_required_packages(b, pools, volumes):
packages = list()
for pool in pools:
Expand Down Expand Up @@ -780,6 +862,14 @@ def update_fstab_identifiers(b, pools, volumes):
for volume in all_volumes:
if volume['state'] == 'present':
device = b.devicetree.resolve_device(volume['_mount_id'])
if device is None and volume['encryption']:
device = b.devicetree.resolve_device(volume['_raw_device'])
if device is not None and not device.isleaf:
device = device.children[0]
volume['_device'] = device.path

if device is None:
raise BlivetAnsibleError("failed to look up device for volume %s (%s/%s)" % (volume['name'], volume['_device'], volume['_mount_id']))
volume['_mount_id'] = device.fstab_spec
if device.format.type == 'swap':
device.format.setup()
Expand Down Expand Up @@ -817,6 +907,7 @@ def run_module():
actions=list(),
leaves=list(),
mounts=list(),
crypts=list(),
pools=list(),
volumes=list(),
packages=list(),
Expand Down Expand Up @@ -893,7 +984,8 @@ def action_dict(action):
result['packages'] = b.packages[:]

for action in scheduled:
if action.is_destroy and action.is_format and action.format.exists:
if action.is_destroy and action.is_format and action.format.exists and \
(action.format.mountable or action.format.type == "swap"):
action.format.teardown()

if scheduled:
Expand All @@ -911,6 +1003,7 @@ def action_dict(action):
activate_swaps(b, module.params['pools'], module.params['volumes'])

result['mounts'] = get_mount_info(module.params['pools'], module.params['volumes'], actions, fstab)
result['crypts'] = get_crypt_info(actions)
result['leaves'] = [d.path for d in b.devicetree.leaves]
result['pools'] = module.params['pools']
result['volumes'] = module.params['volumes']
Expand Down
1 change: 1 addition & 0 deletions molecule_extra_requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# SPDX-License-Identifier: MIT

# Write extra requirements for running molecule here:
jmespath
37 changes: 35 additions & 2 deletions tasks/main-blivet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,14 +129,18 @@
# changed options? (just add w/ new settings?)
# add new mounts
#
- name: manage mounts to match the specified state
# XXX Apparently we have to do the removals, then tell systemd to
# update its view, then set up the new mounts. Otherwise,
# systemd will forcibly prevent mounting a new volume to an
# existing mount point.
- name: remove obsolete mounts
mount:
src: "{{ mount_info['src']|default(omit) }}"
path: "{{ mount_info['path'] }}"
fstype: "{{ mount_info['fstype']|default(omit) }}"
opts: "{{ mount_info['opts']|default(omit) }}"
state: "{{ mount_info['state'] }}"
loop: "{{ blivet_output.mounts }}"
loop: "{{ blivet_output.mounts|json_query('[?state==`absent`]') }}"
loop_control:
loop_var: mount_info

Expand All @@ -145,6 +149,35 @@
daemon_reload: yes
when: blivet_output['mounts']

- name: set up new/current mounts
mount:
src: "{{ mount_info['src']|default(omit) }}"
path: "{{ mount_info['path'] }}"
fstype: "{{ mount_info['fstype']|default(omit) }}"
opts: "{{ mount_info['opts']|default(omit) }}"
state: "{{ mount_info['state'] }}"
loop: "{{ blivet_output.mounts|json_query('[?state!=`absent`]') }}"
loop_control:
loop_var: mount_info

- name: tell systemd to refresh its view of /etc/fstab
systemd:
daemon_reload: yes
when: blivet_output['mounts']

#
# Manage /etc/crypttab
#
- name: Manage /etc/crypttab to account for changes we just made
crypttab:
name: "{{ entry.name }}"
backing_device: "{{ entry.backing_device }}"
password: "{{ entry.password }}"
state: "{{ entry.state }}"
loop: "{{ blivet_output.crypts }}"
loop_control:
loop_var: entry

#
# Update facts since we may have changed system state.
#
Expand Down
6 changes: 3 additions & 3 deletions tests/test-verify-volume-device.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# name/path
- name: See whether the device node is present
stat:
path: "{{ storage_test_volume._device }}"
path: "{{ storage_test_volume._raw_device }}"
follow: yes
register: storage_test_dev

Expand All @@ -16,13 +16,13 @@

- name: Make sure we got info about this volume
assert:
that: "{{ storage_test_volume._device in storage_test_blkinfo.info }}"
that: "{{ storage_test_volume._raw_device in storage_test_blkinfo.info }}"
msg: "Failed to gather info about volume '{{ storage_test_volume.name }}'"
when: _storage_test_volume_present

- name: Verify the volume's device type
assert:
that: "{{ storage_test_blkinfo.info[storage_test_volume._device].type == storage_test_volume.type }}"
that: "{{ storage_test_blkinfo.info[storage_test_volume._raw_device].type == storage_test_volume.type }}"
when: _storage_test_volume_present

# disks
Expand Down
Loading

0 comments on commit 98fd8bd

Please sign in to comment.