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