summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--doc/server/plugins/generators/sslca.txt96
-rw-r--r--src/lib/Server/Plugins/SSLCA.py249
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
+