]>
Commit | Line | Data |
---|---|---|
11fdf7f2 TL |
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')) | |
e306af50 TL |
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: | |
11fdf7f2 TL |
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 = '{}/{}.key'.format(self.cadir, cert.name) | |
112 | cert.certificate = '{}/{}.crt'.format(self.cadir, cert.name) | |
113 | ||
114 | # provide the common name in -subj to avoid the openssl command prompts | |
115 | subject = '/CN={}'.format(config.get('cn', cert.remote.hostname)) | |
116 | ||
117 | # if a ca certificate is provided, use it to sign the new certificate | |
118 | ca = config.get('ca', None) | |
119 | if ca: | |
120 | # the ca certificate must have been created by a prior ssl task | |
121 | ca_cert = self.ctx.ssl_certificates.get(ca, None) | |
122 | if not ca_cert: | |
123 | raise ConfigError('ssl: ca {} not found for certificate {}' | |
124 | .format(ca, cert.name)) | |
125 | ||
126 | # these commands are run on the ca certificate's client because | |
127 | # they need access to its private key and cert | |
128 | ||
129 | # generate a private key and signing request | |
130 | csr = '{}/{}.csr'.format(self.cadir, cert.name) | |
131 | ca_cert.remote.run(args=['openssl', 'req', '-nodes', | |
132 | '-newkey', cert.key_type, '-keyout', cert.key, | |
133 | '-out', csr, '-subj', subject]) | |
134 | ||
135 | # create the signed certificate | |
136 | ca_cert.remote.run(args=['openssl', 'x509', '-req', '-in', csr, | |
137 | '-CA', ca_cert.certificate, '-CAkey', ca_cert.key, '-CAcreateserial', | |
138 | '-out', cert.certificate, '-days', '365', '-sha256']) | |
139 | ||
140 | srl = '{}/{}.srl'.format(self.cadir, ca_cert.name) | |
141 | ca_cert.remote.run(args=['rm', csr, srl]) # clean up the signing request and serial | |
142 | ||
143 | # verify the new certificate against its ca cert | |
144 | ca_cert.remote.run(args=['openssl', 'verify', | |
145 | '-CAfile', ca_cert.certificate, cert.certificate]) | |
146 | ||
147 | if cert.remote != ca_cert.remote: | |
148 | # copy to remote client | |
149 | self.remote_copy_file(ca_cert.remote, cert.certificate, cert.remote, cert.certificate) | |
150 | self.remote_copy_file(ca_cert.remote, cert.key, cert.remote, cert.key) | |
151 | # clean up the local copies | |
152 | ca_cert.remote.run(args=['rm', cert.certificate, cert.key]) | |
153 | # verify the remote certificate (requires ca to be in its trusted ca certificate store) | |
154 | cert.remote.run(args=['openssl', 'verify', cert.certificate]) | |
155 | else: | |
156 | # otherwise, generate a private key and use it to self-sign a new certificate | |
157 | cert.remote.run(args=['openssl', 'req', '-x509', '-nodes', | |
158 | '-newkey', cert.key_type, '-keyout', cert.key, | |
159 | '-days', '365', '-out', cert.certificate, '-subj', subject]) | |
160 | ||
161 | if config.get('embed-key', False): | |
162 | # append the private key to the certificate file | |
163 | cert.remote.run(args=['cat', cert.key, run.Raw('>>'), cert.certificate]) | |
164 | ||
165 | return cert | |
166 | ||
167 | def remove_cert(self, cert): | |
168 | """ | |
169 | Delete all of the files associated with the given certificate. | |
170 | """ | |
171 | # remove the private key and certificate | |
172 | cert.remote.run(args=['rm', '-f', cert.certificate, cert.key]) | |
173 | ||
174 | # remove ca subdirectory if it's empty | |
175 | cert.remote.run(args=['rmdir', '--ignore-fail-on-non-empty', self.cadir]) | |
176 | ||
177 | def install_cert(self, cert, client): | |
178 | """ | |
179 | Install as a trusted ca certificate on the given client. | |
180 | """ | |
181 | (remote,) = self.ctx.cluster.only(client).remotes.keys() | |
182 | ||
183 | installed = argparse.Namespace() | |
184 | installed.remote = remote | |
185 | ||
186 | if remote.os.package_type == 'deb': | |
187 | installed.path = '/usr/local/share/ca-certificates/{}.crt'.format(cert.name) | |
188 | installed.command = ['sudo', 'update-ca-certificates'] | |
189 | else: | |
190 | installed.path = '/usr/share/pki/ca-trust-source/anchors/{}.crt'.format(cert.name) | |
191 | installed.command = ['sudo', 'update-ca-trust'] | |
192 | ||
193 | cp_or_mv = 'cp' | |
194 | if remote != cert.remote: | |
195 | # copy into remote cadir (with mkdir if necessary) | |
196 | remote.run(args=['mkdir', '-p', self.cadir]) | |
197 | self.remote_copy_file(cert.remote, cert.certificate, remote, cert.certificate) | |
198 | cp_or_mv = 'mv' # move this remote copy into the certificate store | |
199 | ||
200 | # install into certificate store as root | |
201 | remote.run(args=['sudo', cp_or_mv, cert.certificate, installed.path]) | |
202 | remote.run(args=installed.command) | |
203 | ||
204 | return installed | |
205 | ||
206 | def uninstall_cert(self, installed): | |
207 | """ | |
208 | Uninstall a certificate from the trusted certificate store. | |
209 | """ | |
210 | installed.remote.run(args=['sudo', 'rm', installed.path]) | |
211 | installed.remote.run(args=installed.command) | |
212 | ||
213 | def remote_copy_file(self, from_remote, from_path, to_remote, to_path): | |
214 | """ | |
215 | Copies a file from one remote to another. | |
216 | ||
217 | The remotes don't have public-key auth for 'scp' or misc.copy_file(), | |
218 | so this copies through an intermediate local tmp file. | |
219 | """ | |
220 | log.info('copying from {}:{} to {}:{}...'.format(from_remote, from_path, to_remote, to_path)) | |
221 | local_path = from_remote.get_file(from_path) | |
222 | try: | |
223 | to_remote.put_file(local_path, to_path) | |
224 | finally: | |
225 | os.remove(local_path) | |
226 | ||
227 | task = OpenSSLKeys |