]> git.proxmox.com Git - ceph.git/blob - ceph/qa/tasks/openssl_keys.py
f9a7f7edecca96a86fb24b864ffd653ca82d0bcd
[ceph.git] / ceph / qa / tasks / openssl_keys.py
1 """
2 Generates and installs a signed SSL certificate.
3 """
4 import argparse
5 import logging
6 import os
7
8 from teuthology import misc
9 from teuthology.exceptions import ConfigError
10 from teuthology.orchestra import run
11 from teuthology.task import Task
12
13 log = logging.getLogger(__name__)
14
15 class OpenSSLKeys(Task):
16 name = 'openssl_keys'
17 """
18 Generates and installs a signed SSL certificate.
19
20 To create a self-signed certificate:
21
22 - openssl_keys:
23 # certificate name
24 root: # results in root.key and root.crt
25
26 # [required] make the private key and certificate available in this client's test directory
27 client: client.0
28
29 # common name, defaults to `hostname`. chained certificates must not share a common name
30 cn: teuthology
31
32 # private key type for -newkey, defaults to rsa:2048
33 key-type: rsa:4096
34
35 # install the certificate as trusted on these clients:
36 install: [client.0, client.1]
37
38
39 To create a certificate signed by a ca certificate:
40
41 - openssl_keys:
42 root: (self-signed certificate as above)
43 ...
44
45 cert-for-client1:
46 client: client.1
47
48 # use another ssl certificate (by 'name') as the certificate authority
49 ca: root # --CAkey=root.key -CA=root.crt
50
51 # embed the private key in the certificate file
52 embed-key: true
53 """
54
55 def __init__(self, ctx, config):
56 super(OpenSSLKeys, self).__init__(ctx, config)
57 self.certs = []
58 self.installed = []
59
60 def setup(self):
61 # global dictionary allows other tasks to look up certificate paths
62 if not hasattr(self.ctx, 'ssl_certificates'):
63 self.ctx.ssl_certificates = {}
64
65 # use testdir/ca as a working directory
66 self.cadir = '/'.join((misc.get_testdir(self.ctx), 'ca'))
67 # make sure self-signed certs get added first, they don't have 'ca' field
68 configs = sorted(self.config.items(), key=lambda x: 'ca' in x[1])
69 for name, config in configs:
70 # names must be unique to avoid clobbering each others files
71 if name in self.ctx.ssl_certificates:
72 raise ConfigError('ssl: duplicate certificate name {}'.format(name))
73
74 # create the key and certificate
75 cert = self.create_cert(name, config)
76
77 self.ctx.ssl_certificates[name] = cert
78 self.certs.append(cert)
79
80 # install as trusted on the requested clients
81 for client in config.get('install', []):
82 installed = self.install_cert(cert, client)
83 self.installed.append(installed)
84
85 def teardown(self):
86 """
87 Clean up any created/installed certificate files.
88 """
89 for cert in self.certs:
90 self.remove_cert(cert)
91
92 for installed in self.installed:
93 self.uninstall_cert(installed)
94
95 def create_cert(self, name, config):
96 """
97 Create a certificate with the given configuration.
98 """
99 cert = argparse.Namespace()
100 cert.name = name
101 cert.key_type = config.get('key-type', 'rsa:2048')
102
103 cert.client = config.get('client', None)
104 if not cert.client:
105 raise ConfigError('ssl: missing required field "client"')
106
107 (cert.remote,) = self.ctx.cluster.only(cert.client).remotes.keys()
108
109 cert.remote.run(args=['mkdir', '-p', self.cadir])
110
111 cert.key = f'{self.cadir}/{cert.name}.key'
112 cert.certificate = f'{self.cadir}/{cert.name}.crt'
113
114 san_ext = []
115 add_san_default = False
116 cn = config.get('cn', '')
117 if cn == '':
118 cn = cert.remote.hostname
119 add_san_default = True
120 if config.get('add-san', add_san_default):
121 ext = f'{self.cadir}/{cert.name}.ext'
122 san_ext = ['-extfile', ext]
123
124 # provide the common name in -subj to avoid the openssl command prompts
125 subject = f'/CN={cn}'
126
127 # if a ca certificate is provided, use it to sign the new certificate
128 ca = config.get('ca', None)
129 if ca:
130 # the ca certificate must have been created by a prior ssl task
131 ca_cert = self.ctx.ssl_certificates.get(ca, None)
132 if not ca_cert:
133 raise ConfigError(f'ssl: ca {ca} not found for certificate {cert.name}')
134
135 csr = f'{self.cadir}/{cert.name}.csr'
136 srl = f'{self.cadir}/{ca_cert.name}.srl'
137 remove_files = ['rm', csr, srl]
138
139 # these commands are run on the ca certificate's client because
140 # they need access to its private key and cert
141
142 # generate a private key and signing request
143 ca_cert.remote.run(args=['openssl', 'req', '-nodes',
144 '-newkey', cert.key_type, '-keyout', cert.key,
145 '-out', csr, '-subj', subject])
146
147 if san_ext:
148 remove_files.append(ext)
149 ca_cert.remote.write_file(path=ext,
150 data='subjectAltName = DNS:{},IP:{}'.format(
151 cn,
152 config.get('ip', cert.remote.ip_address)))
153
154 # create the signed certificate
155 ca_cert.remote.run(args=['openssl', 'x509', '-req', '-in', csr,
156 '-CA', ca_cert.certificate, '-CAkey', ca_cert.key, '-CAcreateserial',
157 '-out', cert.certificate, '-days', '365', '-sha256'] + san_ext)
158
159 ca_cert.remote.run(args=remove_files) # clean up the signing request and serial
160
161 # verify the new certificate against its ca cert
162 ca_cert.remote.run(args=['openssl', 'verify',
163 '-CAfile', ca_cert.certificate, cert.certificate])
164
165 if cert.remote != ca_cert.remote:
166 # copy to remote client
167 self.remote_copy_file(ca_cert.remote, cert.certificate, cert.remote, cert.certificate)
168 self.remote_copy_file(ca_cert.remote, cert.key, cert.remote, cert.key)
169 # clean up the local copies
170 ca_cert.remote.run(args=['rm', cert.certificate, cert.key])
171 # verify the remote certificate (requires ca to be in its trusted ca certificate store)
172 cert.remote.run(args=['openssl', 'verify', cert.certificate])
173 else:
174 # otherwise, generate a private key and use it to self-sign a new certificate
175 cert.remote.run(args=['openssl', 'req', '-x509', '-nodes',
176 '-newkey', cert.key_type, '-keyout', cert.key,
177 '-days', '365', '-out', cert.certificate, '-subj', subject])
178
179 if config.get('embed-key', False):
180 # append the private key to the certificate file
181 cert.remote.run(args=['cat', cert.key, run.Raw('>>'), cert.certificate])
182
183 return cert
184
185 def remove_cert(self, cert):
186 """
187 Delete all of the files associated with the given certificate.
188 """
189 # remove the private key and certificate
190 cert.remote.run(args=['rm', '-f', cert.certificate, cert.key])
191
192 # remove ca subdirectory if it's empty
193 cert.remote.run(args=['rmdir', '--ignore-fail-on-non-empty', self.cadir])
194
195 def install_cert(self, cert, client):
196 """
197 Install as a trusted ca certificate on the given client.
198 """
199 (remote,) = self.ctx.cluster.only(client).remotes.keys()
200
201 installed = argparse.Namespace()
202 installed.remote = remote
203
204 if remote.os.package_type == 'deb':
205 installed.path = '/usr/local/share/ca-certificates/{}.crt'.format(cert.name)
206 installed.command = ['sudo', 'update-ca-certificates']
207 else:
208 installed.path = '/usr/share/pki/ca-trust-source/anchors/{}.crt'.format(cert.name)
209 installed.command = ['sudo', 'update-ca-trust']
210
211 cp_or_mv = 'cp'
212 if remote != cert.remote:
213 # copy into remote cadir (with mkdir if necessary)
214 remote.run(args=['mkdir', '-p', self.cadir])
215 self.remote_copy_file(cert.remote, cert.certificate, remote, cert.certificate)
216 cp_or_mv = 'mv' # move this remote copy into the certificate store
217
218 # install into certificate store as root
219 remote.run(args=['sudo', cp_or_mv, cert.certificate, installed.path])
220 remote.run(args=installed.command)
221
222 return installed
223
224 def uninstall_cert(self, installed):
225 """
226 Uninstall a certificate from the trusted certificate store.
227 """
228 installed.remote.run(args=['sudo', 'rm', installed.path])
229 installed.remote.run(args=installed.command)
230
231 def remote_copy_file(self, from_remote, from_path, to_remote, to_path):
232 """
233 Copies a file from one remote to another.
234
235 The remotes don't have public-key auth for 'scp' or misc.copy_file(),
236 so this copies through an intermediate local tmp file.
237 """
238 log.info('copying from {}:{} to {}:{}...'.format(from_remote, from_path, to_remote, to_path))
239 local_path = from_remote.get_file(from_path)
240 try:
241 to_remote.put_file(local_path, to_path)
242 finally:
243 os.remove(local_path)
244
245 task = OpenSSLKeys