Merge lp:~gandelman-a/charms/precise/keystone/ssl_file_sync into lp:~openstack-charmers/charms/precise/keystone/ha-support
- Precise Pangolin (12.04)
- ssl_file_sync
- Merge into ha-support
Status: | Merged |
---|---|
Merged at revision: | 57 |
Proposed branch: | lp:~gandelman-a/charms/precise/keystone/ssl_file_sync |
Merge into: | lp:~openstack-charmers/charms/precise/keystone/ha-support |
Diff against target: |
843 lines (+643/-20) 6 files modified
config.yaml (+4/-0) hooks/keystone-hooks (+74/-5) hooks/keystone_ssl.py (+298/-0) hooks/lib/unison.py (+215/-0) hooks/utils.py (+51/-14) revision (+1/-1) |
To merge this branch: | bzr merge lp:~gandelman-a/charms/precise/keystone/ssl_file_sync |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
James Page | Approve | ||
Review via email: mp+150390@code.launchpad.net |
Commit message
Description of the change
Adds support for Keystone managed SSL certificates for service endpoints. Adds a keystone_ssl.py library that will initialize a local directory as what will be used as the certificate authority. In addition to service credentials, new services that join via the identity-service relation will also receive a signed SSL certificate, key and CA cert. The remote service charm is in charge of using this cert/key to configure its own HTTPS. When it has done so, it can update its endpoint in keystone catalog via its relation. Every service now has an optional ssl_cert + ssl_key config option in case admin wants to update certificates manually, override those installed by keystone, or use certificates signed by a real certificate authority.
The KS certificate authority needs to be synchronized among all peer keystone units. To acehive this, I've added a new unison.py library that takes care of setting up peer SSH access via an intermediate user, and allows for file and directory synchronization between peers using unison. It's assumed that only one node will be synchornizing files to peers (the eligible_leader). I've replaced the old file-sync-
It is not required that the remote charms support HTTPS endpoint reconfiguration, they can simply choose not to use the generated certificate data. But the reviews for charms that support this are filed:
https:/
https:/
https:/
https:/
Adam Gandelman (gandelman-a) wrote : | # |
- 60. By Adam Gandelman
-
Rebase against current ha-support branch.
Preview Diff
1 | === modified file 'config.yaml' |
2 | --- config.yaml 2013-01-22 17:49:36 +0000 |
3 | +++ config.yaml 2013-03-08 21:36:24 +0000 |
4 | @@ -112,3 +112,7 @@ |
5 | default: "false" |
6 | type: string |
7 | description: "Enable PKI token signing (Grizzly and beyond)" |
8 | + https-service-endpoints: |
9 | + default: "False" |
10 | + type: string |
11 | + description: "Manage SSL certificates for all service endpoints." |
12 | |
13 | === added symlink 'hooks/cluster-relation-joined' |
14 | === target is u'keystone-hooks' |
15 | === modified file 'hooks/keystone-hooks' |
16 | --- hooks/keystone-hooks 2013-02-22 19:20:54 +0000 |
17 | +++ hooks/keystone-hooks 2013-03-08 21:36:24 +0000 |
18 | @@ -2,13 +2,18 @@ |
19 | |
20 | import sys |
21 | import time |
22 | +import urlparse |
23 | + |
24 | +from base64 import b64encode |
25 | |
26 | from utils import * |
27 | + |
28 | from lib.openstack_common import * |
29 | +import lib.unison as unison |
30 | |
31 | config = config_get() |
32 | |
33 | -packages = "keystone python-mysqldb pwgen haproxy python-jinja2" |
34 | +packages = "keystone python-mysqldb pwgen haproxy python-jinja2 openssl unison" |
35 | service = "keystone" |
36 | |
37 | # used to verify joined services are valid openstack components. |
38 | @@ -81,6 +86,11 @@ |
39 | execute("service keystone stop", echo=True) |
40 | execute("keystone-manage db_sync") |
41 | execute("service keystone start", echo=True) |
42 | + |
43 | + # ensure /var/lib/keystone is g+wrx for peer relations that |
44 | + # may be syncing data there via SSH_USER. |
45 | + execute("chmod -R g+wrx /var/lib/keystone/") |
46 | + |
47 | time.sleep(5) |
48 | ensure_initial_admin(config) |
49 | |
50 | @@ -97,6 +107,7 @@ |
51 | 'db_host' not in relation_data): |
52 | juju_log("db_host or password not set. Peer not ready, exit 0") |
53 | exit(0) |
54 | + |
55 | update_config_block('sql', connection="mysql://%s:%s@%s/%s" % |
56 | (config["database-user"], |
57 | relation_data["password"], |
58 | @@ -125,6 +136,21 @@ |
59 | (id, unit)) |
60 | identity_changed(relation_id=id, remote_unit=unit) |
61 | |
62 | +def ensure_valid_service(service): |
63 | + if service not in valid_services.keys(): |
64 | + juju_log("WARN: Invalid service requested: '%s'" % service) |
65 | + realtion_set({ "admin_token": -1 }) |
66 | + return |
67 | + |
68 | +def add_endpoint(region, service, public_url, admin_url, internal_url): |
69 | + desc = valid_services[service]["desc"] |
70 | + service_type = valid_services[service]["type"] |
71 | + create_service_entry(service, service_type, desc) |
72 | + create_endpoint_template(region=region, service=service, |
73 | + public_url=public_url, |
74 | + admin_url=admin_url, |
75 | + internal_url=internal_url) |
76 | + |
77 | def identity_joined(): |
78 | """ Do nothing until we get information about requested service """ |
79 | pass |
80 | @@ -170,7 +196,6 @@ |
81 | 'internal_url']) |
82 | if single.issubset(settings): |
83 | # other end of relation advertised only one endpoint |
84 | - |
85 | if 'None' in [v for k,v in settings.iteritems()]: |
86 | # Some backend services advertise no endpoint but require a |
87 | # hook execution to update auth strategy. |
88 | @@ -191,11 +216,14 @@ |
89 | |
90 | |
91 | ensure_valid_service(settings['service']) |
92 | + |
93 | add_endpoint(region=settings['region'], service=settings['service'], |
94 | publicurl=settings['public_url'], |
95 | adminurl=settings['admin_url'], |
96 | internalurl=settings['internal_url']) |
97 | service_username = settings['service'] |
98 | + https_cn = urlparse.urlparse(settings['internal_url']) |
99 | + https_cn = https_cn.hostname |
100 | else: |
101 | # assemble multiple endpoints from relation data. service name |
102 | # should be prepended to setting name, ie: |
103 | @@ -217,10 +245,11 @@ |
104 | for k,v in settings.iteritems(): |
105 | ep = k.split('_')[0] |
106 | x = '_'.join(k.split('_')[1:]) |
107 | - if ep not in endpoints: |
108 | + if ep not in endpoints: |
109 | endpoints[ep] = {} |
110 | endpoints[ep][x] = v |
111 | services = [] |
112 | + https_cn = None |
113 | for ep in endpoints: |
114 | # weed out any unrelated relation stuff Juju might have added |
115 | # by ensuring each possible endpiont has appropriate fields |
116 | @@ -233,6 +262,9 @@ |
117 | adminurl=ep['admin_url'], |
118 | internalurl=ep['internal_url']) |
119 | services.append(ep['service']) |
120 | + if not https_cn: |
121 | + https_cn = urlparse.urlparse(ep['internal_url']) |
122 | + https_cn = https_cn.hostname |
123 | service_username = '_'.join(services) |
124 | |
125 | if 'None' in [v for k,v in settings.iteritems()]: |
126 | @@ -262,8 +294,16 @@ |
127 | "auth_port": config["admin-port"], |
128 | "service_username": service_username, |
129 | "service_password": service_password, |
130 | - "service_tenant": config['service-tenant'] |
131 | + "service_tenant": config['service-tenant'], |
132 | + "https_keystone": "False", |
133 | + "ssl_cert": "", |
134 | + "ssl_key": "", |
135 | + "ca_cert": "" |
136 | } |
137 | + |
138 | + if relation_id: |
139 | + relation_data['rid'] = relation_id |
140 | + |
141 | # Check if clustered and use vip + haproxy ports if so |
142 | if is_clustered(): |
143 | relation_data["auth_host"] = config['vip'] |
144 | @@ -271,7 +311,19 @@ |
145 | relation_data["service_host"] = config['vip'] |
146 | relation_data["service_port"] = SERVICE_PORTS['keystone_service'] |
147 | |
148 | - relation_set(relation_data) |
149 | + # generate or get a new cert/key for service if set to manage certs. |
150 | + if config['https-service-endpoints'] in ['True', 'true']: |
151 | + ca = get_ca(user=SSH_USER) |
152 | + service = os.getenv('JUJU_REMOTE_UNIT').split('/')[0] |
153 | + cert, key = ca.get_cert_and_key(common_name=https_cn) |
154 | + ca_bundle= ca.get_ca_bundle() |
155 | + relation_data['ssl_cert'] = b64encode(cert) |
156 | + relation_data['ssl_key'] = b64encode(key) |
157 | + relation_data['ca_cert'] = b64encode(ca_bundle) |
158 | + relation_data['https_keystone'] = 'True' |
159 | + unison.sync_to_peers(peer_interface='cluster', |
160 | + paths=[SSL_DIR], user=SSH_USER, verbose=True) |
161 | + relation_set_2(**relation_data) |
162 | synchronize_service_credentials() |
163 | |
164 | def config_changed(): |
165 | @@ -318,12 +370,24 @@ |
166 | "keystone_service": int(config['service-port']) + 1 |
167 | } |
168 | |
169 | +def cluster_joined(): |
170 | + unison.ssh_authorized_peers(user=SSH_USER, |
171 | + group='keystone', |
172 | + peer_interface='cluster', |
173 | + ensure_user=True) |
174 | |
175 | def cluster_changed(): |
176 | + unison.ssh_authorized_peers(user=SSH_USER, |
177 | + group='keystone', |
178 | + peer_interface='cluster', |
179 | + ensure_user=True) |
180 | cluster_hosts = {} |
181 | cluster_hosts['self'] = config['hostname'] |
182 | for r_id in relation_ids('cluster'): |
183 | for unit in relation_list(r_id): |
184 | + # trigger identity-changed to reconfigure HTTPS |
185 | + # as necessary. |
186 | + identity_changed(relation_id=r_id, remote_unit=unit) |
187 | cluster_hosts[unit.replace('/','-')] = \ |
188 | relation_get_dict(relation_id=r_id, |
189 | remote_unit=unit)['private-address'] |
190 | @@ -332,6 +396,10 @@ |
191 | |
192 | synchronize_service_credentials() |
193 | |
194 | + for r_id in relation_ids('identity-service'): |
195 | + for unit in relation_list(r_id): |
196 | + # trigger identity-changed to reconfigure HTTPS as necessary |
197 | + identity_changed(relation_id=r_id, remote_unit=unit) |
198 | |
199 | def ha_relation_changed(): |
200 | relation_data = relation_get_dict() |
201 | @@ -400,6 +468,7 @@ |
202 | "identity-service-relation-joined": identity_joined, |
203 | "identity-service-relation-changed": identity_changed, |
204 | "config-changed": config_changed, |
205 | + "cluster-relation-joined": cluster_joined, |
206 | "cluster-relation-changed": cluster_changed, |
207 | "cluster-relation-departed": cluster_changed, |
208 | "ha-relation-joined": ha_relation_joined, |
209 | |
210 | === added file 'hooks/keystone_ssl.py' |
211 | --- hooks/keystone_ssl.py 1970-01-01 00:00:00 +0000 |
212 | +++ hooks/keystone_ssl.py 2013-03-08 21:36:24 +0000 |
213 | @@ -0,0 +1,298 @@ |
214 | +#!/usr/bin/python |
215 | + |
216 | +import base64 |
217 | +import os |
218 | +import shutil |
219 | +import subprocess |
220 | +import tarfile |
221 | +import tempfile |
222 | +from utils import * |
223 | + |
224 | +CA_EXPIRY = '365' |
225 | +ORG_NAME = 'Ubuntu' |
226 | +ORG_UNIT = 'Ubuntu Cloud' |
227 | +CA_BUNDLE='/usr/local/share/ca-certificates/juju_ca_cert.crt' |
228 | + |
229 | +CA_CONFIG = """ |
230 | +[ ca ] |
231 | +default_ca = CA_default |
232 | + |
233 | +[ CA_default ] |
234 | +dir = %(ca_dir)s |
235 | +policy = policy_match |
236 | +database = $dir/index.txt |
237 | +serial = $dir/serial |
238 | +certs = $dir/certs |
239 | +crl_dir = $dir/crl |
240 | +new_certs_dir = $dir/newcerts |
241 | +certificate = $dir/cacert.pem |
242 | +private_key = $dir/private/cacert.key |
243 | +RANDFILE = $dir/private/.rand |
244 | +default_md = default |
245 | + |
246 | +[ req ] |
247 | +default_bits = 1024 |
248 | +default_md = sha1 |
249 | + |
250 | +prompt = no |
251 | +distinguished_name = ca_distinguished_name |
252 | + |
253 | +x509_extensions = ca_extensions |
254 | + |
255 | +[ ca_distinguished_name ] |
256 | +organizationName = %(org_name)s |
257 | +organizationalUnitName = %(org_unit_name)s Certificate Authority |
258 | +commonName = %(common_name)s |
259 | + |
260 | +[ policy_match ] |
261 | +countryName = optional |
262 | +stateOrProvinceName = optional |
263 | +organizationName = match |
264 | +organizationalUnitName = optional |
265 | +commonName = supplied |
266 | + |
267 | +[ ca_extensions ] |
268 | +basicConstraints = critical,CA:true |
269 | +subjectKeyIdentifier = hash |
270 | +authorityKeyIdentifier = keyid:always, issuer |
271 | +keyUsage = cRLSign, keyCertSign |
272 | +""" |
273 | + |
274 | +SIGNING_CONFIG=""" |
275 | +[ ca ] |
276 | +default_ca = CA_default |
277 | + |
278 | +[ CA_default ] |
279 | +dir = %(ca_dir)s |
280 | +policy = policy_match |
281 | +database = $dir/index.txt |
282 | +serial = $dir/serial |
283 | +certs = $dir/certs |
284 | +crl_dir = $dir/crl |
285 | +new_certs_dir = $dir/newcerts |
286 | +certificate = $dir/cacert.pem |
287 | +private_key = $dir/private/cacert.key |
288 | +RANDFILE = $dir/private/.rand |
289 | +default_md = default |
290 | + |
291 | +[ req ] |
292 | +default_bits = 1024 |
293 | +default_md = sha1 |
294 | + |
295 | +prompt = no |
296 | +distinguished_name = req_distinguished_name |
297 | + |
298 | +x509_extensions = req_extensions |
299 | + |
300 | +[ req_distinguished_name ] |
301 | +organizationName = %(org_name)s |
302 | +organizationalUnitName = %(org_unit_name)s Server Farm |
303 | + |
304 | +[ policy_match ] |
305 | +countryName = optional |
306 | +stateOrProvinceName = optional |
307 | +organizationName = match |
308 | +organizationalUnitName = optional |
309 | +commonName = supplied |
310 | + |
311 | +[ req_extensions ] |
312 | +basicConstraints = CA:false |
313 | +subjectKeyIdentifier = hash |
314 | +authorityKeyIdentifier = keyid:always, issuer |
315 | +keyUsage = digitalSignature, keyEncipherment, keyAgreement |
316 | +extendedKeyUsage = serverAuth, clientAuth |
317 | +""" |
318 | + |
319 | + |
320 | +def init_ca(ca_dir, common_name, org_name=ORG_NAME, org_unit_name=ORG_UNIT): |
321 | + print 'Ensuring certificate authority exists at %s.' % ca_dir |
322 | + if not os.path.exists(ca_dir): |
323 | + print 'Initializing new certificate authority at %s' % ca_dir |
324 | + os.mkdir(ca_dir) |
325 | + |
326 | + for i in ['certs', 'crl', 'newcerts', 'private']: |
327 | + d = os.path.join(ca_dir, i) |
328 | + if not os.path.exists(d): |
329 | + print 'Creating %s.' % d |
330 | + os.mkdir(d) |
331 | + os.chmod(os.path.join(ca_dir, 'private'), 0710) |
332 | + |
333 | + if not os.path.isfile(os.path.join(ca_dir, 'serial')): |
334 | + with open(os.path.join(ca_dir, 'serial'), 'wb') as out: |
335 | + out.write('01\n') |
336 | + |
337 | + if not os.path.isfile(os.path.join(ca_dir, 'index.txt')): |
338 | + with open(os.path.join(ca_dir, 'index.txt'), 'wb') as out: |
339 | + out.write('') |
340 | + if not os.path.isfile(os.path.join(ca_dir, 'ca.cnf')): |
341 | + print 'Creating new CA config in %s' % ca_dir |
342 | + with open(os.path.join(ca_dir, 'ca.cnf'), 'wb') as out: |
343 | + out.write(CA_CONFIG % locals()) |
344 | + |
345 | + |
346 | +def root_ca_crt_key(ca_dir): |
347 | + init = False |
348 | + crt = os.path.join(ca_dir, 'cacert.pem') |
349 | + key = os.path.join(ca_dir, 'private', 'cacert.key') |
350 | + for f in [crt, key]: |
351 | + if not os.path.isfile(f): |
352 | + print 'Missing %s, will re-initialize cert+key.' % f |
353 | + init = True |
354 | + else: |
355 | + print 'Found %s.' % f |
356 | + if init: |
357 | + cmd = ['openssl', 'req', '-config', os.path.join(ca_dir, 'ca.cnf'), '-x509', |
358 | + '-nodes', '-newkey', 'rsa', '-days', '21360', '-keyout', key, |
359 | + '-out', crt, '-outform', 'PEM'] |
360 | + subprocess.check_call(cmd) |
361 | + return crt, key |
362 | + |
363 | + |
364 | +def intermediate_ca_csr_key(ca_dir): |
365 | + print 'Creating new intermediate CSR.' |
366 | + key = os.path.join(ca_dir, 'private', 'cacert.key') |
367 | + csr = os.path.join(ca_dir, 'cacert.csr') |
368 | + cmd = ['openssl', 'req', '-config', os.path.join(ca_dir, 'ca.cnf'), '-sha1', |
369 | + '-newkey', 'rsa', '-nodes', '-keyout', key, '-out', csr, '-outform', |
370 | + 'PEM'] |
371 | + subprocess.check_call(cmd) |
372 | + return csr, key |
373 | + |
374 | + |
375 | +def sign_int_csr(ca_dir, csr, common_name): |
376 | + print 'Signing certificate request %s.' % csr |
377 | + crt = os.path.join(ca_dir, 'certs', |
378 | + '%s.crt' % os.path.basename(csr).split('.')[0]) |
379 | + subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name) |
380 | + cmd = ['openssl', 'ca', '-batch', '-config', os.path.join(ca_dir, 'ca.cnf'), |
381 | + '-extensions', 'ca_extensions', '-days', CA_EXPIRY, '-notext', |
382 | + '-in', csr, '-out', crt, '-subj', subj, '-batch'] |
383 | + print ' '.join(cmd) |
384 | + subprocess.check_call(cmd) |
385 | + return crt |
386 | + |
387 | + |
388 | +def init_root_ca(ca_dir, common_name): |
389 | + init_ca(ca_dir, common_name) |
390 | + return root_ca_crt_key(ca_dir) |
391 | + |
392 | + |
393 | +def init_intermediate_ca(ca_dir, common_name, root_ca_dir, |
394 | + org_name=ORG_NAME, org_unit_name=ORG_UNIT): |
395 | + init_ca(ca_dir, common_name) |
396 | + if not os.path.isfile(os.path.join(ca_dir, 'cacert.pem')): |
397 | + csr, key = intermediate_ca_csr_key(ca_dir) |
398 | + crt = sign_int_csr(root_ca_dir, csr, common_name) |
399 | + shutil.copy(crt, os.path.join(ca_dir, 'cacert.pem')) |
400 | + else: |
401 | + print 'Intermediate CA certificate already exists.' |
402 | + |
403 | + if not os.path.isfile(os.path.join(ca_dir, 'signing.cnf')): |
404 | + print 'Creating new signing config in %s' % ca_dir |
405 | + with open(os.path.join(ca_dir, 'signing.cnf'), 'wb') as out: |
406 | + out.write(SIGNING_CONFIG % locals()) |
407 | + |
408 | + |
409 | +def create_certificate(ca_dir, service): |
410 | + common_name = service |
411 | + subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name) |
412 | + csr = os.path.join(ca_dir, 'certs', '%s.csr' % service) |
413 | + key = os.path.join(ca_dir, 'certs', '%s.key' % service) |
414 | + cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout', |
415 | + key, '-out', csr, '-subj', subj] |
416 | + subprocess.check_call(cmd) |
417 | + crt = sign_csr(ca_dir, csr, common_name) |
418 | + print 'Signed new CSR, crt @ %s' % crt |
419 | + return |
420 | + |
421 | +def update_bundle(bundle_file, new_bundle): |
422 | + return |
423 | + if os.path.isfile(bundle_file): |
424 | + current = open(bundle_file, 'r').read().strip() |
425 | + if new_bundle == current: |
426 | + print 'CA Bundle @ %s is up to date.' % bundle_file |
427 | + return |
428 | + else: |
429 | + print 'Updating CA bundle @ %s.' % bundle_file |
430 | + |
431 | + with open(bundle_file, 'wb') as out: |
432 | + out.write(new_bundle) |
433 | + subprocess.check_call(['update-ca-certificates']) |
434 | + |
435 | +def tar_directory(path): |
436 | + cwd = os.getcwd() |
437 | + parent=os.path.dirname(path) |
438 | + directory=os.path.basename(path) |
439 | + tmp = tempfile.TemporaryFile() |
440 | + os.chdir(parent) |
441 | + tarball = tarfile.TarFile(fileobj=tmp, mode='w') |
442 | + tarball.add(directory) |
443 | + tarball.close() |
444 | + tmp.seek(0) |
445 | + out = tmp.read() |
446 | + tmp.close() |
447 | + os.chdir(cwd) |
448 | + return out |
449 | + |
450 | +class JujuCA(object): |
451 | + def __init__(self, name, ca_dir, root_ca_dir, user, group): |
452 | + root_crt, root_key = init_root_ca(root_ca_dir, |
453 | + '%s Certificate Authority' % name) |
454 | + init_intermediate_ca(ca_dir, |
455 | + '%s Intermediate Certificate Authority' % name, |
456 | + root_ca_dir) |
457 | + cmd = ['chown', '-R', '%s.%s' % (user, group), ca_dir] |
458 | + subprocess.check_call(cmd) |
459 | + cmd = ['chown', '-R', '%s.%s' % (user, group), root_ca_dir] |
460 | + subprocess.check_call(cmd) |
461 | + self.ca_dir = ca_dir |
462 | + self.root_ca_dir = root_ca_dir |
463 | + self.user = user |
464 | + self.group = group |
465 | + update_bundle(CA_BUNDLE, self.get_ca_bundle()) |
466 | + |
467 | + def _sign_csr(self, csr, service, common_name): |
468 | + subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name) |
469 | + crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name) |
470 | + cmd = ['openssl', 'ca', '-config', |
471 | + os.path.join(self.ca_dir, 'signing.cnf'), '-extensions', |
472 | + 'req_extensions', '-days', '365', '-notext', '-in', csr, |
473 | + '-out', crt, '-batch', '-subj', subj] |
474 | + subprocess.check_call(cmd) |
475 | + return crt |
476 | + |
477 | + def _create_certificate(self, service, common_name): |
478 | + subj = '/O=%s/OU=%s/CN=%s' % (ORG_NAME, ORG_UNIT, common_name) |
479 | + csr = os.path.join(self.ca_dir, 'certs', '%s.csr' % service) |
480 | + key = os.path.join(self.ca_dir, 'certs', '%s.key' % service) |
481 | + cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa', '-nodes', '-keyout', |
482 | + key, '-out', csr, '-subj', subj] |
483 | + subprocess.check_call(cmd) |
484 | + crt = self._sign_csr(csr, service, common_name) |
485 | + cmd = ['chown', '-R', '%s.%s' % (self.user, self.group), self.ca_dir] |
486 | + subprocess.check_call(cmd) |
487 | + print 'Signed new CSR, crt @ %s' % crt |
488 | + return crt, key |
489 | + |
490 | + def get_cert_and_key(self, common_name): |
491 | + print 'Getting certificate and key for %s.' % common_name |
492 | + key = os.path.join(self.ca_dir, 'certs', '%s.key' % common_name) |
493 | + crt = os.path.join(self.ca_dir, 'certs', '%s.crt' % common_name) |
494 | + if os.path.isfile(crt): |
495 | + print 'Found existing certificate for %s.' % common_name |
496 | + crt = open(crt, 'r').read() |
497 | + try: |
498 | + key = open(key, 'r').read() |
499 | + except: |
500 | + print 'Could not load ssl private key for %s from %s' %\ |
501 | + (common_name, key) |
502 | + exit(1) |
503 | + return crt, key |
504 | + crt, key = self._create_certificate(common_name, common_name) |
505 | + return open(crt, 'r').read(), open(key, 'r').read() |
506 | + |
507 | + def get_ca_bundle(self): |
508 | + int_cert = open(os.path.join(self.ca_dir, 'cacert.pem')).read() |
509 | + root_cert = open(os.path.join(self.root_ca_dir, 'cacert.pem')).read() |
510 | + # NOTE: ordering of certs in bundle matters! |
511 | + return int_cert + root_cert |
512 | |
513 | === added file 'hooks/lib/unison.py' |
514 | --- hooks/lib/unison.py 1970-01-01 00:00:00 +0000 |
515 | +++ hooks/lib/unison.py 2013-03-08 21:36:24 +0000 |
516 | @@ -0,0 +1,215 @@ |
517 | +#!/usr/bin/python |
518 | +# |
519 | +# Easy file synchronization among peer units using ssh + unison. |
520 | +# |
521 | +# From *both* peer relation -joined and -changed, add a call to |
522 | +# ssh_authorized_peers() describing the peer relation and the desired |
523 | +# user + group. After all peer relations have settled, all hosts should |
524 | +# be able to connect to on another via key auth'd ssh as the specified user. |
525 | +# |
526 | +# Other hooks are then free to synchronize files and directories using |
527 | +# sync_to_peers(). |
528 | +# |
529 | +# For a peer relation named 'cluster', for example: |
530 | +# |
531 | +# cluster-relation-joined: |
532 | +# ... |
533 | +# ssh_authorized_peers(peer_interface='cluster', |
534 | +# user='juju_ssh', group='juju_ssh', |
535 | +# ensure_user=True) |
536 | +# ... |
537 | +# |
538 | +# cluster-relation-changed: |
539 | +# ... |
540 | +# ssh_authorized_peers(peer_interface='cluster', |
541 | +# user='juju_ssh', group='juju_ssh', |
542 | +# ensure_user=True) |
543 | +# ... |
544 | +# |
545 | +# Hooks are now free to sync files as easily as: |
546 | +# |
547 | +# files = ['/etc/fstab', '/etc/apt.conf.d/'] |
548 | +# sync_to_peers(peer_interface='cluster', |
549 | +# user='juju_ssh, paths=[files]) |
550 | +# |
551 | +# It is assumed the charm itself has setup permissions on each unit |
552 | +# such that 'juju_ssh' has read + write permissions. Also assumed |
553 | +# that the calling charm takes care of leader delegation. |
554 | +# |
555 | +# TODO: Currently depends on the utils.py shipped with the keystone charm. |
556 | +# Either copy required functionality to this library or depend on |
557 | +# something more generic. |
558 | + |
559 | +import json |
560 | +import os |
561 | +import sys |
562 | +import utils |
563 | +import subprocess |
564 | +import shutil |
565 | +import grp |
566 | +import pwd |
567 | + |
568 | + |
569 | +def get_homedir(user): |
570 | + try: |
571 | + user = pwd.getpwnam(user) |
572 | + return user.pw_dir |
573 | + except KeyError: |
574 | + utils.juju_log('Could not get homedir for user %s: user exists?') |
575 | + sys.exit(1) |
576 | + |
577 | + |
578 | +def get_keypair(user): |
579 | + home_dir = get_homedir(user) |
580 | + ssh_dir = os.path.join(home_dir, '.ssh') |
581 | + if not os.path.isdir(ssh_dir): |
582 | + os.mkdir(ssh_dir) |
583 | + |
584 | + priv_key = os.path.join(ssh_dir, 'id_rsa') |
585 | + if not os.path.isfile(priv_key): |
586 | + utils.juju_log('Generating new ssh key for user %s.' % user) |
587 | + cmd = ['ssh-keygen', '-q', '-N', '', '-t', 'rsa', '-b', '2048', |
588 | + '-f', priv_key] |
589 | + subprocess.check_call(cmd) |
590 | + |
591 | + pub_key = '%s.pub' % priv_key |
592 | + if not os.path.isfile(pub_key): |
593 | + utils.juju_log('Generatring missing ssh public key @ %s.' % pub_key) |
594 | + cmd = ['ssh-keygen', '-y', '-f', priv_key] |
595 | + p = subprocess.check_output(cmd).strip() |
596 | + with open(pub_key, 'wb') as out: |
597 | + out.write(p) |
598 | + subprocess.check_call(['chown', '-R', user, ssh_dir]) |
599 | + return open(priv_key, 'r').read().strip(), open(pub_key, 'r').read().strip() |
600 | + |
601 | + |
602 | +def write_authorized_keys(user, keys): |
603 | + home_dir = get_homedir(user) |
604 | + ssh_dir = os.path.join(home_dir, '.ssh') |
605 | + auth_keys = os.path.join(ssh_dir, 'authorized_keys') |
606 | + utils.juju_log('Syncing authorized_keys @ %s.' % auth_keys) |
607 | + with open(auth_keys, 'wb') as out: |
608 | + for k in keys: |
609 | + out.write('%s\n' % k) |
610 | + |
611 | + |
612 | +def write_known_hosts(user, hosts): |
613 | + home_dir = get_homedir(user) |
614 | + ssh_dir = os.path.join(home_dir, '.ssh') |
615 | + known_hosts = os.path.join(ssh_dir, 'known_hosts') |
616 | + khosts = [] |
617 | + for host in hosts: |
618 | + cmd = ['ssh-keyscan', '-H', '-t', 'rsa', host] |
619 | + remote_key = subprocess.check_output(cmd).strip() |
620 | + khosts.append(remote_key) |
621 | + utils.juju_log('Syncing known_hosts @ %s.' % known_hosts) |
622 | + with open(known_hosts, 'wb') as out: |
623 | + for host in khosts: |
624 | + out.write('%s\n' % host) |
625 | + |
626 | + |
627 | +def _ensure_user(user, group=None): |
628 | + # need to ensure a bash shell'd user exists. |
629 | + try: |
630 | + pwd.getpwnam(user) |
631 | + except KeyError: |
632 | + utils.juju_log('Creating new user %s.%s.' % (user, group)) |
633 | + cmd = ['adduser', '--system', '--shell', '/bin/bash', user] |
634 | + if group: |
635 | + try: |
636 | + grp.getgrnam(group) |
637 | + except KeyError: |
638 | + subprocess.check_call(['addgroup', group]) |
639 | + cmd += ['--ingroup', group] |
640 | + subprocess.check_call(cmd) |
641 | + |
642 | + |
643 | +def ssh_authorized_peers(peer_interface, user, group=None, ensure_user=False): |
644 | + """ |
645 | + Main setup function, should be called from both peer -changed and -joined |
646 | + hooks with the same parameters. |
647 | + """ |
648 | + if ensure_user: |
649 | + _ensure_user(user, group) |
650 | + priv_key, pub_key = get_keypair(user) |
651 | + hook = os.path.basename(sys.argv[0]) |
652 | + if hook == '%s-relation-joined' % peer_interface: |
653 | + utils.relation_set_2(ssh_pub_key=pub_key) |
654 | + print 'joined' |
655 | + elif hook == '%s-relation-changed' % peer_interface: |
656 | + hosts = [] |
657 | + keys = [] |
658 | + for r_id in utils.relation_ids(peer_interface): |
659 | + for unit in utils.relation_list(r_id): |
660 | + settings = utils.relation_get_dict(relation_id=r_id, |
661 | + remote_unit=unit) |
662 | + if 'ssh_pub_key' in settings: |
663 | + keys.append(settings['ssh_pub_key']) |
664 | + hosts.append(settings['private-address']) |
665 | + else: |
666 | + utils.juju_log('ssh_authorized_peers(): ssh_pub_key '\ |
667 | + 'missing for unit %s, skipping.' % unit) |
668 | + write_authorized_keys(user, keys) |
669 | + write_known_hosts(user, hosts) |
670 | + authed_hosts = ':'.join(hosts) |
671 | + utils.relation_set_2(ssh_authorized_hosts=authed_hosts) |
672 | + |
673 | + |
674 | +def _run_as_user(user): |
675 | + try: |
676 | + user = pwd.getpwnam(user) |
677 | + except KeyError: |
678 | + utils.juju_log('Invalid user: %s' % user) |
679 | + sys.exit(1) |
680 | + uid, gid = user.pw_uid, user.pw_gid |
681 | + os.environ['HOME'] = user.pw_dir |
682 | + def _inner(): |
683 | + os.setgid(gid) |
684 | + os.setuid(uid) |
685 | + return _inner |
686 | + |
687 | +def run_as_user(user, cmd): |
688 | + return subprocess.check_output(cmd, preexec_fn=_run_as_user(user)) |
689 | + |
690 | +def sync_to_peers(peer_interface, user, paths=[], verbose=False): |
691 | + base_cmd = ['unison', '-auto', '-batch=true', '-confirmbigdel=false', |
692 | + '-fastcheck=true', '-group=false', '-owner=false', '-prefer=newer', |
693 | + '-times=true'] |
694 | + if not verbose: |
695 | + base_cmd.append('-silent') |
696 | + |
697 | + hosts = [] |
698 | + for r_id in (utils.relation_ids(peer_interface) or []): |
699 | + for unit in utils.relation_list(r_id): |
700 | + settings = utils.relation_get_dict(relation_id=r_id, |
701 | + remote_unit=unit) |
702 | + try: |
703 | + authed_hosts = settings['ssh_authorized_hosts'].split(':') |
704 | + except KeyError: |
705 | + print 'unison sync_to_peers: peer has not authorized *any* '\ |
706 | + 'hosts yet.' |
707 | + return |
708 | + |
709 | + unit_hostname = utils.unit_get('private-address') |
710 | + add_host = None |
711 | + for authed_host in authed_hosts: |
712 | + if unit_hostname == authed_host: |
713 | + add_host = settings['private-address'] |
714 | + if add_host: |
715 | + hosts.append(settings['private-address']) |
716 | + else: |
717 | + print 'unison sync_to_peers: peer (%s) has not authorized '\ |
718 | + '*this* host yet, skipping.' %\ |
719 | + settings['private-address'] |
720 | + |
721 | + for path in paths: |
722 | + # removing trailing slash from directory paths, unison |
723 | + # doesn't like these. |
724 | + if path.endswith('/'): |
725 | + path = path[:(len(path)-1)] |
726 | + for host in hosts: |
727 | + cmd = base_cmd + [path, 'ssh://%s@%s/%s' % (user, host, path)] |
728 | + utils.juju_log('Syncing local path %s to %s@%s:%s' %\ |
729 | + (path, user, host, path)) |
730 | + print ' '.join(cmd) |
731 | + run_as_user(user, cmd) |
732 | |
733 | === modified file 'hooks/utils.py' |
734 | --- hooks/utils.py 2013-02-22 19:20:54 +0000 |
735 | +++ hooks/utils.py 2013-03-08 21:36:24 +0000 |
736 | @@ -4,15 +4,25 @@ |
737 | import sys |
738 | import json |
739 | import os |
740 | +import tarfile |
741 | +import tempfile |
742 | import time |
743 | |
744 | from lib.openstack_common import * |
745 | |
746 | +import keystone_ssl as ssl |
747 | +import lib.unison as unison |
748 | + |
749 | keystone_conf = "/etc/keystone/keystone.conf" |
750 | stored_passwd = "/var/lib/keystone/keystone.passwd" |
751 | stored_token = "/var/lib/keystone/keystone.token" |
752 | SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd' |
753 | |
754 | +SSL_DIR = '/var/lib/keystone/juju_ssl/' |
755 | +SSL_CA_NAME = 'Ubuntu Cloud' |
756 | + |
757 | +SSH_USER='juju_keystone' |
758 | + |
759 | def execute(cmd, die=False, echo=False): |
760 | """ Executes a command |
761 | |
762 | @@ -95,6 +105,19 @@ |
763 | cmd += args |
764 | subprocess.check_call(cmd) |
765 | |
766 | + |
767 | +def unit_get(attribute): |
768 | + cmd = [ |
769 | + 'unit-get', |
770 | + attribute |
771 | + ] |
772 | + value = subprocess.check_output(cmd).strip() # IGNORE:E1103 |
773 | + if value == "": |
774 | + return None |
775 | + else: |
776 | + return value |
777 | + |
778 | + |
779 | def relation_get(relation_data): |
780 | """ Obtain all current relation data |
781 | relation_data is a list of options to query from the relation |
782 | @@ -356,8 +379,7 @@ |
783 | create_role("KeystoneAdmin", config["admin-user"], 'admin') |
784 | create_role("KeystoneServiceAdmin", config["admin-user"], 'admin') |
785 | create_service_entry("keystone", "identity", "Keystone Identity Service") |
786 | - # following documentation here, perhaps we should be using juju |
787 | - # public/private addresses for public/internal urls. |
788 | + |
789 | if is_clustered(): |
790 | juju_log("Creating endpoint for clustered configuration") |
791 | for region in config['region'].split(): |
792 | @@ -543,17 +565,32 @@ |
793 | Broadcast service credentials to peers or consume those that have been |
794 | broadcasted by peer, depending on hook context. |
795 | ''' |
796 | - if os.path.basename(sys.argv[0]) == 'cluster-relation-changed': |
797 | - r_data = relation_get_dict() |
798 | - if 'service_credentials' in r_data: |
799 | - juju_log('Saving service passwords from peer.') |
800 | - save_stored_passwords(**json.loads(r_data['service_credentials'])) |
801 | - return |
802 | - |
803 | - creds = load_stored_passwords() |
804 | - if not creds: |
805 | + if (not eligible_leader() or |
806 | + not os.path.isfile(SERVICE_PASSWD_PATH)): |
807 | return |
808 | juju_log('Synchronizing service passwords to all peers.') |
809 | - creds = json.dumps(creds) |
810 | - for r_id in (relation_ids('cluster') or []): |
811 | - relation_set_2(rid=r_id, service_credentials=creds) |
812 | + unison.sync_to_peers(peer_interface='cluster', |
813 | + paths=[SERVICE_PASSWD_PATH], user=SSH_USER, |
814 | + verbose=True) |
815 | + |
816 | +CA = [] |
817 | +def get_ca(user='keystone', group='keystone'): |
818 | + """ |
819 | + Initialize a new CA object if one hasn't already been loaded. |
820 | + This will create a new CA or load an existing one. |
821 | + """ |
822 | + if not CA: |
823 | + if not os.path.isdir(SSL_DIR): |
824 | + os.mkdir(SSL_DIR) |
825 | + d_name = '_'.join(SSL_CA_NAME.lower().split(' ')) |
826 | + ca = ssl.JujuCA(name=SSL_CA_NAME, user=user, group=group, |
827 | + ca_dir=os.path.join(SSL_DIR, |
828 | + '%s_intermediate_ca' % d_name), |
829 | + root_ca_dir=os.path.join(SSL_DIR, |
830 | + '%s_root_ca' % d_name)) |
831 | + # SSL_DIR is synchronized via all peers over unison+ssh, need |
832 | + # to ensure permissions. |
833 | + execute('chown -R %s.%s %s' % (user, group, SSL_DIR)) |
834 | + execute('chmod -R g+rwx %s' % SSL_DIR) |
835 | + CA.append(ca) |
836 | + return CA[0] |
837 | |
838 | === modified file 'revision' |
839 | --- revision 2013-03-06 21:50:30 +0000 |
840 | +++ revision 2013-03-08 21:36:24 +0000 |
841 | @@ -1,1 +1,1 @@ |
842 | -198 |
843 | +211 |
Also:
- This allows HTTPS to be turned on/off at will for the entire catalog via the config setting in keystone, if all services are using KS-managed SSL certs/keys.
- I should not that these changes do not yet take care of HTTPS for the actual keystone endpoint (OS_AUTH_URL). Only for the corresponding services in the catalog. If the approach proposed here works for other services, it should be easy to port to the keystone charm and have it manage its own endpoint similarly.
- After enabling HTTPs, many client tools will fail SSL verification and fail to interact with the API servers. Clients need to add the CA cert to their local system, eg:
$ curl http:// $NOVA_CC_ HOST/keystone_ juju_ca_ cert.crt | sudo tee /usr/local/ share/ca- certificates/ ks.crt && sudo update- ca-certificates