Eevans has uploaded a new change for review. https://gerrit.wikimedia.org/r/236389
Change subject: WIP: certificate/keystore generation script ...................................................................... WIP: certificate/keystore generation script cassandra-ca-mgr accepts a yaml-formatted manifest file as its only argument and generates a root CA and corresponding Java truststore, and an arbitrary number of Java keystores, signed by the root CA. The script is idempotent, so subsequent invocations with an identical manifest should effect no change; Invocations with additional keystore entity definitions will result only on the creation of the new keystores. Try `pydoc ./cassandra-ca-mgr' to see an example of how to format a manifest. Bug: T108953 Change-Id: I497ff7de13cb87e82cd7f6562f7abfdb7c97fcd2 --- A modules/cassandra/files/cassandra-ca-mgr 1 file changed, 364 insertions(+), 0 deletions(-) git pull ssh://gerrit.wikimedia.org:29418/operations/puppet refs/changes/89/236389/1 diff --git a/modules/cassandra/files/cassandra-ca-mgr b/modules/cassandra/files/cassandra-ca-mgr new file mode 100755 index 0000000..189bda6 --- /dev/null +++ b/modules/cassandra/files/cassandra-ca-mgr @@ -0,0 +1,364 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Cassandra certificate management + +First, you need a manifest that specifies the Certificate Authority, and +each of the keystores. For example: + + # The top-level working directory + base_directory: /path/to/base/directory + + # The Certificate Authority + authority: + key: + size: 2048 + cert: + subject: + organization: WMF + country: US + unit: Services + valid: 365 + password: qwerty + + # Java keystores + keystores: + - name: restbase1001-a + key: + size: 2048 + cert: + subject: + organization: WMF + country: US + unit: Services + valid: 365 + password: qwerty + + - name: restbase1001-b + key: + size: 2048 + cert: + subject: + organization: WMF + country: US + unit: Services + valid: 365 + password: qwerty + + - name: restbase1002-a + key: + size: 2048 + cert: + subject: + organization: WMF + country: US + unit: Services + valid: 365 + password: qwerty + +Next, run the script with the manifest as its only argument: + + $ cassandra-ca manifest.yaml + $ tree /path/to/base/directory + /path/to/base/directory + ├── restbase1001-a + │ ├── restbase1001-a.crt + │ └── restbase1001-a.csr + │ └── restbase1001-a.kst + ├── restbase1001-b + │ ├── restbase1001-b.crt + │ └── restbase1001-b.csr + │ └── restbase1001-b.kst + ├── restbase1002-a + │ ├── restbase1002-a.crt + │ └── restbase1002-a.csr + │ └── restbase1002-a.kst + ├── rootCa.crt + ├── rootCa.key + ├── rootCa.srl + └── truststore + + 3 directories, 13 files + + +""" + +import logging +import os +import os.path +import subprocess +import sys +import yaml # PyYAML (python-yaml) + + +logging.basicConfig(level=logging.DEBUG) + + +class Subject(object): + def __init__(self, common_name, **kwargs): + self.common_name = common_name + self.organization = kwargs.get("organization", "WMF") + self.country = kwargs.get("country", "US") + self.unit = kwargs.get("unit", "Services") + + def __repr__(self): + return "%s(cn=%s, o=%s, c=%s, u=%s)" \ + % (self.__class__.__name__, self.common_name, self.organization, self.country, self.unit) + +class KeytoolSubject(Subject): + def __str__(self): + return "cn=%s, ou=%s, o=%s, c=%s" % (self.common_name, self.unit, self.organization, self.country) + +class Keystore(object): + def __init__(self, path, authority, **kwargs): + name = kwargs.get("name") + password = kwargs.get("password") + + if name is None: + raise RuntimeError("corrupt keystore entry; missing keystore name") + if password is None: + raise RuntimeError("corrupt keystore entry; missing keystore password") + + key = kwargs.get("key", dict(size=2048)) + size = int(key.get("size", 2048)) + cert = kwargs.get("cert", dict(valid=365)) + + self.base = os.path.abspath(path) + self.name = name + self.authority = authority + self.filename = os.path.join(self.base, name, "%s.kst" % self.name) + self.csr = os.path.join(self.base, name, "%s.csr" % name) + self.crt = os.path.join(self.base, name, "%s.crt" % name) + self.password = password + self.size = size + self.subject = KeytoolSubject(self.name, **cert) + self.valid = int(cert.get("valid", 365)) + + mkdirs(os.path.join(self.base, name)) + + def generate(self): + if not os.path.exists(self.filename): + # Generate the node key + # + # It looks as though a key password is required (if you do not pass the + # argument, then keytool prompts for the password on STDIN). Cassandra + # it seems, depends upon the key and store passwords being identical, (and + # indeed, keytool itself will attempt to use the -storepass when -keypass + # is omitted). So much WTF. + command = [ + "keytool", + "-genkeypair", + "-dname", str(self.subject), + "-keyalg", "RSA", + "-alias", self.name, + "-validity", str(self.valid), + "-storepass", self.password, + "-keypass", self.password, + "-keystore", self.filename + ] + if not run_command(command): + raise RuntimeError("CA key generation failed") + + # Generate a certificate signing request. + command = [ + "keytool", + "-certreq", + "-dname", str(self.subject), + "-alias", self.name, + "-file", self.csr, + "-keypass", self.password, + "-storepass", self.password, + "-keystore", self.filename + ] + if not run_command(command): + raise RuntimeError("signing request generation failed") + + # Sign (and verify). + command = [ + "openssl", + "x509", + "-req", + "-CAcreateserial", + "-in", self.csr, + "-CA", self.authority.certificate.filename, + "-CAkey", self.authority.key.filename, + "-days", str(self.valid), + "-out", self.crt + ] + if not run_command(command): + raise RuntimeError("certificate signing failed") + + command = [ + "openssl", + "verify", + "-CAfile", self.authority.certificate.filename, + self.crt + ] + if not run_command(command): + raise RuntimeError("certificate verification failed") + + # Before we can import the signed certificate, the signer must be trusted, + # either with a trust entry in this keystore, or with one in the system + # truststore, aka 'cacerts', (provided -trustcacerts is passed). + command = [ + "keytool", + "-importcert", + "-noprompt", + "-file", self.authority.certificate.filename, + "-storepass", self.password, + "-keystore", self.filename + ] + if not run_command(command): + raise RuntimeError("import of CA cert failed") + + # Import the CA signed certificate. + command = [ + "keytool", + "-importcert", + "-noprompt", + "-file", self.crt, + "-alias", self.name, + "-storepass", self.password, + "-keystore", self.filename + ] + if not run_command(command): + raise RuntimeError("import of CA-signed cert failed") + else: + logging.warn("%s already exists, skipping keystore generation...", self.filename) + + def __repr__(self): + return "%s(name=%s, filename=%s, size=%s, subject=%s)" \ + % (self.__class__.__name__, self.name, self.filename, self.size, self.subject) + +class OpensslSubject(Subject): + def __str__(self): + return "/CN=%s/OU=%s/O=%s/C=%s/" % (self.common_name, self.unit, self.organization, self.country) + +class OpensslCertificate(object): + def __init__(self, name, path, key, password, **kwargs): + self.name = name + self.base = os.path.abspath(path) + self.filename = os.path.join(self.base, "%s.crt" % self.name) + self.truststore = os.path.join(self.base, "truststore") + self.key = key + self.password = password + self.subject = OpensslSubject(name, **(kwargs.get("subject"))) + self.valid = int(kwargs.get("valid", 365)) + + def generate(self): + # Generate the CA certificate + if not os.path.exists(self.filename): + command = [ + "openssl", + "req", + "-x509", + "-new", + "-nodes", + "-subj", str(self.subject), + "-days", str(self.valid), + "-key", self.key.filename, + "-out", self.filename + ] + if not run_command(command): + raise RuntimeError("CA certificate generation failed") + else: + logging.warn("%s already exists, skipping certificate generation...", self.filename) + + # Import the CA certificate to a Java truststore + if not os.path.exists(self.truststore): + # FIXME: -storepass should use :file or :env specifier to avoid exposing password to process list + command = [ + "keytool", + "-importcert", + "-v", + "-noprompt", + "-trustcacerts", + "-alias", "rootCa", + "-file", self.filename, + "-storepass", self.password, + "-keystore", self.truststore + ] + if not run_command(command): + raise RuntimeError("CA truststore generation failed") + else : + logging.warn("%s already exists, skipping truststore generation...", self.filename) + + def __repr__(self): + return "%s(name=%s, filename=%s, subject=%s, valid=%d)" \ + % (self.__class__.__name__, self.name, self.filename, self.subject, self.valid) + +class OpensslKey(object): + def __init__(self, name, path, **kwargs): + self.name = name + self.base = os.path.abspath(path) + self.filename = os.path.join(self.base, "%s.key" % self.name) + self.size = kwargs.get("size", 2048) + + mkdirs(self.base) + + def generate(self): + if not os.path.exists(self.filename): + if not run_command(["openssl", "genrsa", "-out", self.filename, str(self.size)]): + raise RuntimeError("CA key generation failed") + else: + logging.warn("%s already exists, skipping key generation...", self.filename) + + def __repr__(self): + return "%s(name=%s, filename=%s, size=%s)" % (self.__class__.__name__, self.name, self.filename, self.size) + +class Authority(object): + def __init__(self, base_directory, **kwargs): + self.password = kwargs.get("password") + if self.password is None: + raise RuntimeError("authority is missing mandatory password entry") + + self.base_directory = base_directory + self.key = OpensslKey("rootCa", self.base_directory, **(kwargs.get("key"))) + self.certificate = OpensslCertificate("rootCa", self.base_directory, self.key, self.password, **(kwargs.get("cert"))) + + def generate(self): + self.key.generate() + self.certificate.generate() + + def __repr__(self): + return "%s(key=%s, certifcate=%s)" % (self.__class__.__name__, self.key, self.certificate) + +def read_manifest(manifest): + with open(manifest, 'r') as f: + return yaml.load(f.read()) + +def run_command(command): + try: + output = subprocess.check_output(command, stderr=subprocess.STDOUT) + for ln in output.split("\n"): logging.debug(ln) + logging.debug("command succeeded: %s", " ".join(command)) + except subprocess.CalledProcessError, e: + for ln in e.output.split("\n"): logging.error(ln) + logging.error("command returned status %d: %s", e.returncode, " ".join(command)) + return False + return True + +def mkdirs(directory): + if not os.path.exists(directory): + os.makedirs(directory) + +if __name__ == '__main__': + import argparse + + parser = argparse.ArgumentParser(description='Manage a certificate authority') + parser.add_argument("manifest", type=str, + help="YAML specification of managed keys and certificates") + args = parser.parse_args() + + manifest = read_manifest(args.manifest) + + base_directory = manifest.get("base_directory", os.path.abspath(os.curdir)) + authority = Authority(base_directory, **(manifest.get("authority"))) + + authority.generate() + + entities = manifest.get("keystores") + + for entity in entities: + Keystore(base_directory, authority, **entity).generate() -- To view, visit https://gerrit.wikimedia.org/r/236389 To unsubscribe, visit https://gerrit.wikimedia.org/r/settings Gerrit-MessageType: newchange Gerrit-Change-Id: I497ff7de13cb87e82cd7f6562f7abfdb7c97fcd2 Gerrit-PatchSet: 1 Gerrit-Project: operations/puppet Gerrit-Branch: production Gerrit-Owner: Eevans <eev...@wikimedia.org> _______________________________________________ MediaWiki-commits mailing list MediaWiki-commits@lists.wikimedia.org https://lists.wikimedia.org/mailman/listinfo/mediawiki-commits