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