Merge ssl-everywhere branch

This commit is contained in:
James Page 2014-03-05 12:13:33 +00:00
commit bd32a5adee
11 changed files with 625 additions and 87 deletions

View File

@ -1,8 +1,9 @@
destination: lib/charmhelpers
branch: lp:charm-helpers
branch: lp:~openstack-charmers/charm-helpers/ssl-everywhere
include:
- fetch
- core
- contrib.charmsupport
- contrib.openstack
- contrib.storage
- contrib.ssl

View File

@ -1,24 +1,42 @@
options:
ssl_enabled:
type: boolean
default: False
description: enable SSL
management_plugin:
type: boolean
default: False
description: enable the management plugin
# SSL Configuration options
ssl:
type: string
default: "off"
description: |
Enable SSL connections on rabbitmq, valid values are 'off', 'on', 'only'. If ssl_key,
ssl_cert, ssl_ca are provided then then those values will be used. Otherwise
the service will act as its own certificate authority and pass its ca cert to clients.
For HA or clustered rabbits ssl key/cert must be provided.
ssl_enabled:
type: boolean
default: False
description: |
(DEPRECATED see 'ssl' config option.) enable SSL
ssl_port:
type: int
default: 5671
description: SSL port
ssl_key:
type: string
description: private unencrypted key in PEM format (starts "-----BEGIN RSA PRIVATE KEY-----")
description: private unencrypted key in base64 PEM format (starts "-----BEGIN RSA PRIVATE KEY-----")
default: ""
ssl_cert:
type: string
description: X.509 certificate in PEM format (starts "-----BEGIN CERTIFICATE-----")
description: X.509 certificate in base64 PEM format (starts "-----BEGIN CERTIFICATE-----")
default: ""
ssl_ca:
type: string
description: |
Certificate authority cert that the cert. Optional if the ssl_cert is signed by a ca
recognized by the os. Format is base64 PEM (concatenated certs if needed).
default: ""
nagios_context:
default: "juju"
type: string
@ -87,6 +105,7 @@ options:
rbd pool has been created, changing this value will not have any
effect (although it can be changed in ceph by manually configuring
your ceph cluster).
<<<<<<< TREE
use-syslog:
type: boolean
default: False

View File

@ -163,8 +163,10 @@ def get_ceph_nodes():
hosts = []
for r_id in utils.relation_ids('ceph'):
for unit in utils.relation_list(r_id):
hosts.append(utils.relation_get('private-address',
unit=unit, rid=r_id))
ceph_public_addr = utils.relation_get(
'ceph_public_addr', unit=unit, rid=r_id) or \
utils.relation_get('private-address', unit=unit, rid=r_id)
hosts.append(ceph_public_addr)
return hosts

View File

@ -74,7 +74,11 @@ CLOUD_ARCHIVE_POCKETS = {
'grizzly/proposed': 'precise-proposed/grizzly',
'havana': 'precise-updates/havana',
'havana/updates': 'precise-updates/havana',
'havana/proposed': 'precise-proposed/havana'}
'havana/proposed': 'precise-proposed/havana',
'icehouse': 'precise-updates/icehouse',
'icehouse/updates': 'precise-updates/icehouse',
'icehouse/proposed': 'precise-proposed/icehouse',
}
def configure_source():
@ -249,17 +253,15 @@ def unit_get(attribute):
@cached
def config_get(attribute):
cmd = [
'config-get',
'--format',
'json']
out = subprocess.check_output(cmd).strip() # IGNORE:E1103
cfg = json.loads(out)
def config_get(scope=None):
"""Juju charm configuration"""
config_cmd_line = ['config-get']
if scope is not None:
config_cmd_line.append(scope)
config_cmd_line.append('--format=json')
try:
return cfg[attribute]
except KeyError:
return json.loads(subprocess.check_output(config_cmd_line))
except ValueError:
return None

View File

@ -241,22 +241,39 @@ def disable_plugin(plugin):
ssl_key_file = "/etc/rabbitmq/rabbit-server-privkey.pem"
ssl_cert_file = "/etc/rabbitmq/rabbit-server-cert.pem"
ssl_ca_file = "/etc/rabbitmq/rabbit-server-ca.pem"
def enable_ssl(ssl_key, ssl_cert, ssl_port):
with open(ssl_key_file, 'w') as key_file:
key_file.write(ssl_key)
utils.chmod(ssl_key_file, 0640)
utils.chown(ssl_key_file, "root", RABBIT_USER)
with open(ssl_cert_file, 'w') as cert_file:
cert_file.write(ssl_cert)
utils.chmod(ssl_cert_file, 0640)
utils.chown(ssl_cert_file, "root", RABBIT_USER)
def enable_ssl(ssl_key, ssl_cert, ssl_port,
ssl_ca=None, ssl_only=False, ssl_client=None):
uid = pwd.getpwnam("root").pw_uid
gid = grp.getgrnam("rabbitmq").gr_gid
for contents, path in (
(ssl_key, ssl_key_file),
(ssl_cert, ssl_cert_file),
(ssl_ca, ssl_ca_file)):
if not contents:
continue
with open(path, 'w') as fh:
fh.write(contents)
os.chmod(path, 0640)
os.chown(path, uid, gid)
data = {
"ssl_port": ssl_port,
"ssl_cert_file": ssl_cert_file,
"ssl_key_file": ssl_key_file,
"ssl_client": ssl_client,
"ssl_ca_file": "",
"ssl_only": ssl_only}
if ssl_ca:
data["ssl_ca_file"] = ssl_ca_file
with open(RABBITMQ_CONF, 'w') as rmq_conf:
rmq_conf.write(utils.render_template(os.path.basename(RABBITMQ_CONF),
{"ssl_port": ssl_port,
"ssl_cert_file": ssl_cert_file,
"ssl_key_file": ssl_key_file}))
rmq_conf.write(utils.render_template(
os.path.basename(RABBITMQ_CONF), data))
def execute(cmd, die=False, echo=False):

View File

@ -1,5 +1,5 @@
#!/usr/bin/python
import base64
import os
import shutil
import sys
@ -22,6 +22,7 @@ from charmhelpers.fetch import (
from charmhelpers.core import hookenv
from charmhelpers.core.host import rsync, mkdir, pwgen
from charmhelpers.contrib.charmsupport.nrpe import NRPE
from charmhelpers.contrib.ssl.service import ServiceCA
SERVICE_NAME = os.getenv('JUJU_UNIT_NAME').split('/')[0]
@ -65,18 +66,16 @@ def amqp_changed(relation_id=None, remote_unit=None):
relation_settings = {}
settings = hookenv.relation_get(rid=relation_id, unit=remote_unit)
singleset = set([
'username',
'vhost'])
singleset = set(['username', 'vhost'])
if singleset.issubset(settings):
if None in [settings['username'], settings['vhost']]:
utils.juju_log('INFO', 'amqp_changed(): Relation not ready.')
return
relation_settings['password'] = \
configure_amqp(username=settings['username'],
vhost=settings['vhost'])
relation_settings['password'] = configure_amqp(
username=settings['username'],
vhost=settings['vhost'])
else:
queues = {}
for k, v in settings.iteritems():
@ -85,12 +84,15 @@ def amqp_changed(relation_id=None, remote_unit=None):
if amqp not in queues:
queues[amqp] = {}
queues[amqp][x] = v
relation_settings = {}
for amqp in queues:
if singleset.issubset(queues[amqp]):
relation_settings['_'.join([amqp, 'password'])] = \
configure_amqp(queues[amqp]['username'],
queues[amqp]['vhost'])
relation_settings[
'_'.join([amqp, 'password'])] = configure_amqp(
queues[amqp]['username'],
queues[amqp]['vhost'])
relation_settings['hostname'] = utils.unit_get('private-address')
configure_client_ssl(relation_settings)
if cluster.is_clustered():
relation_settings['clustered'] = 'true'
@ -278,6 +280,7 @@ def ha_changed():
def ceph_joined():
utils.juju_log('INFO', 'Start Ceph Relation Joined')
utils.configure_source()
ceph.install()
utils.juju_log('INFO', 'Finish Ceph Relation Joined')
@ -390,6 +393,104 @@ def upgrade_charm():
MAN_PLUGIN = 'rabbitmq_management'
def configure_client_ssl(relation_data):
"""Configure client with ssl
"""
ssl_mode, external_ca = _get_ssl_mode()
if ssl_mode == 'off':
return
relation_data['ssl_port'] = utils.config_get('ssl_port')
if external_ca:
if utils.config_get('ssl_ca'):
relation_data['ssl_ca'] = base64.b64encode(
utils.config_get('ssl_ca'))
return
ca = ServiceCA.get_ca()
relation_data['ssl_ca'] = base64.b64encode(ca.get_ca_bundle())
def _get_ssl_mode():
config = utils.config_get()
ssl_mode = config.get('ssl')
external_ca = False
# Legacy config boolean option
ssl_on = config.get('ssl_enabled')
if ssl_mode == 'off' and ssl_on is False:
ssl_mode = 'off'
elif ssl_mode == 'off' and ssl_on:
ssl_mode = 'on'
ssl_key = utils.config_get('ssl_key')
ssl_cert = utils.config_get('ssl_cert')
if all((ssl_key, ssl_cert)):
external_ca = True
return ssl_mode, external_ca
def _convert_from_base64(v):
# Rabbit originally supported pem encoded key/cert in config, play
# nice on upgrades as we now expect base64 encoded key/cert/ca.
if not v:
return v
if v.startswith('-----BEGIN'):
return v
try:
return base64.b64decode(v)
except TypeError:
return v
def reconfigure_client_ssl(ssl_enabled=False):
ssl_config_keys = set(('ssl_key', 'ssl_cert', 'ssl_ca'))
for rid in hookenv.relation_ids('amqp'):
rdata = hookenv.relation_get(
rid=rid, unit=os.environ['JUJU_UNIT_NAME'])
if not ssl_enabled and ssl_config_keys.intersection(rdata):
# No clean way to remove entirely, but blank them.
utils.relation_set(
rid=rid, ssl_key='', ssl_cert='', ssl_ca='')
elif ssl_enabled and not ssl_config_keys.intersection(rdata):
configure_client_ssl(rdata)
utils.relation_set(rid=rid, **rdata)
def configure_rabbit_ssl():
"""
The legacy config support adds some additional complications.
ssl_enabled = True, ssl = off -> ssl enabled
ssl_enabled = False, ssl = on -> ssl enabled
"""
ssl_mode, external_ca = _get_ssl_mode()
if ssl_mode == 'off':
if os.path.exists(rabbit.RABBITMQ_CONF):
os.remove(rabbit.RABBITMQ_CONF)
utils.close_port(utils.config_get('ssl_port'))
reconfigure_client_ssl()
return
ssl_key = _convert_from_base64(utils.config_get('ssl_key'))
ssl_cert = _convert_from_base64(utils.config_get('ssl_cert'))
ssl_ca = _convert_from_base64(utils.config_get('ssl_ca'))
ssl_port = utils.config_get('ssl_port')
# If external managed certs then we need all the fields.
if (ssl_mode in ('on', 'only') and any((ssl_key, ssl_cert)) and
not all((ssl_key, ssl_cert))):
utils.juju_log(
'ERROR',
'If ssl_key or ssl_cert are specified both are required.')
sys.exit(1)
if not external_ca:
ssl_cert, ssl_key, ssl_ca = ServiceCA.get_service_cert()
rabbit.enable_ssl(
ssl_key, ssl_cert, ssl_port, ssl_ca,
ssl_only=(ssl_mode == "only"), ssl_client=False)
reconfigure_client_ssl(True)
utils.open_port(ssl_port)
def config_changed():
if utils.config_get('management_plugin') is True:
rabbit.enable_plugin(MAN_PLUGIN)
@ -398,22 +499,7 @@ def config_changed():
rabbit.disable_plugin(MAN_PLUGIN)
utils.close_port(55672)
if utils.config_get('ssl_enabled') is True:
ssl_key = utils.config_get('ssl_key')
ssl_cert = utils.config_get('ssl_cert')
ssl_port = utils.config_get('ssl_port')
if None in [ssl_key, ssl_cert, ssl_port]:
utils.juju_log('ERROR',
'Please provide ssl_key, ssl_cert and ssl_port'
' config when enabling SSL support')
sys.exit(1)
else:
rabbit.enable_ssl(ssl_key, ssl_cert, ssl_port)
utils.open_port(ssl_port)
else:
if os.path.exists(rabbit.RABBITMQ_CONF):
os.remove(rabbit.RABBITMQ_CONF)
utils.close_port(utils.config_get('ssl_port'))
configure_rabbit_ssl()
if cluster.eligible_leader('res_rabbitmq_vip') or \
utils.config_get('ha-vip-only') is True:

View File

@ -1,5 +1,6 @@
import json
import os
import time
from base64 import b64decode
@ -113,7 +114,8 @@ class OSContextGenerator(object):
class SharedDBContext(OSContextGenerator):
interfaces = ['shared-db']
def __init__(self, database=None, user=None, relation_prefix=None):
def __init__(self,
database=None, user=None, relation_prefix=None, ssl_dir=None):
'''
Allows inspecting relation for settings prefixed with relation_prefix.
This is useful for parsing access for multiple databases returned via
@ -122,6 +124,7 @@ class SharedDBContext(OSContextGenerator):
self.relation_prefix = relation_prefix
self.database = database
self.user = user
self.ssl_dir = ssl_dir
def __call__(self):
self.database = self.database or config('database')
@ -139,19 +142,44 @@ class SharedDBContext(OSContextGenerator):
for rid in relation_ids('shared-db'):
for unit in related_units(rid):
passwd = relation_get(password_setting, rid=rid, unit=unit)
rdata = relation_get(rid=rid, unit=unit)
ctxt = {
'database_host': relation_get('db_host', rid=rid,
unit=unit),
'database_host': rdata.get('db_host'),
'database': self.database,
'database_user': self.user,
'database_password': passwd,
'database_password': rdata.get(password_setting)
}
if context_complete(ctxt):
db_ssl(rdata, ctxt, self.ssl_dir)
return ctxt
return {}
def db_ssl(rdata, ctxt, ssl_dir):
if 'ssl_ca' in rdata and ssl_dir:
ca_path = os.path.join(ssl_dir, 'db-client.ca')
with open(ca_path, 'w') as fh:
fh.write(b64decode(rdata['ssl_ca']))
ctxt['database_ssl_ca'] = ca_path
elif 'ssl_ca' in rdata:
log("Charm not setup for ssl support but ssl ca found")
return ctxt
if 'ssl_cert' in rdata:
cert_path = os.path.join(
ssl_dir, 'db-client.cert')
if not os.path.exists(cert_path):
log("Waiting 1m for ssl client cert validity")
time.sleep(60)
with open(cert_path, 'w') as fh:
fh.write(b64decode(rdata['ssl_cert']))
ctxt['database_ssl_cert'] = cert_path
key_path = os.path.join(ssl_dir, 'db-client.key')
with open(key_path, 'w') as fh:
fh.write(b64decode(rdata['ssl_key']))
ctxt['database_ssl_key'] = key_path
return ctxt
class IdentityServiceContext(OSContextGenerator):
interfaces = ['identity-service']
@ -161,22 +189,19 @@ class IdentityServiceContext(OSContextGenerator):
for rid in relation_ids('identity-service'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
ctxt = {
'service_port': relation_get('service_port', rid=rid,
unit=unit),
'service_host': relation_get('service_host', rid=rid,
unit=unit),
'auth_host': relation_get('auth_host', rid=rid, unit=unit),
'auth_port': relation_get('auth_port', rid=rid, unit=unit),
'admin_tenant_name': relation_get('service_tenant',
rid=rid, unit=unit),
'admin_user': relation_get('service_username', rid=rid,
unit=unit),
'admin_password': relation_get('service_password', rid=rid,
unit=unit),
# XXX: Hard-coded http.
'service_protocol': 'http',
'auth_protocol': 'http',
'service_port': rdata.get('service_port'),
'service_host': rdata.get('service_host'),
'auth_host': rdata.get('auth_host'),
'auth_port': rdata.get('auth_port'),
'admin_tenant_name': rdata.get('service_tenant'),
'admin_user': rdata.get('service_username'),
'admin_password': rdata.get('service_password'),
'service_protocol':
rdata.get('service_protocol') or 'http',
'auth_protocol':
rdata.get('auth_protocol') or 'http',
}
if context_complete(ctxt):
return ctxt
@ -186,6 +211,9 @@ class IdentityServiceContext(OSContextGenerator):
class AMQPContext(OSContextGenerator):
interfaces = ['amqp']
def __init__(self, ssl_dir=None):
self.ssl_dir = ssl_dir
def __call__(self):
log('Generating template context for amqp')
conf = config()
@ -196,7 +224,6 @@ class AMQPContext(OSContextGenerator):
log('Could not generate shared_db context. '
'Missing required charm config options: %s.' % e)
raise OSContextError
ctxt = {}
for rid in relation_ids('amqp'):
for unit in related_units(rid):
@ -213,7 +240,24 @@ class AMQPContext(OSContextGenerator):
unit=unit),
'rabbitmq_virtual_host': vhost,
})
ssl_port = relation_get('ssl_port', rid=rid, unit=unit)
if ssl_port:
ctxt['rabbit_ssl_port'] = ssl_port
ssl_ca = relation_get('ssl_ca', rid=rid, unit=unit)
if ssl_ca:
ctxt['rabbit_ssl_ca'] = ssl_ca
if context_complete(ctxt):
if 'rabbit_ssl_ca' in ctxt:
if not self.ssl_dir:
log(("Charm not setup for ssl support "
"but ssl ca found"))
break
ca_path = os.path.join(
self.ssl_dir, 'rabbit-client-ca.pem')
with open(ca_path, 'w') as fh:
fh.write(b64decode(ctxt['rabbit_ssl_ca']))
ctxt['rabbit_ssl_ca'] = ca_path
# Sufficient information found = break out!
break
# Used for active/active rabbitmq >= grizzly
@ -388,6 +432,8 @@ class ApacheSSLContext(OSContextGenerator):
'private_address': unit_get('private-address'),
'endpoints': []
}
if is_clustered():
ctxt['private_address'] = config('vip')
for api_port in self.external_ports:
ext_port = determine_apache_port(api_port)
int_port = determine_api_port(api_port)

View File

@ -17,8 +17,11 @@ def headers_package():
kver = check_output(['uname', '-r']).strip()
return 'linux-headers-%s' % kver
QUANTUM_CONF_DIR = '/etc/quantum'
# legacy
def quantum_plugins():
from charmhelpers.contrib.openstack import context
return {
@ -30,7 +33,8 @@ def quantum_plugins():
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
relation_prefix='neutron',
ssl_dir=QUANTUM_CONF_DIR)],
'services': ['quantum-plugin-openvswitch-agent'],
'packages': [[headers_package(), 'openvswitch-datapath-dkms'],
['quantum-plugin-openvswitch-agent']],
@ -45,7 +49,8 @@ def quantum_plugins():
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
relation_prefix='neutron',
ssl_dir=QUANTUM_CONF_DIR)],
'services': [],
'packages': [],
'server_packages': ['quantum-server',
@ -54,6 +59,8 @@ def quantum_plugins():
}
}
NEUTRON_CONF_DIR = '/etc/neutron'
def neutron_plugins():
from charmhelpers.contrib.openstack import context
@ -66,7 +73,8 @@ def neutron_plugins():
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
relation_prefix='neutron',
ssl_dir=NEUTRON_CONF_DIR)],
'services': ['neutron-plugin-openvswitch-agent'],
'packages': [[headers_package(), 'openvswitch-datapath-dkms'],
['quantum-plugin-openvswitch-agent']],
@ -81,7 +89,8 @@ def neutron_plugins():
'contexts': [
context.SharedDBContext(user=config('neutron-database-user'),
database=config('neutron-database'),
relation_prefix='neutron')],
relation_prefix='neutron',
ssl_dir=NEUTRON_CONF_DIR)],
'services': [],
'packages': [],
'server_packages': ['neutron-server',

View File

@ -0,0 +1,78 @@
import subprocess
from charmhelpers.core import hookenv
def generate_selfsigned(keyfile, certfile, keysize="1024", config=None, subject=None, cn=None):
"""Generate selfsigned SSL keypair
You must provide one of the 3 optional arguments:
config, subject or cn
If more than one is provided the leftmost will be used
Arguments:
keyfile -- (required) full path to the keyfile to be created
certfile -- (required) full path to the certfile to be created
keysize -- (optional) SSL key length
config -- (optional) openssl configuration file
subject -- (optional) dictionary with SSL subject variables
cn -- (optional) cerfificate common name
Required keys in subject dict:
cn -- Common name (eq. FQDN)
Optional keys in subject dict
country -- Country Name (2 letter code)
state -- State or Province Name (full name)
locality -- Locality Name (eg, city)
organization -- Organization Name (eg, company)
organizational_unit -- Organizational Unit Name (eg, section)
email -- Email Address
"""
cmd = []
if config:
cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
"rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
"-keyout", keyfile,
"-out", certfile, "-config", config]
elif subject:
ssl_subject = ""
if "country" in subject:
ssl_subject = ssl_subject + "/C={}".format(subject["country"])
if "state" in subject:
ssl_subject = ssl_subject + "/ST={}".format(subject["state"])
if "locality" in subject:
ssl_subject = ssl_subject + "/L={}".format(subject["locality"])
if "organization" in subject:
ssl_subject = ssl_subject + "/O={}".format(subject["organization"])
if "organizational_unit" in subject:
ssl_subject = ssl_subject + "/OU={}".format(subject["organizational_unit"])
if "cn" in subject:
ssl_subject = ssl_subject + "/CN={}".format(subject["cn"])
else:
hookenv.log("When using \"subject\" argument you must "
"provide \"cn\" field at very least")
return False
if "email" in subject:
ssl_subject = ssl_subject + "/emailAddress={}".format(subject["email"])
cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
"rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
"-keyout", keyfile,
"-out", certfile, "-subj", ssl_subject]
elif cn:
cmd = ["/usr/bin/openssl", "req", "-new", "-newkey",
"rsa:{}".format(keysize), "-days", "365", "-nodes", "-x509",
"-keyout", keyfile,
"-out", certfile, "-subj", "/CN={}".format(cn)]
if not cmd:
hookenv.log("No config, subject or cn provided,"
"unable to generate self signed SSL certificates")
return False
try:
subprocess.check_call(cmd)
return True
except Exception as e:
print "Execution of openssl command failed:\n{}".format(e)
return False

View File

@ -0,0 +1,267 @@
import logging
import os
from os.path import join as path_join
from os.path import exists
import subprocess
log = logging.getLogger("service_ca")
logging.basicConfig(level=logging.DEBUG)
STD_CERT = "standard"
# Mysql server is fairly picky about cert creation
# and types, spec its creation separately for now.
MYSQL_CERT = "mysql"
class ServiceCA(object):
default_expiry = str(365 * 2)
default_ca_expiry = str(365 * 6)
def __init__(self, name, ca_dir, cert_type=STD_CERT):
self.name = name
self.ca_dir = ca_dir
self.cert_type = cert_type
###############
# Hook Helper API
@staticmethod
def get_ca(type=STD_CERT):
service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
ca_path = os.path.join(os.environ['CHARM_DIR'], 'ca')
ca = ServiceCA(service_name, ca_path, type)
ca.init()
return ca
@classmethod
def get_service_cert(cls, type=STD_CERT):
service_name = os.environ['JUJU_UNIT_NAME'].split('/')[0]
ca = cls.get_ca()
crt, key = ca.get_or_create_cert(service_name)
return crt, key, ca.get_ca_bundle()
###############
def init(self):
log.debug("initializing service ca")
if not exists(self.ca_dir):
self._init_ca_dir(self.ca_dir)
self._init_ca()
@property
def ca_key(self):
return path_join(self.ca_dir, 'private', 'cacert.key')
@property
def ca_cert(self):
return path_join(self.ca_dir, 'cacert.pem')
@property
def ca_conf(self):
return path_join(self.ca_dir, 'ca.cnf')
@property
def signing_conf(self):
return path_join(self.ca_dir, 'signing.cnf')
def _init_ca_dir(self, ca_dir):
os.mkdir(ca_dir)
for i in ['certs', 'crl', 'newcerts', 'private']:
sd = path_join(ca_dir, i)
if not exists(sd):
os.mkdir(sd)
if not exists(path_join(ca_dir, 'serial')):
with open(path_join(ca_dir, 'serial'), 'wb') as fh:
fh.write('02\n')
if not exists(path_join(ca_dir, 'index.txt')):
with open(path_join(ca_dir, 'index.txt'), 'wb') as fh:
fh.write('')
def _init_ca(self):
"""Generate the root ca's cert and key.
"""
if not exists(path_join(self.ca_dir, 'ca.cnf')):
with open(path_join(self.ca_dir, 'ca.cnf'), 'wb') as fh:
fh.write(
CA_CONF_TEMPLATE % (self.get_conf_variables()))
if not exists(path_join(self.ca_dir, 'signing.cnf')):
with open(path_join(self.ca_dir, 'signing.cnf'), 'wb') as fh:
fh.write(
SIGNING_CONF_TEMPLATE % (self.get_conf_variables()))
if exists(self.ca_cert) or exists(self.ca_key):
raise RuntimeError("Initialized called when CA already exists")
cmd = ['openssl', 'req', '-config', self.ca_conf,
'-x509', '-nodes', '-newkey', 'rsa',
'-days', self.default_ca_expiry,
'-keyout', self.ca_key, '-out', self.ca_cert,
'-outform', 'PEM']
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
log.debug("CA Init:\n %s", output)
def get_conf_variables(self):
return dict(
org_name="juju",
org_unit_name="%s service" % self.name,
common_name=self.name,
ca_dir=self.ca_dir)
def get_or_create_cert(self, common_name):
if common_name in self:
return self.get_certificate(common_name)
return self.create_certificate(common_name)
def create_certificate(self, common_name):
if common_name in self:
return self.get_certificate(common_name)
key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
csr_p = path_join(self.ca_dir, "certs", "%s.csr" % common_name)
self._create_certificate(common_name, key_p, csr_p, crt_p)
return self.get_certificate(common_name)
def get_certificate(self, common_name):
if not common_name in self:
raise ValueError("No certificate for %s" % common_name)
key_p = path_join(self.ca_dir, "certs", "%s.key" % common_name)
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
with open(crt_p) as fh:
crt = fh.read()
with open(key_p) as fh:
key = fh.read()
return crt, key
def __contains__(self, common_name):
crt_p = path_join(self.ca_dir, "certs", "%s.crt" % common_name)
return exists(crt_p)
def _create_certificate(self, common_name, key_p, csr_p, crt_p):
template_vars = self.get_conf_variables()
template_vars['common_name'] = common_name
subj = '/O=%(org_name)s/OU=%(org_unit_name)s/CN=%(common_name)s' % (
template_vars)
log.debug("CA Create Cert %s", common_name)
cmd = ['openssl', 'req', '-sha1', '-newkey', 'rsa:2048',
'-nodes', '-days', self.default_expiry,
'-keyout', key_p, '-out', csr_p, '-subj', subj]
subprocess.check_call(cmd)
cmd = ['openssl', 'rsa', '-in', key_p, '-out', key_p]
subprocess.check_call(cmd)
log.debug("CA Sign Cert %s", common_name)
if self.cert_type == MYSQL_CERT:
cmd = ['openssl', 'x509', '-req',
'-in', csr_p, '-days', self.default_expiry,
'-CA', self.ca_cert, '-CAkey', self.ca_key,
'-set_serial', '01', '-out', crt_p]
else:
cmd = ['openssl', 'ca', '-config', self.signing_conf,
'-extensions', 'req_extensions',
'-days', self.default_expiry, '-notext',
'-in', csr_p, '-out', crt_p, '-subj', subj, '-batch']
log.debug("running %s", " ".join(cmd))
subprocess.check_call(cmd)
def get_ca_bundle(self):
with open(self.ca_cert) as fh:
return fh.read()
CA_CONF_TEMPLATE = """
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = %(ca_dir)s
policy = policy_match
database = $dir/index.txt
serial = $dir/serial
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cacert.key
RANDFILE = $dir/private/.rand
default_md = default
[ req ]
default_bits = 1024
default_md = sha1
prompt = no
distinguished_name = ca_distinguished_name
x509_extensions = ca_extensions
[ ca_distinguished_name ]
organizationName = %(org_name)s
organizationalUnitName = %(org_unit_name)s Certificate Authority
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = match
organizationalUnitName = optional
commonName = supplied
[ ca_extensions ]
basicConstraints = critical,CA:true
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
keyUsage = cRLSign, keyCertSign
"""
SIGNING_CONF_TEMPLATE = """
[ ca ]
default_ca = CA_default
[ CA_default ]
dir = %(ca_dir)s
policy = policy_match
database = $dir/index.txt
serial = $dir/serial
certs = $dir/certs
crl_dir = $dir/crl
new_certs_dir = $dir/newcerts
certificate = $dir/cacert.pem
private_key = $dir/private/cacert.key
RANDFILE = $dir/private/.rand
default_md = default
[ req ]
default_bits = 1024
default_md = sha1
prompt = no
distinguished_name = req_distinguished_name
x509_extensions = req_extensions
[ req_distinguished_name ]
organizationName = %(org_name)s
organizationalUnitName = %(org_unit_name)s machine resources
commonName = %(common_name)s
[ policy_match ]
countryName = optional
stateOrProvinceName = optional
organizationName = match
organizationalUnitName = optional
commonName = supplied
[ req_extensions ]
basicConstraints = CA:false
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always, issuer
keyUsage = digitalSignature, keyEncipherment, keyAgreement
extendedKeyUsage = serverAuth, clientAuth
"""

View File

@ -1,10 +1,21 @@
[
{rabbit, [
{% if ssl_only %}
{tcp_listeners, []},
{% else %}
{tcp_listeners, [5672]},
{% endif %}
{ssl_listeners, [{{ ssl_port }}]},
{ssl_options, [
{verify, verify_peer},
{% if ssl_client %}
{fail_if_no_peer_cert, true},
{% else %}
{fail_if_no_peer_cert, false},
{% endif %}{% if ssl_ca_file %}
{cacertfile, "{{ ssl_ca_file }}"}, {% endif %}
{certfile, "{{ ssl_cert_file }}"},
{keyfile, "{{ ssl_key_file }}"}
]},
{tcp_listeners, [5672]}
]}
]}
].