diff options
-rw-r--r-- | doc/server/plugins/generators/sslca.txt | 96 | ||||
-rw-r--r-- | src/lib/Server/Plugins/SSLCA.py | 249 |
2 files changed, 345 insertions, 0 deletions
diff --git a/doc/server/plugins/generators/sslca.txt b/doc/server/plugins/generators/sslca.txt new file mode 100644 index 000000000..118a16559 --- /dev/null +++ b/doc/server/plugins/generators/sslca.txt @@ -0,0 +1,96 @@ +.. -*- mode: rst -*- + +.. _server-plugins-generators-sslca: + +===== +SSLCA +===== + +SSLCA is a generator plugin designed to handle creation of SSL private keys +and certificates on request. + +Borrowing ideas from the TGenshi and SSHbase plugins, SSLCA automates the +generation of SSL certificates by allowing you to specify key and certificate +definitions. Then, when a client requests a Path that contains such a +definition within the SSLCA repository, the matching key/cert is generated, and +stored in a hostfile in the repo so that subsequent requests do not result in +repeated key/cert recreation. In the event that a new key or cert is needed, +the offending hostfile can simply be removed from the repository, and the next +time that host checks in, a new file will be created. If that file happens to +be the key, any dependent certificates will also be regenerated. + +Getting started +=============== + +In order to use SSLCA, you must first have at least one CA configured on +your system. For details on setting up your own OpenSSL based CA, please +see http://www.openssl.org/docs/apps/ca.html for details of the suggested +directory layout and configuration directives. + +For SSLCA to work, the openssl.cnf (or other configuration file) for that CA +must contain full (not relative) paths. + +#. Add SSLCA to the **plugins** line in ``/etc/bcfg2.conf`` and restart the + server -- This enabled the SSLCA plugin on the Bcfg2 server. + +#. Add a section to your ``/etc/bcfg2.conf`` called sslca_foo, replacing foo +with the name you wish to give your CA so you can reference it in certificate +definitions. + +#. Under that section, add an entry for ``config`` that gives the location of +the openssl configuration file for your CA. + +#. If necessary, add an entry for ``passphrase`` containing the passphrase for +the CA's private key. We store this in ``/etc/bcfg2.conf`` as the permissions +on that file should have it only readable by the bcfg2 user. If no passphrase +is entry exists, it is assumed that the private key is stored unencrypted. + +#. Add an entry ``chaincert`` that points to the location of your ssl chaining +certificate. This is used when preexisting certifcate hostfiles are found, so +that they can be validated and only regenerated if they no longer meet the +specification. + +#. Once all this is done, you should have a section in your ``/etc/bcfg2.conf`` +that looks similar to the following: + + [sslca_default] + config = /etc/pki/CA/openssl.cnf + passphrase = youReallyThinkIdShareThis? + chaincert = /etc/pki/CA/chaincert.crt + +#. You are now ready to create key and certificate definitions. For this +example we'll assume you've added Path entries for the key, +``/etc/pki/tls/private/localhost.key``, and the certificate, +``/etc/pki/tls/certs/localhost.crt`` to a bundle or base. + +#. Defining a key or certificate is similar to defining a TGenshi template. +Under your Bcfg2's SSLCA directory, create the directory structure to match the +path to your key. In this case this would be something like +``/var/lib/bcfg2/SSLCA/etc/pki/tls/private/localhost.key``. + +#. Within that directory, create a ``key.xml`` file containing the following: + + <KeyInfo> + <Key type="rsa" bits="2048" /> + </KeyInfo> + +#. This will cause the generation of an 2048 bit RSA key when a client requests +that Path. Alternatively you can specify ``dsa`` as the keytype, or a different +number of bits. + +#. Similarly, create the matching directory structure for the certificate path, +and a ``cert.xml`` containinng the following: + + <CertInfo> + <Cert format="pem" key="/etc/pki/tls/private/localhost.key" ca="default" days="365" c="US" l="New York" st="New York" o="Your Company Name" /> + </CertInfo> + +#. When a client requests the cert path, a certificate will be generated using +the key hostfile at the specified key location, using the CA matching the ca +attribute. ie. ca="default" will match [sslca_default] in your +``/etc/bcfg2.conf`` + +TODO +==== + +#. Add generation of pkcs12 format certs diff --git a/src/lib/Server/Plugins/SSLCA.py b/src/lib/Server/Plugins/SSLCA.py new file mode 100644 index 000000000..a961e744a --- /dev/null +++ b/src/lib/Server/Plugins/SSLCA.py @@ -0,0 +1,249 @@ +""" +Notes: + +1. Put these notes in real docs!!! +2. dir structure for CA's must be correct +3. for subjectAltNames to work, openssl.conf must have copy_extensions on +""" + + +import Bcfg2.Server.Plugin +import Bcfg2.Options +import lxml.etree +import posixpath +import tempfile +import os +from subprocess import Popen, PIPE +from ConfigParser import ConfigParser + +import pdb + +class SSLCA(Bcfg2.Server.Plugin.GroupSpool): + """ + The SSLCA generator handles the creation and + management of ssl certificates and their keys. + """ + name = 'SSLCA' + __version__ = '$Id:$' + __author__ = 'g.hagger@gmail.com' + __child__ = Bcfg2.Server.Plugin.FileBacked + key_specs = {} + cert_specs = {} + CAs = {} + + def HandleEvent(self, event=None): + """ + Updates which files this plugin handles based upon filesystem events. + Allows configuration items to be added/removed without server restarts. + """ + action = event.code2str() + if event.filename[0] == '/': + return + epath = "".join([self.data, self.handles[event.requestID], + event.filename]) + if posixpath.isdir(epath): + ident = self.handles[event.requestID] + event.filename + else: + ident = self.handles[event.requestID][:-1] + + fname = "".join([ident, '/', event.filename]) + + if event.filename.endswith('.xml'): + if action in ['exists', 'created', 'changed']: + if event.filename.endswith('key.xml'): + key_spec = dict(lxml.etree.parse(epath).find('Key').items()) + self.key_specs[ident] = { + 'bits': key_spec.get('bits', 2048), + 'type': key_spec.get('type', 'rsa') + } + self.Entries['Path'][ident] = self.get_key + elif event.filename.endswith('cert.xml'): + cert_spec = dict(lxml.etree.parse(epath).find('Cert').items()) + ca = cert_spec.get('ca', 'default') + self.cert_specs[ident] = { + 'ca': ca, + 'format': cert_spec.get('format', 'pem'), + 'key': cert_spec.get('key'), + 'days': cert_spec.get('days', 365), + 'C': cert_spec.get('c'), + 'L': cert_spec.get('l'), + 'ST': cert_spec.get('st'), + 'OU': cert_spec.get('ou'), + 'O': cert_spec.get('o'), + 'emailAddress': cert_spec.get('emailaddress') + } + cp = ConfigParser() + cp.read(self.core.cfile) + self.CAs[ca] = dict(cp.items('sslca_'+ca)) + self.Entries['Path'][ident] = self.get_cert + if action == 'deleted': + if ident in self.Entries['Path']: + del self.Entries['Path'][ident] + else: + if action in ['exists', 'created']: + if posixpath.isdir(epath): + self.AddDirectoryMonitor(epath[len(self.data):]) + if ident not in self.entries and posixpath.isfile(epath): + self.entries[fname] = self.__child__(epath) + self.entries[fname].HandleEvent(event) + if action == 'changed': + self.entries[fname].HandleEvent(event) + elif action == 'deleted': + if fname in self.entries: + del self.entries[fname] + else: + self.entries[fname].HandleEvent(event) + + def get_key(self, entry, metadata): + """ + either grabs a prexisting key hostfile, or triggers the generation + of a new key if one doesn't exist. + """ + # set path type and permissions, otherwise bcfg2 won't bind the file + permdata = {'owner':'root', + 'group':'root', + 'type':'file', + 'perms':'644'} + [entry.attrib.__setitem__(key, permdata[key]) for key in permdata] + + # check if we already have a hostfile, or need to generate a new key + # TODO: verify key fits the specs + path = entry.get('name') + filename = "".join([path, '/', path.rsplit('/', 1)[1], '.H_', metadata.hostname]) + if filename not in self.entries.keys(): + key = self.build_key(filename, entry, metadata) + open(self.data + filename, 'w').write(key) + entry.text = key + else: + entry.text = self.entries[filename].data + + def build_key(self, filename, entry, metadata): + """ + generates a new key according the the specification + """ + type = self.key_specs[entry.get('name')]['type'] + bits = self.key_specs[entry.get('name')]['bits'] + if type == 'rsa': + cmd = "openssl genrsa %s " % bits + elif type == 'dsa': + cmd = "openssl dsaparam -noout -genkey %s" % bits + key = Popen(cmd, shell=True, stdout=PIPE).stdout.read() + return key + + def get_cert(self, entry, metadata): + """ + either grabs a prexisting cert hostfile, or triggers the generation + of a new cert if one doesn't exist. + """ + # set path type and permissions, otherwise bcfg2 won't bind the file + permdata = {'owner':'root', + 'group':'root', + 'type':'file', + 'perms':'644'} + [entry.attrib.__setitem__(key, permdata[key]) for key in permdata] + + path = entry.get('name') + filename = "".join([path, '/', path.rsplit('/', 1)[1], '.H_', metadata.hostname]) + + # first - ensure we have a key to work with + key = self.cert_specs[entry.get('name')].get('key') + key_filename = "".join([key, '/', key.rsplit('/', 1)[1], '.H_', metadata.hostname]) + if key_filename not in self.entries: + e = lxml.etree.Element('Path') + e.attrib['name'] = key + self.core.Bind(e, metadata) + + # check if we have a valid hostfile + if filename in self.entries.keys() and self.verify_cert(filename, entry): + entry.text = self.entries[filename].data + else: + cert = self.build_cert(entry, metadata) + open(self.data + filename, 'w').write(cert) + entry.text = cert + + def verify_cert(self, filename, entry): + """ + check that a certificate validates against the ca cert, + and that it has not expired. + """ + chaincert = self.CAs[self.cert_specs[entry.get('name')]['ca']].get('chaincert') + cert = "".join([self.data, '/', filename]) + cmd = "openssl verify -CAfile %s %s" % (chaincert, cert) + proc = Popen(cmd, shell=True) + proc.communicate() + if proc.returncode != 0: + return False + return True + + def build_cert(self, entry, metadata): + """ + creates a new certificate according to the specification + """ + req_config = self.build_req_config(entry, metadata) + req = self.build_request(req_config, entry) + ca = self.cert_specs[entry.get('name')]['ca'] + ca_config = self.CAs[ca]['config'] + days = self.cert_specs[entry.get('name')]['days'] + passphrase = self.CAs[ca].get('passphrase') + if passphrase: + cmd = "openssl ca -config %s -in %s -days %s -batch -passin pass:%s" % (ca_config, req, days, passphrase) + else: + cmd = "openssl ca -config %s -in %s -days %s -batch" % (ca_config, req, days) + cert = Popen(cmd, shell=True, stdout=PIPE).stdout.read() + try: + os.unlink(req_config) + os.unlink(req) + except OSError: + self.logger.error("Failed to unlink temporary files") + return cert + + def build_req_config(self, entry, metadata): + """ + generates a temporary openssl configuration file that is + used to generate the required certificate request + """ + # create temp request config file + conffile = open(tempfile.mkstemp()[1], 'w') + cp = ConfigParser({}) + cp.optionxform = str + defaults = { + 'req': { + 'default_md': 'sha1', + 'distinguished_name': 'req_distinguished_name', + 'req_extensions': 'v3_req', + 'x509_extensions': 'v3_req', + 'prompt': 'no' + }, + 'req_distinguished_name': {}, + 'v3_req': { + 'subjectAltName': '@alt_names' + }, + 'alt_names': {} + } + for section in defaults.keys(): + cp.add_section(section) + for key in defaults[section]: + cp.set(section, key, defaults[section][key]) + x = 1 + for alias in metadata.aliases: + cp.set('alt_names', 'DNS.'+str(x), alias) + x += 1 + for item in ['C', 'L', 'ST', 'O', 'OU', 'emailAddress']: + if self.cert_specs[entry.get('name')][item]: + cp.set('req_distinguished_name', item, self.cert_specs[entry.get('name')][item]) + cp.set('req_distinguished_name', 'CN', metadata.hostname) + cp.write(conffile) + conffile.close() + return conffile.name + + def build_request(self, req_config, entry): + """ + creates the certificate request + """ + req = tempfile.mkstemp()[1] + key = self.cert_specs[entry.get('name')]['key'] + days = self.cert_specs[entry.get('name')]['days'] + cmd = "openssl req -new -config %s -days %s -key %s -text -out %s" % (req_config, days, key, req) + res = Popen(cmd, shell=True, stdout=PIPE).stdout.read() + return req + |