Merge lp:~hazmat/charms/precise/mysql/ssl-everywhere into lp:charms/mysql
- Precise Pangolin (12.04)
- ssl-everywhere
- Merge into trunk
Status: | Superseded |
---|---|
Proposed branch: | lp:~hazmat/charms/precise/mysql/ssl-everywhere |
Merge into: | lp:charms/mysql |
Diff against target: |
824 lines (+481/-135) 8 files modified
config.yaml (+18/-0) hooks/common.py (+49/-11) hooks/config-changed (+37/-6) hooks/db-relation-joined (+32/-26) hooks/shared_db_relations.py (+88/-91) hooks/slave-relation-changed (+3/-0) hooks/sslca.py (+253/-0) revision (+1/-1) |
To merge this branch: | bzr merge lp:~hazmat/charms/precise/mysql/ssl-everywhere |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
charmers | Pending | ||
Review via email: mp+207904@code.launchpad.net |
This proposal has been superseded by a proposal from 2014-03-04.
Commit message
Description of the change
SSL Support for MySQL
Adds a few additional configuration options for SSL.
'ssl' is the primary one, it can be 'off', 'on', or 'only'.
If its off, no changes. If its 'on' then ssl support is
enabled and ssl_ca cert is sent to clients along the relation.
If its only, then all clients must connect using ssl.
The server cert/key and ca cert can be provided via config. If
they are not, the service will act as its own ca, and also
in only mode will also generate client certs and pass them
along the relation (the client use of those certs is not
required atm).
Kapil Thangavelu (hazmat) wrote : | # |
Marco Ceppi (marcoceppi) wrote : | # |
I have some concerns about the way this affects the mysql interface
below. Overall, this is a great change and will bring security to
communication within a deployed environment, I'm open to discussion
about the points below - I simply want to make sure the charm stays as
compatible as possible as it grows.
https:/
File config.yaml (right):
https:/
config.yaml:38: ssl_key:
I don't like that these are _'d while the majority of the configuration
options for MySQL are hypenated (with the exception of vip_iface, and
vip_cidr which I would have rejected had I maintained the charm at the
time). If there isn't a hard requirement for these to be _ then I'd
prefer if they followed the convention of the majority of the charm's
config.
https:/
File hooks/db-
https:/
hooks/db-
With this, you're now overloading the mysql interface to the point
where, if a user deploys the MySQL charm, configures it for SSL, then
joins to a charm that doesn't know how to speak to MySQL on SSL with on
and off, no change (I assume), with ssl=only that charm just won't work.
Given the nature of this change, it seem more apt that this should be
implemented as a new interface, maybe mysql-ssl - so when you connect on
that interface and no ssl cert/ca/key are provided MySQL generates one,
otherwise uses the values provided by the configuration. In doing so
services can expose their compat with MySQL over SSL without having
cases that charms will break if MySQL is configured in a manner that's
too restrictive.
Kapil Thangavelu (hazmat) wrote : | # |
i disagree, i think that makes for a lousy experience to make a separate
interface, instead of adding ssl to one place now you have to reconfigure
your entire infrastructure to add it later (breaking and readding
relations, etc). Imagine trying to upgrade a production system to take
advantage of ssl.. instead of ugprade charm and config ssl, you now have to
break your entire production system. Its only the config ssl only that
forces the client to ssl. the ssl_ key names are chosen for consistency
with ssl support through the charm ecosystem, there are several others
already using these keys.
-k
On Thu, Feb 27, 2014 at 9:24 AM, Marco Ceppi <email address hidden> wrote:
> I have some concerns about the way this affects the mysql interface
> below. Overall, this is a great change and will bring security to
> communication within a deployed environment, I'm open to discussion
> about the points below - I simply want to make sure the charm stays as
> compatible as possible as it grows.
>
>
> https:/
> File config.yaml (right):
>
> https:/
> config.yaml:38: ssl_key:
> I don't like that these are _'d while the majority of the configuration
> options for MySQL are hypenated (with the exception of vip_iface, and
> vip_cidr which I would have rejected had I maintained the charm at the
> time). If there isn't a hard requirement for these to be _ then I'd
> prefer if they followed the convention of the majority of the charm's
> config.
>
> https:/
> File hooks/db-
>
>
> https:/
> hooks/db-
> With this, you're now overloading the mysql interface to the point
> where, if a user deploys the MySQL charm, configures it for SSL, then
> joins to a charm that doesn't know how to speak to MySQL on SSL with on
> and off, no change (I assume), with ssl=only that charm just won't work.
>
> Given the nature of this change, it seem more apt that this should be
> implemented as a new interface, maybe mysql-ssl - so when you connect on
> that interface and no ssl cert/ca/key are provided MySQL generates one,
> otherwise uses the values provided by the configuration. In doing so
> services can expose their compat with MySQL over SSL without having
> cases that charms will break if MySQL is configured in a manner that's
> too restrictive.
>
> https:/
>
> --
>
> https:/
> You are the owner of lp:~hazmat/charms/precise/mysql/ssl-everywhere.
>
Kapil Thangavelu (hazmat) wrote : | # |
plus with that logic, your talking about 3 extra interfaces to mirror the
three client interfaces we have now on mysql. that seems silly.
On Thu, Feb 27, 2014 at 9:30 AM, Kapil Thangavelu <
<email address hidden>> wrote:
> i disagree, i think that makes for a lousy experience to make a separate
> interface, instead of adding ssl to one place now you have to reconfigure
> your entire infrastructure to add it later (breaking and readding
> relations, etc). Imagine trying to upgrade a production system to take
> advantage of ssl.. instead of ugprade charm and config ssl, you now have to
> break your entire production system. Its only the config ssl only that
> forces the client to ssl. the ssl_ key names are chosen for consistency
> with ssl support through the charm ecosystem, there are several others
> already using these keys.
>
> -k
>
>
>
> On Thu, Feb 27, 2014 at 9:24 AM, Marco Ceppi <email address hidden> wrote:
>
>> I have some concerns about the way this affects the mysql interface
>> below. Overall, this is a great change and will bring security to
>> communication within a deployed environment, I'm open to discussion
>> about the points below - I simply want to make sure the charm stays as
>> compatible as possible as it grows.
>>
>>
>> https:/
>> File config.yaml (right):
>>
>> https:/
>> config.yaml:38: ssl_key:
>> I don't like that these are _'d while the majority of the configuration
>> options for MySQL are hypenated (with the exception of vip_iface, and
>> vip_cidr which I would have rejected had I maintained the charm at the
>> time). If there isn't a hard requirement for these to be _ then I'd
>> prefer if they followed the convention of the majority of the charm's
>> config.
>>
>> https:/
>> File hooks/db-
>>
>>
>> https:/
>> hooks/db-
>> cmd.append("%s=%s" % (k, v))
>> With this, you're now overloading the mysql interface to the point
>> where, if a user deploys the MySQL charm, configures it for SSL, then
>> joins to a charm that doesn't know how to speak to MySQL on SSL with on
>> and off, no change (I assume), with ssl=only that charm just won't work.
>>
>> Given the nature of this change, it seem more apt that this should be
>> implemented as a new interface, maybe mysql-ssl - so when you connect on
>> that interface and no ssl cert/ca/key are provided MySQL generates one,
>> otherwise uses the values provided by the configuration. In doing so
>> services can expose their compat with MySQL over SSL without having
>> cases that charms will break if MySQL is configured in a manner that's
>> too restrictive.
>>
>> https:/
>>
>> --
>>
>> https:/
>> You are the owner of lp:~hazmat/charms/precise/mysql/ssl-everywhere....
Kapil Thangavelu (hazmat) wrote : | # |
fwiw, i'm refactoring up this branch a bit to use charm-helpers and to cleanly re-configure existing relations when the setting changes at runtime. the core feature functionality is the same though, so the interface extension discussion remains.
Unmerged revisions
- 118. By Kapil Thangavelu
-
external ca and cert support
- 117. By Kapil Thangavelu
-
mysql is picky about cert gen, accomodate
- 116. By Kapil Thangavelu
-
ftest fixes
- 115. By Kapil Thangavelu
-
off is yaml magic for bool, quote appropriately
- 114. By Kapil Thangavelu
-
ssl support
Preview Diff
1 | === modified file 'config.yaml' |
2 | --- config.yaml 2014-02-04 21:48:28 +0000 |
3 | +++ config.yaml 2014-02-24 11:48:01 +0000 |
4 | @@ -31,6 +31,24 @@ |
5 | default: 'MIXED' |
6 | type: string |
7 | description: If binlogging is enabled, this is the format that will be used. Ignored when tuning-level == fast. |
8 | + ssl: |
9 | + default: 'off' |
10 | + type: string |
11 | + description: Enable SSL connections on mysql, valid values are 'off', 'on', and 'only' |
12 | + ssl_key: |
13 | + type: string |
14 | + description: private unencrypted key in PEM format (starts "-----BEGIN RSA PRIVATE KEY-----") |
15 | + default: "" |
16 | + ssl_cert: |
17 | + type: string |
18 | + description: X.509 certificate in PEM format (starts "-----BEGIN CERTIFICATE-----") |
19 | + default: "" |
20 | + ssl_ca: |
21 | + type: string |
22 | + description: | |
23 | + Certificate authority cert that signed pem. Optional if the ssl_cert is signed by a ca |
24 | + recognized by the os. |
25 | + default: "" |
26 | vip: |
27 | type: string |
28 | default: '' |
29 | |
30 | === modified file 'hooks/common.py' |
31 | --- hooks/common.py 2013-02-12 17:54:29 +0000 |
32 | +++ hooks/common.py 2014-02-24 11:48:01 +0000 |
33 | @@ -1,15 +1,51 @@ |
34 | # vim: syntax=python |
35 | |
36 | +import base64 |
37 | import os |
38 | +import json |
39 | import sys |
40 | import MySQLdb |
41 | +import sslca |
42 | import subprocess |
43 | import uuid |
44 | |
45 | + |
46 | def get_service_user_file(service): |
47 | return '/var/lib/mysql/%s.service_user2' % service |
48 | |
49 | |
50 | +def get_service_ca(): |
51 | + service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0] |
52 | + ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca') |
53 | + ca = sslca.ServiceCA(service_name, ca_path, sslca.MYSQL_CERT) |
54 | + ca.init() |
55 | + return ca |
56 | + |
57 | + |
58 | +def is_ext_ssl_ca(): |
59 | + ssl_key = configs.get('ssl_key') |
60 | + ssl_cert = configs.get('ssl_cert') |
61 | + return all((ssl_key, ssl_cert)) |
62 | + |
63 | + |
64 | +def configure_client_ssl(): |
65 | + if configs.get('ssl', '') not in ('only', 'on'): |
66 | + return dict() |
67 | + if is_ext_ssl_ca(): |
68 | + if configs.get('ssl_ca'): |
69 | + return dict(ssl_ca=base64.b64encode(configs['ssl_ca'])) |
70 | + return |
71 | + # self-managed-client-certs |
72 | + ca = get_service_ca() |
73 | + ca_bundle = ca.get_ca_bundle() |
74 | + service_cert, service_key = ca.get_or_create_cert( |
75 | + change_unit.split('/', 1)[0]) |
76 | + return dict( |
77 | + ssl_ca=base64.b64encode(ca_bundle), |
78 | + ssl_key=base64.b64encode(service_key), |
79 | + ssl_cert=base64.b64encode(service_cert)) |
80 | + |
81 | + |
82 | def get_service_user(service): |
83 | if service == '': |
84 | return (None, None) |
85 | @@ -17,7 +53,8 @@ |
86 | if os.path.exists(sfile): |
87 | with open(sfile, 'r') as f: |
88 | return (f.readline().strip(), f.readline().strip()) |
89 | - (suser, service_password) = subprocess.check_output(['pwgen', '-N 2', '15']).strip().split("\n") |
90 | + (suser, service_password) = subprocess.check_output( |
91 | + ['pwgen', '-N 2', '15']).strip().split("\n") |
92 | with open(sfile, 'w') as f: |
93 | f.write("%s\n" % suser) |
94 | f.write("%s\n" % service_password) |
95 | @@ -55,6 +92,8 @@ |
96 | slave = os.path.exists('/var/lib/juju/i.am.a.slave') |
97 | broken_path = '/var/lib/juju/%s.mysql.broken' % database_name |
98 | broken = os.path.exists(broken_path) |
99 | +configs = json.loads(subprocess.check_output(['config-get', '--format=json'])) |
100 | + |
101 | |
102 | def get_db_cursor(): |
103 | # Connect to mysql |
104 | @@ -95,15 +134,14 @@ |
105 | return "GRANT ALL PRIVILEGES ON `{}`".format(db_name) in grants |
106 | |
107 | |
108 | -def create_grant(db_name, db_user, |
109 | - remote_ip, password): |
110 | +def create_grant(db_name, db_user, remote_ip, password): |
111 | cursor = get_db_cursor() |
112 | + grant = "GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' IDENTIFIED BY '{}'" |
113 | + # http://dev.mysql.com/doc/refman/5.5/en/grant.html |
114 | + if configs.get('ssl') == 'only': |
115 | + grant += ' REQUIRE SSL' |
116 | try: |
117 | - cursor.execute("GRANT ALL PRIVILEGES ON {}.* TO '{}'@'{}' "\ |
118 | - "IDENTIFIED BY '{}'".format(db_name, |
119 | - db_user, |
120 | - remote_ip, |
121 | - password)) |
122 | + cursor.execute(grant.format(db_name, db_user, remote_ip, password)) |
123 | finally: |
124 | cursor.close() |
125 | |
126 | @@ -112,8 +150,8 @@ |
127 | remote_ip): |
128 | cursor = get_db_cursor() |
129 | try: |
130 | - cursor.execute("DROP FROM mysql.user WHERE user='{}' "\ |
131 | - "AND HOST='{}'".format(db_user, |
132 | - remote_ip)) |
133 | + cursor.execute( |
134 | + "DROP FROM mysql.user WHERE user='{}' AND HOST='{}'".format( |
135 | + db_user, remote_ip)) |
136 | finally: |
137 | cursor.close() |
138 | |
139 | === modified file 'hooks/config-changed' |
140 | --- hooks/config-changed 2014-02-15 21:37:45 +0000 |
141 | +++ hooks/config-changed 2014-02-24 11:48:01 +0000 |
142 | @@ -10,6 +10,8 @@ |
143 | import platform |
144 | from string import upper |
145 | |
146 | +from common import get_service_ca, configs |
147 | + |
148 | num_re = re.compile('^[0-9]+$') |
149 | |
150 | # There should be a library for this |
151 | @@ -48,7 +50,6 @@ |
152 | if IS_32BIT_SYSTEM: |
153 | check_call(['juju-log','-l','INFO','32bit system restrictions in play']) |
154 | |
155 | -configs=json.loads(check_output(['config-get','--format=json'])) |
156 | |
157 | def get_memtotal(): |
158 | with open('/proc/meminfo') as meminfo_file: |
159 | @@ -153,6 +154,36 @@ |
160 | else: |
161 | configs['max-connections'] = 'max_connections = %s' % configs['max-connections'] |
162 | |
163 | + |
164 | +if configs.get('ssl', 'off').strip() in ('on', 'only'): |
165 | + service_name = os.environ.get('JUJU_UNIT_NAME').split('/')[0] |
166 | + ssl_cert, ssl_key, ssl_ca = ( |
167 | + configs.get('ssl_cert'), |
168 | + configs.get('ssl_key'), |
169 | + configs.get('ssl_ca')) |
170 | + |
171 | + if any((ssl_cert, ssl_key, ssl_ca)): |
172 | + if not all((ssl_cert, ssl_key)): |
173 | + print "ERROR", "SSL Key and cert are both required if either specified" |
174 | + sys.exit(1) |
175 | + else: |
176 | + ca = get_service_ca() |
177 | + ssl_cert, ssl_key = ca.create_certificate(service_name) |
178 | + ssl_ca = ca.get_ca_bundle() |
179 | + |
180 | + with open('/etc/mysql/cacert.pem', 'w') as fh: |
181 | + fh.write(ssl_ca) |
182 | + with open('/etc/mysql/server-cert.pem', 'w') as fh: |
183 | + fh.write(ssl_cert) |
184 | + with open('/etc/mysql/server-key.pem', 'w') as fh: |
185 | + fh.write(ssl_key) |
186 | + if ssl_ca: # In case a user providers key/cert but no ca cause its official. |
187 | + configs['ssl_ca_marker'] = '' |
188 | + if ssl_key: |
189 | + configs['ssl_cert_marker'] = '' |
190 | +else: |
191 | + configs['ssl_cert_marker'] = configs['ssl_ca_marker'] = "#" |
192 | + |
193 | template=""" |
194 | ###################################### |
195 | # |
196 | @@ -171,7 +202,7 @@ |
197 | # You can copy this to one of: |
198 | # - "/etc/mysql/my.cnf" to set global options, |
199 | # - "~/.my.cnf" to set user-specific options. |
200 | -# |
201 | +# |
202 | # One can use all long options that the program supports. |
203 | # Run program with --help to get a list of available options and with |
204 | # --print-defaults to see which it would actually understand and use. |
205 | @@ -277,9 +308,9 @@ |
206 | # |
207 | # For generating SSL certificates I recommend the OpenSSL GUI "tinyca". |
208 | # |
209 | -# ssl-ca=/etc/mysql/cacert.pem |
210 | -# ssl-cert=/etc/mysql/server-cert.pem |
211 | -# ssl-key=/etc/mysql/server-key.pem |
212 | +%(ssl_ca_marker)s ssl-ca=/etc/mysql/cacert.pem |
213 | +%(ssl_cert_marker)s ssl-cert=/etc/mysql/server-cert.pem |
214 | +%(ssl_cert_marker)s ssl-key=/etc/mysql/server-key.pem |
215 | |
216 | |
217 | |
218 | @@ -326,7 +357,7 @@ |
219 | |
220 | need_restart = False |
221 | for target,content in targets.iteritems(): |
222 | - tdir = os.path.dirname(target) |
223 | + tdir = os.path.dirname(target) |
224 | if len(content) == 0 and os.path.exists(target): |
225 | os.unlink(target) |
226 | need_restart = True |
227 | |
228 | === modified file 'hooks/db-relation-joined' |
229 | --- hooks/db-relation-joined 2012-11-02 06:41:12 +0000 |
230 | +++ hooks/db-relation-joined 2014-02-24 11:48:01 +0000 |
231 | @@ -1,22 +1,27 @@ |
232 | #!/usr/bin/env python |
233 | |
234 | -from common import * |
235 | +from common import ( |
236 | + change_unit, configs, get_db_cursor, get_service_ca, is_ext_ssl_ca, |
237 | + configure_client_ssl) |
238 | |
239 | -import urllib |
240 | -import subprocess |
241 | +import base64 |
242 | +import random |
243 | import os |
244 | +import pickle |
245 | import sys |
246 | import string |
247 | -import random |
248 | -import pickle |
249 | +import subprocess |
250 | +import urllib |
251 | + |
252 | |
253 | cursor = get_db_cursor() |
254 | |
255 | admin = os.path.basename(sys.argv[0]) == 'db-admin-relation-joined' |
256 | |
257 | def runsql(sql): |
258 | - print "[%s]" % sql |
259 | - cursor.execute(sql) |
260 | + print "[%s]" % sql |
261 | + cursor.execute(sql) |
262 | + |
263 | |
264 | runsql( |
265 | "grant replication client on *.* to `%s` identified by '%s'" % ( |
266 | @@ -27,33 +32,33 @@ |
267 | try: |
268 | runsql( |
269 | "revoke all on `%s`.* from `%s`" % ( |
270 | - database_name, |
271 | + database_name, |
272 | user)) |
273 | except: |
274 | print "revoke failed, ignoring error" |
275 | runsql( |
276 | "grant select on `%s`.* to `%s`" % ( |
277 | - database_name, |
278 | + database_name, |
279 | user)) |
280 | else: |
281 | if admin: |
282 | - runsql( |
283 | - "grant all privileges on *.* to `%s` identified by '%s'" % ( |
284 | - user, |
285 | - service_password)) |
286 | + grant = "grant all privileges on *.* to `%s` identified by '%s'" |
287 | + if configs.get('ssl', '') == 'only': |
288 | + grant += " REQUIRE SSL" |
289 | + runsql( grant % (user, service_password)) |
290 | else: |
291 | - runsql( |
292 | - "grant all on `%s`.* to `%s` identified by '%s'" % ( |
293 | - database_name, |
294 | - user, |
295 | - service_password)) |
296 | + grant = "grant all on `%s`.* to `%s` identified by '%s'" |
297 | + if configs.get('ssl', '') == 'only': |
298 | + grant += " REQUIRE SSL" |
299 | + runsql( grant % (database_name, user, service_password)) |
300 | |
301 | hostname = subprocess.check_output(['unit-get','private-address']).strip() |
302 | |
303 | + |
304 | print str(["relation-set", |
305 | "database=%s" % database_name, |
306 | "user=%s" % user, |
307 | - "password=%s" % service_password, |
308 | +# "password=%s" % service_password, |
309 | 'host=%s' % hostname, |
310 | 'slave=%s' % slave]) |
311 | |
312 | @@ -78,10 +83,11 @@ |
313 | cursor.close() |
314 | |
315 | # Store new values in relation settings. |
316 | -subprocess.call( |
317 | - ["relation-set", |
318 | - "database=%s" % database_name, |
319 | - "user=%s" % user, |
320 | - "password=%s" % service_password, |
321 | - 'host=%s' % hostname, |
322 | - 'slave=%s' % slave,]) |
323 | +cmd = ["relation-set", "database=%s" % database_name, 'host=%s' % hostname, |
324 | + "user=%s" % user, "password=%s" % service_password, |
325 | + 'slave=%s' % slave,]) |
326 | + |
327 | +for k, v in configure_client_ssl().items(): |
328 | + cmd.append("%s=%s" % (k, v)) |
329 | + |
330 | +subprocess.check_call(cmd) |
331 | |
332 | === modified file 'hooks/shared_db_relations.py' |
333 | --- hooks/shared_db_relations.py 2013-07-10 16:32:52 +0000 |
334 | +++ hooks/shared_db_relations.py 2014-02-24 11:48:01 +0000 |
335 | @@ -11,15 +11,22 @@ |
336 | from common import ( |
337 | database_exists, |
338 | create_database, |
339 | + configure_client_ssl, |
340 | grant_exists, |
341 | - create_grant |
342 | - ) |
343 | -import subprocess |
344 | + create_grant, |
345 | + configs, |
346 | + change_unit, |
347 | + get_service_ca) |
348 | + |
349 | +import base64 |
350 | +import functools |
351 | import json |
352 | -import socket |
353 | -import os |
354 | import lib.utils as utils |
355 | import lib.cluster_utils as cluster |
356 | +import os |
357 | +import subprocess |
358 | +import socket |
359 | + |
360 | |
361 | LEADER_RES = 'res_mysql_vip' |
362 | |
363 | @@ -29,44 +36,36 @@ |
364 | |
365 | |
366 | def relation_get(): |
367 | - return json.loads(subprocess.check_output( |
368 | - ['relation-get', |
369 | - '--format', |
370 | - 'json'] |
371 | - ) |
372 | - ) |
373 | + return json.loads( |
374 | + subprocess.check_output(['relation-get', '--format', 'json'])) |
375 | + |
376 | + |
377 | +local_hostname = utils.unit_get('private-address') |
378 | + |
379 | + |
380 | +def configure_db(hostname, database, username): |
381 | + passwd_file = "/var/lib/mysql/mysql-{}.passwd".format(username) |
382 | + if hostname != local_hostname: |
383 | + remote_ip = socket.gethostbyname(hostname) |
384 | + else: |
385 | + remote_ip = '127.0.0.1' |
386 | + |
387 | + if not os.path.exists(passwd_file): |
388 | + password = pwgen() |
389 | + with open(passwd_file, 'w') as pfile: |
390 | + pfile.write(password) |
391 | + else: |
392 | + with open(passwd_file) as pfile: |
393 | + password = pfile.read().strip() |
394 | + |
395 | + if not database_exists(database): |
396 | + create_database(database) |
397 | + if not grant_exists(database, username, remote_ip): |
398 | + create_grant(database, username, remote_ip, password) |
399 | + return password |
400 | |
401 | |
402 | def shared_db_changed(): |
403 | - |
404 | - def configure_db(hostname, |
405 | - database, |
406 | - username): |
407 | - passwd_file = "/var/lib/mysql/mysql-{}.passwd"\ |
408 | - .format(username) |
409 | - if hostname != local_hostname: |
410 | - remote_ip = socket.gethostbyname(hostname) |
411 | - else: |
412 | - remote_ip = '127.0.0.1' |
413 | - |
414 | - if not os.path.exists(passwd_file): |
415 | - password = pwgen() |
416 | - with open(passwd_file, 'w') as pfile: |
417 | - pfile.write(password) |
418 | - else: |
419 | - with open(passwd_file) as pfile: |
420 | - password = pfile.read().strip() |
421 | - |
422 | - if not database_exists(database): |
423 | - create_database(database) |
424 | - if not grant_exists(database, |
425 | - username, |
426 | - remote_ip): |
427 | - create_grant(database, |
428 | - username, |
429 | - remote_ip, password) |
430 | - return password |
431 | - |
432 | if not cluster.eligible_leader(LEADER_RES): |
433 | utils.juju_log('INFO', |
434 | 'MySQL service is peered, bailing shared-db relation' |
435 | @@ -74,64 +73,62 @@ |
436 | return |
437 | |
438 | settings = relation_get() |
439 | - local_hostname = utils.unit_get('private-address') |
440 | - singleset = set([ |
441 | - 'database', |
442 | - 'username', |
443 | - 'hostname' |
444 | - ]) |
445 | + singleset = set(['database', 'username', 'hostname']) |
446 | + |
447 | + if configs.get('ssl', '').strip() in ('on', 'only'): |
448 | + client_ssl = configure_client_ssl() |
449 | + relation_set = functools.partial(utils.relation_set, **client_ssl) |
450 | + else: |
451 | + relation_set = utils.relation_set |
452 | |
453 | if singleset.issubset(settings): |
454 | # Process a single database configuration |
455 | - password = configure_db(settings['hostname'], |
456 | - settings['database'], |
457 | - settings['username']) |
458 | + password = configure_db( |
459 | + settings['hostname'], settings['database'], settings['username']) |
460 | if not cluster.is_clustered(): |
461 | - utils.relation_set(db_host=local_hostname, |
462 | - password=password) |
463 | + relation_set(db_host=local_hostname, password=password) |
464 | else: |
465 | - utils.relation_set(db_host=utils.config_get("vip"), |
466 | - password=password) |
467 | + relation_set(db_host=utils.config_get("vip"), password=password) |
468 | + return |
469 | |
470 | + # Process multiple database setup requests. |
471 | + # from incoming relation data: |
472 | + # nova_database=xxx nova_username=xxx nova_hostname=xxx |
473 | + # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx |
474 | + # create |
475 | + #{ |
476 | + # "nova": { |
477 | + # "username": xxx, |
478 | + # "database": xxx, |
479 | + # "hostname": xxx |
480 | + # }, |
481 | + # "quantum": { |
482 | + # "username": xxx, |
483 | + # "database": xxx, |
484 | + # "hostname": xxx |
485 | + # } |
486 | + #} |
487 | + # |
488 | + databases = {} |
489 | + for k, v in settings.iteritems(): |
490 | + db = k.split('_')[0] |
491 | + x = '_'.join(k.split('_')[1:]) |
492 | + if db not in databases: |
493 | + databases[db] = {} |
494 | + databases[db][x] = v |
495 | + return_data = {} |
496 | + for db in databases: |
497 | + if singleset.issubset(databases[db]): |
498 | + return_data['_'.join([db, 'password'])] = \ |
499 | + configure_db(databases[db]['hostname'], |
500 | + databases[db]['database'], |
501 | + databases[db]['username']) |
502 | + if len(return_data) > 0: |
503 | + relation_set(**return_data) |
504 | + if not cluster.is_clustered(): |
505 | + relation_set(db_host=local_hostname) |
506 | else: |
507 | - # Process multiple database setup requests. |
508 | - # from incoming relation data: |
509 | - # nova_database=xxx nova_username=xxx nova_hostname=xxx |
510 | - # quantum_database=xxx quantum_username=xxx quantum_hostname=xxx |
511 | - # create |
512 | - #{ |
513 | - # "nova": { |
514 | - # "username": xxx, |
515 | - # "database": xxx, |
516 | - # "hostname": xxx |
517 | - # }, |
518 | - # "quantum": { |
519 | - # "username": xxx, |
520 | - # "database": xxx, |
521 | - # "hostname": xxx |
522 | - # } |
523 | - #} |
524 | - # |
525 | - databases = {} |
526 | - for k, v in settings.iteritems(): |
527 | - db = k.split('_')[0] |
528 | - x = '_'.join(k.split('_')[1:]) |
529 | - if db not in databases: |
530 | - databases[db] = {} |
531 | - databases[db][x] = v |
532 | - return_data = {} |
533 | - for db in databases: |
534 | - if singleset.issubset(databases[db]): |
535 | - return_data['_'.join([db, 'password'])] = \ |
536 | - configure_db(databases[db]['hostname'], |
537 | - databases[db]['database'], |
538 | - databases[db]['username']) |
539 | - if len(return_data) > 0: |
540 | - utils.relation_set(**return_data) |
541 | - if not cluster.is_clustered(): |
542 | - utils.relation_set(db_host=local_hostname) |
543 | - else: |
544 | - utils.relation_set(db_host=utils.config_get("vip")) |
545 | + relation_set(db_host=utils.config_get("vip")) |
546 | |
547 | hooks = { |
548 | "shared-db-relation-changed": shared_db_changed |
549 | |
550 | === modified file 'hooks/slave-relation-changed' |
551 | --- hooks/slave-relation-changed 2013-02-12 17:54:29 +0000 |
552 | +++ hooks/slave-relation-changed 2014-02-24 11:48:01 +0000 |
553 | @@ -87,3 +87,6 @@ |
554 | mysql $ROOTARGS -e "START SLAVE" |
555 | mysql $ROOTARGS -e "SHOW SLAVE STATUS" |
556 | touch /var/lib/juju/i.am.a.slave |
557 | + |
558 | + |
559 | +configs=json.loads(check_output(['config-get','--format=json'])) |
560 | |
561 | === added file 'hooks/sslca.py' |
562 | --- hooks/sslca.py 1970-01-01 00:00:00 +0000 |
563 | +++ hooks/sslca.py 2014-02-24 11:48:01 +0000 |
564 | @@ -0,0 +1,253 @@ |
565 | +import logging |
566 | +import datetime |
567 | +import os |
568 | +from os.path import join as path_join |
569 | +from os.path import exists |
570 | +import subprocess |
571 | + |
572 | + |
573 | +log = logging.getLogger("service_ca") |
574 | + |
575 | +logging.basicConfig(level=logging.DEBUG) |
576 | + |
577 | +STD_CERT = "standard" |
578 | + |
579 | +# Mysql server is fairly picky about cert creation |
580 | +# and types, spec its creation separately for now. |
581 | +MYSQL_CERT = "mysql" |
582 | + |
583 | + |
584 | +class ServiceCA(object): |
585 | + |
586 | + default_expiry = str(365 * 2) |
587 | + default_ca_expiry = str(365 * 6) |
588 | + |
589 | + def __init__(self, name, ca_dir, cert_type=STD_CERT): |
590 | + self.name = name |
591 | + self.ca_dir = ca_dir |
592 | + self.cert_type = cert_type |
593 | + |
594 | + def init(self): |
595 | + log.debug("initializing service ca") |
596 | + if not exists(self.ca_dir): |
597 | + self._init_ca_dir(self.ca_dir) |
598 | + self._init_ca() |
599 | + |
600 | + @property |
601 | + def ca_key(self): |
602 | + return path_join(self.ca_dir, 'private', 'cacert.key') |
603 | + |
604 | + @property |
605 | + def ca_cert(self): |
606 | + return path_join(self.ca_dir, 'cacert.pem') |
607 | + |
608 | + @property |
609 | + def ca_conf(self): |
610 | + return path_join(self.ca_dir, 'ca.cnf') |
611 | + |
612 | + @property |
613 | + def signing_conf(self): |
614 | + return path_join(self.ca_dir, 'signing.cnf') |
615 | + |
616 | + def _init_ca_dir(self, ca_dir): |
617 | + os.mkdir(ca_dir) |
618 | + for i in ['certs', 'crl', 'newcerts', 'private']: |
619 | + sd = path_join(ca_dir, i) |
620 | + if not exists(sd): |
621 | + os.mkdir(sd) |
622 | + |
623 | + if not exists(path_join(ca_dir, 'serial')): |
624 | + with open(path_join(ca_dir, 'serial'), 'wb') as fh: |
625 | + fh.write('02\n') |
626 | + |
627 | + if not exists(path_join(ca_dir, 'index.txt')): |
628 | + with open(path_join(ca_dir, 'index.txt'), 'wb') as fh: |
629 | + fh.write('') |
630 | + |
631 | + def _init_ca(self): |
632 | + """Generate the root ca's cert and key. |
633 | + """ |
634 | + if not exists(path_join(self.ca_dir, 'ca.cnf')): |
635 | + with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh: |
636 | + fh.write( |
637 | + CA_CONF_TEMPLATE % (self.get_conf_variables())) |
638 | + |
639 | + if not exists(path_join(self.ca_dir, 'signing.cnf')): |
640 | + with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh: |
641 | + fh.write( |
642 | + SIGNING_CONF_TEMPLATE % (self.get_conf_variables())) |
643 | + |
644 | + if exists(self.ca_cert) or exists(self.ca_key): |
645 | + raise RuntimeError("Initialized called when CA already exists") |
646 | + cmd = ['openssl', 'req', '-config', self.ca_conf, |
647 | + '-x509', '-nodes', '-newkey', 'rsa', |
648 | + '-days', self.default_ca_expiry, |
649 | + '-keyout', self.ca_key, '-out', self.ca_cert, |
650 | + '-outform', 'PEM'] |
651 | + output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) |
652 | + log.debug("CA Init:\n %s", output) |
653 | + |
654 | + def get_conf_variables(self): |
655 | + return dict( |
656 | + org_name="juju", |
657 | + org_unit_name="%s service" % self.name, |
658 | + common_name=self.name, |
659 | + ca_dir=self.ca_dir) |
660 | + |
661 | + def get_or_create_cert(self, common_name): |
662 | + if common_name in self: |
663 | + return self.get_certificate(common_name) |
664 | + return self.create_certificate(common_name) |
665 | + |
666 | + def create_certificate(self, common_name): |
667 | + if common_name in self: |
668 | + return self.get_certificate(common_name) |
669 | + key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name) |
670 | + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) |
671 | + csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name) |
672 | + self._create_certificate(common_name, key_p, csr_p, crt_p) |
673 | + return self.get_certificate(common_name) |
674 | + |
675 | + def get_certificate(self, common_name): |
676 | + if not common_name in self: |
677 | + raise ValueError("No certificate for %s" % common_name) |
678 | + key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name) |
679 | + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) |
680 | + with open(crt_p) as fh: |
681 | + crt = fh.read() |
682 | + with open(key_p) as fh: |
683 | + key = fh.read() |
684 | + # Required for mysql client/server not sure if it matters |
685 | + # to others. |
686 | + #key = key.replace('BEGIN PRIVATE', 'BEGIN RSA PRIVATE') |
687 | + #key = key.replace('END PRIVATE', 'END RSA PRIVATE') |
688 | + return crt, key |
689 | + |
690 | + def __contains__(self, common_name): |
691 | + crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name) |
692 | + return exists(crt_p) |
693 | + |
694 | + def _create_certificate(self, common_name, key_p, csr_p, crt_p): |
695 | + template_vars = self.get_conf_variables() |
696 | + template_vars['common_name'] = common_name |
697 | + subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % ( |
698 | + template_vars) |
699 | + |
700 | + log.debug("CA Create Cert %s", common_name) |
701 | + cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048', |
702 | + '-nodes', '-days', self.default_expiry, |
703 | + '-keyout', key_p, '-out', csr_p, '-subj', subj] |
704 | + subprocess.check_call(cmd) |
705 | + cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p] |
706 | + subprocess.check_call(cmd) |
707 | + |
708 | + log.debug("CA Sign Cert %s", common_name) |
709 | + if self.cert_type == MYSQL_CERT: |
710 | + cmd = ['openssl', 'x509', '-req', |
711 | + '-in', csr_p, '-days', self.default_expiry, |
712 | + '-CA', self.ca_cert, '-CAkey', self.ca_key, |
713 | + '-set_serial', '01', '-out', crt_p] |
714 | + else: |
715 | + cmd = ['openssl', 'ca', '-config', self.signing_conf, |
716 | + '-extensions', 'req_extensions', |
717 | + '-days', self.default_expiry, '-notext', |
718 | + '-in', csr_p, '-out', crt_p, '-subj', subj, '-batch'] |
719 | + log.debug("running %s", " ".join(cmd)) |
720 | + subprocess.check_call(cmd) |
721 | + |
722 | + def get_ca_bundle(self): |
723 | + with open(self.ca_cert) as fh: |
724 | + return fh.read() |
725 | + |
726 | + |
727 | +CA_CONF_TEMPLATE = """ |
728 | +[ ca ] |
729 | +default_ca = CA_default |
730 | + |
731 | +[ CA_default ] |
732 | +dir = %(ca_dir)s |
733 | +policy = policy_match |
734 | +database = $dir/index.txt |
735 | +serial = $dir/serial |
736 | +certs = $dir/certs |
737 | +crl_dir = $dir/crl |
738 | +new_certs_dir = $dir/newcerts |
739 | +certificate = $dir/cacert.pem |
740 | +private_key = $dir/private/cacert.key |
741 | +RANDFILE = $dir/private/.rand |
742 | +default_md = default |
743 | + |
744 | +[ req ] |
745 | +default_bits = 1024 |
746 | +default_md = sha1 |
747 | + |
748 | +prompt = no |
749 | +distinguished_name = ca_distinguished_name |
750 | + |
751 | +x509_extensions = ca_extensions |
752 | + |
753 | +[ ca_distinguished_name ] |
754 | +organizationName = %(org_name)s |
755 | +organizationalUnitName = %(org_unit_name)s Certificate Authority |
756 | + |
757 | + |
758 | +[ policy_match ] |
759 | +countryName = optional |
760 | +stateOrProvinceName = optional |
761 | +organizationName = match |
762 | +organizationalUnitName = optional |
763 | +commonName = supplied |
764 | + |
765 | +[ ca_extensions ] |
766 | +basicConstraints = critical,CA:true |
767 | +subjectKeyIdentifier = hash |
768 | +authorityKeyIdentifier = keyid:always, issuer |
769 | +keyUsage = cRLSign, keyCertSign |
770 | +""" |
771 | + |
772 | + |
773 | +SIGNING_CONF_TEMPLATE = """ |
774 | +[ ca ] |
775 | +default_ca = CA_default |
776 | + |
777 | +[ CA_default ] |
778 | +dir = %(ca_dir)s |
779 | +policy = policy_match |
780 | +database = $dir/index.txt |
781 | +serial = $dir/serial |
782 | +certs = $dir/certs |
783 | +crl_dir = $dir/crl |
784 | +new_certs_dir = $dir/newcerts |
785 | +certificate = $dir/cacert.pem |
786 | +private_key = $dir/private/cacert.key |
787 | +RANDFILE = $dir/private/.rand |
788 | +default_md = default |
789 | + |
790 | +[ req ] |
791 | +default_bits = 1024 |
792 | +default_md = sha1 |
793 | + |
794 | +prompt = no |
795 | +distinguished_name = req_distinguished_name |
796 | + |
797 | +x509_extensions = req_extensions |
798 | + |
799 | +[ req_distinguished_name ] |
800 | +organizationName = %(org_name)s |
801 | +organizationalUnitName = %(org_unit_name)s machine resources |
802 | +commonName = %(common_name)s |
803 | + |
804 | +[ policy_match ] |
805 | +countryName = optional |
806 | +stateOrProvinceName = optional |
807 | +organizationName = match |
808 | +organizationalUnitName = optional |
809 | +commonName = supplied |
810 | + |
811 | +[ req_extensions ] |
812 | +basicConstraints = CA:false |
813 | +subjectKeyIdentifier = hash |
814 | +authorityKeyIdentifier = keyid:always, issuer |
815 | +keyUsage = digitalSignature, keyEncipherment, keyAgreement |
816 | +extendedKeyUsage = serverAuth, clientAuth |
817 | +""" |
818 | |
819 | === modified file 'revision' |
820 | --- revision 2014-02-13 18:51:20 +0000 |
821 | +++ revision 2014-02-24 11:48:01 +0000 |
822 | @@ -1,1 +1,1 @@ |
823 | -311 |
824 | +321 |
Reviewers: mp+207904_ code.launchpad. net,
Message:
Please take a look.
Description:
SSL Support for MySQL
Adds a few additional configuration options for SSL.
'ssl' is the primary one, it can be 'off', 'on', or 'only'.
If its off, no changes. If its 'on' then ssl support is
enabled and ssl_ca cert is sent to clients along the relation.
If its only, then all clients must connect using ssl.
The server cert/key and ca cert can be provided via config. If
they are not, the service will act as its own ca, and also
in only mode will also generate client certs and pass them
along the relation (the client use of those certs is not
required atm).
https:/ /code.launchpad .net/~hazmat/ charms/ precise/ mysql/ssl- everywhere/ +merge/ 207904
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/68120043/
Affected files (+483, -135 lines): changed relation- joined db_relations. py relation- changed
A [revision details]
M config.yaml
M hooks/common.py
M hooks/config-
M hooks/db-
M hooks/shared_
M hooks/slave-
A hooks/sslca.py
M revision