1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
|
import base64
import os
import logging
from ceph_volume import process, conf
from ceph_volume.util import constants, system
from ceph_volume.util.device import Device
from .prepare import write_keyring
from .disk import lsblk, device_family, get_part_entry_type
logger = logging.getLogger(__name__)
def create_dmcrypt_key():
"""
Create the secret dm-crypt key used to decrypt a device.
"""
# get the customizable dmcrypt key size (in bits) from ceph.conf fallback
# to the default of 1024
dmcrypt_key_size = conf.ceph.get_safe(
'osd',
'osd_dmcrypt_key_size',
default=1024,
)
# The size of the key is defined in bits, so we must transform that
# value to bytes (dividing by 8) because we read in bytes, not bits
random_string = os.urandom(int(dmcrypt_key_size / 8))
key = base64.b64encode(random_string).decode('utf-8')
return key
def luks_format(key, device):
"""
Decrypt (open) an encrypted device, previously prepared with cryptsetup
:param key: dmcrypt secret key, will be used for decrypting
:param device: Absolute path to device
"""
command = [
'cryptsetup',
'--batch-mode', # do not prompt
'--key-file', # misnomer, should be key
'-', # because we indicate stdin for the key here
'luksFormat',
device,
]
process.call(command, stdin=key, terminal_verbose=True, show_command=True)
def plain_open(key, device, mapping):
"""
Decrypt (open) an encrypted device, previously prepared with cryptsetup in plain mode
.. note: ceph-disk will require an additional b64decode call for this to work
:param key: dmcrypt secret key
:param device: absolute path to device
:param mapping: mapping name used to correlate device. Usually a UUID
"""
command = [
'cryptsetup',
'--key-file',
'-',
'--allow-discards', # allow discards (aka TRIM) requests for device
'open',
device,
mapping,
'--type', 'plain',
'--key-size', '256',
]
process.call(command, stdin=key, terminal_verbose=True, show_command=True)
def luks_open(key, device, mapping):
"""
Decrypt (open) an encrypted device, previously prepared with cryptsetup
.. note: ceph-disk will require an additional b64decode call for this to work
:param key: dmcrypt secret key
:param device: absolute path to device
:param mapping: mapping name used to correlate device. Usually a UUID
"""
command = [
'cryptsetup',
'--key-file',
'-',
'--allow-discards', # allow discards (aka TRIM) requests for device
'luksOpen',
device,
mapping,
]
process.call(command, stdin=key, terminal_verbose=True, show_command=True)
def dmcrypt_close(mapping):
"""
Encrypt (close) a device, previously decrypted with cryptsetup
:param mapping:
"""
if not os.path.exists(mapping):
logger.debug('device mapper path does not exist %s' % mapping)
logger.debug('will skip cryptsetup removal')
return
# don't be strict about the remove call, but still warn on the terminal if it fails
process.run(['cryptsetup', 'remove', mapping], stop_on_error=False)
def get_dmcrypt_key(osd_id, osd_fsid, lockbox_keyring=None):
"""
Retrieve the dmcrypt (secret) key stored initially on the monitor. The key
is sent initially with JSON, and the Monitor then mangles the name to
``dm-crypt/osd/<fsid>/luks``
The ``lockbox.keyring`` file is required for this operation, and it is
assumed it will exist on the path for the same OSD that is being activated.
To support scanning, it is optionally configurable to a custom location
(e.g. inside a lockbox partition mounted in a temporary location)
"""
if lockbox_keyring is None:
lockbox_keyring = '/var/lib/ceph/osd/%s-%s/lockbox.keyring' % (conf.cluster, osd_id)
name = 'client.osd-lockbox.%s' % osd_fsid
config_key = 'dm-crypt/osd/%s/luks' % osd_fsid
stdout, stderr, returncode = process.call(
[
'ceph',
'--cluster', conf.cluster,
'--name', name,
'--keyring', lockbox_keyring,
'config-key',
'get',
config_key
],
show_command=True
)
if returncode != 0:
raise RuntimeError('Unable to retrieve dmcrypt secret')
return ' '.join(stdout).strip()
def write_lockbox_keyring(osd_id, osd_fsid, secret):
"""
Helper to write the lockbox keyring. This is needed because the bluestore OSD will
not persist the keyring, and it can't be stored in the data device for filestore because
at the time this is needed, the device is encrypted.
For bluestore: A tmpfs filesystem is mounted, so the path can get written
to, but the files are ephemeral, which requires this file to be created
every time it is activated.
For filestore: The path for the OSD would exist at this point even if no
OSD data device is mounted, so the keyring is written to fetch the key, and
then the data device is mounted on that directory, making the keyring
"disappear".
"""
if os.path.exists('/var/lib/ceph/osd/%s-%s/lockbox.keyring' % (conf.cluster, osd_id)):
return
name = 'client.osd-lockbox.%s' % osd_fsid
write_keyring(
osd_id,
secret,
keyring_name='lockbox.keyring',
name=name
)
def status(device):
"""
Capture the metadata information of a possibly encrypted device, returning
a dictionary with all the values found (if any).
An encrypted device will contain information about a device. Example
successful output looks like::
$ cryptsetup status /dev/mapper/ed6b5a26-eafe-4cd4-87e3-422ff61e26c4
/dev/mapper/ed6b5a26-eafe-4cd4-87e3-422ff61e26c4 is active and is in use.
type: LUKS1
cipher: aes-xts-plain64
keysize: 256 bits
device: /dev/sdc2
offset: 4096 sectors
size: 20740063 sectors
mode: read/write
As long as the mapper device is in 'open' state, the ``status`` call will work.
:param device: Absolute path or UUID of the device mapper
"""
command = [
'cryptsetup',
'status',
device,
]
out, err, code = process.call(command, show_command=True, verbose_on_failure=False)
metadata = {}
if code != 0:
logger.warning('failed to detect device mapper information')
return metadata
for line in out:
# get rid of lines that might not be useful to construct the report:
if not line.startswith(' '):
continue
try:
column, value = line.split(': ')
except ValueError:
continue
metadata[column.strip()] = value.strip().strip('"')
return metadata
def legacy_encrypted(device):
"""
Detect if a device was encrypted with ceph-disk or not. In the case of
encrypted devices, include the type of encryption (LUKS, or PLAIN), and
infer what the lockbox partition is.
This function assumes that ``device`` will be a partition.
"""
if os.path.isdir(device):
mounts = system.get_mounts(paths=True)
# yes, rebind the device variable here because a directory isn't going
# to help with parsing
device = mounts.get(device, [None])[0]
if not device:
raise RuntimeError('unable to determine the device mounted at %s' % device)
metadata = {'encrypted': False, 'type': None, 'lockbox': '', 'device': device}
# check if the device is online/decrypted first
active_mapper = status(device)
if active_mapper:
# normalize a bit to ensure same values regardless of source
metadata['type'] = active_mapper['type'].lower().strip('12') # turn LUKS1 or LUKS2 into luks
metadata['encrypted'] = True if metadata['type'] in ['plain', 'luks'] else False
# The true device is now available to this function, so it gets
# re-assigned here for the lockbox checks to succeed (it is not
# possible to guess partitions from a device mapper device otherwise
device = active_mapper.get('device', device)
metadata['device'] = device
else:
uuid = get_part_entry_type(device)
guid_match = constants.ceph_disk_guids.get(uuid, {})
encrypted_guid = guid_match.get('encrypted', False)
if encrypted_guid:
metadata['encrypted'] = True
metadata['type'] = guid_match['encryption_type']
# Lets find the lockbox location now, to do this, we need to find out the
# parent device name for the device so that we can query all of its
# associated devices and *then* look for one that has the 'lockbox' label
# on it. Thanks for being awesome ceph-disk
disk_meta = lsblk(device, abspath=True)
if not disk_meta:
return metadata
parent_device = disk_meta['PKNAME']
# With the parent device set, we can now look for the lockbox listing associated devices
devices = [Device(i['NAME']) for i in device_family(parent_device)]
for d in devices:
if d.ceph_disk.type == 'lockbox':
metadata['lockbox'] = d.abspath
break
return metadata
|