*: rejigger tls certs and more
This pretty large change does the following:
- moves nix from bootstrap.hswaw.net to nix/
- changes clustercfg to use cfssl and moves it to cluster/clustercfg
- changes clustercfg to source information about target location of
certs from nix
- changes clustercfg to push nix config
- changes tls certs to have more than one CA
- recalculates all TLS certs
(it keeps the old serviceaccoutns key, otherwise we end up with
invalid serviceaccounts - the cert doesn't match, but who cares,
it's not used anyway)
diff --git a/cluster/clustercfg/ca.py b/cluster/clustercfg/ca.py
new file mode 100644
index 0000000..e4973db
--- /dev/null
+++ b/cluster/clustercfg/ca.py
@@ -0,0 +1,239 @@
+import json
+import logging
+import os
+from six import StringIO
+import subprocess
+
+
+logger = logging.getLogger(__name__)
+
+
+_std_subj = {
+ "C": "PL",
+ "ST": "Mazowieckie",
+ "L": "Warsaw",
+ "O": "Warsaw Hackerspace",
+ "OU": "clustercfg",
+}
+
+_ca_csr = {
+ "CN": "Prototype Test Certificate Authority",
+ "key": {
+ "algo": "rsa",
+ "size": 2048
+ },
+ "names": [ _std_subj ],
+}
+
+_ca_config = {
+ "signing": {
+ "default": {
+ "expiry": "168h"
+ },
+ "profiles": {
+ "server": {
+ "expiry": "8760h",
+ "usages": [
+ "signing",
+ "key encipherment",
+ "server auth"
+ ]
+ },
+ "client": {
+ "expiry": "8760h",
+ "usages": [
+ "signing",
+ "key encipherment",
+ "client auth"
+ ]
+ },
+ "client-server": {
+ "expiry": "8760h",
+ "usages": [
+ "signing",
+ "key encipherment",
+ "server auth",
+ "client auth"
+ ]
+ }
+ }
+ }
+}
+
+
+class CAException(Exception):
+ pass
+
+
+class CA(object):
+ def __init__(self, secretstore, certdir, short, cn):
+ self.ss = secretstore
+ self.cdir = certdir
+ self.short = short
+ self.cn = cn
+ self._init_ca()
+
+ def __str__(self):
+ return 'CN={} ({})'.format(self.cn, self.short)
+
+ @property
+ def _secret_key(self):
+ return 'ca-{}.key'.format(self.short)
+
+ @property
+ def _cert(self):
+ return os.path.join(self.cdir, 'ca-{}.crt'.format(self.short))
+
+ @property
+ def cert_data(self):
+ with open(self._cert) as f:
+ return f.read()
+
+ def _init_ca(self):
+ if self.ss.exists(self._secret_key):
+ return
+
+ ca_csr = dict(_ca_csr)
+ ca_csr['CN'] = self.cn
+
+ logger.info("{}: Generating CA...".format(self))
+ p = subprocess.Popen(['cfssl', 'gencert', '-initca', '-'],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ outs, errs = p.communicate(json.dumps(ca_csr).encode())
+ out = json.loads(outs)
+
+ f = self.ss.open(self._secret_key, 'w')
+ f.write(out['key'])
+ f.close()
+
+ f = open(self._cert, 'w')
+ f.write(out['cert'])
+ f.close()
+
+ def gen_key(self, hosts, o=_std_subj['O'], ou=_std_subj['OU'], save=None):
+ """お元気ですか?"""
+ cfg = {
+ "CN": hosts[0],
+ "hosts": hosts,
+ "key": {
+ "algo": "rsa",
+ "size": 4096,
+ },
+ "names": [
+ {
+ "C": _std_subj["C"],
+ "ST": _std_subj["ST"],
+ "L": _std_subj["L"],
+ "O": o,
+ "OU": ou,
+ },
+ ],
+ }
+ cfg.update(_ca_config)
+ logger.info("{}: Generating key/CSR for {}".format(self, hosts))
+ p = subprocess.Popen(['cfssl', 'genkey', '-'],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ outs, errs = p.communicate(json.dumps(cfg).encode())
+ out = json.loads(outs)
+ key, csr = out['key'], out['csr']
+ if save is not None:
+ logging.info("{}: Saving new key to secret {}".format(self, save))
+ f = self.ss.open(save, 'w')
+ f.write(key)
+ f.close()
+
+ return key, csr
+
+ def sign(self, csr, save=None):
+ logging.info("{}: Signing CSR".format(self))
+ ca = self._cert
+ cakey = self.ss.plaintext(self._secret_key)
+ p = subprocess.Popen(['cfssl', 'sign', '-ca=' + ca, '-ca-key=' + cakey,
+ '-profile=client-server', '-'],
+ stdin=subprocess.PIPE, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ outs, errs = p.communicate(csr.encode())
+ out = json.loads(outs)
+ cert = out['cert']
+ if save is not None:
+ name = os.path.join(self.cdir, save)
+ logging.info("{}: Saving new certificate to {}".format(self, name))
+ f = open(name, 'w')
+ f.write(cert)
+ f.close()
+
+ return cert
+
+ def upload(self, c, remote_cert):
+ logger.info("Uploading CA {} to {}".format(self, remote_cert))
+ c.put(local=self._cert, remote=remote_cert)
+
+ def make_cert(self, *a, **kw):
+ return ManagedCertificate(self, *a, **kw)
+
+
+class ManagedCertificate(object):
+ def __init__(self, ca, name, hosts, o=None, ou=None):
+ self.ca = ca
+
+ self.hosts = hosts
+ self.name = name
+ self.key = '{}.key'.format(name)
+ self.cert = '{}.cert'.format(name)
+ self.o = o
+ self.ou = ou
+
+ self.ensure()
+
+ def __str__(self):
+ return '{}'.format(self.name)
+
+ @property
+ def key_exists(self):
+ return self.ca.ss.exists(self.key)
+
+ @property
+ def key_data(self):
+ f = open(self.ca.ss.open(self.key))
+ d = f.read()
+ f.close()
+ return d
+
+ @property
+ def key_path(self):
+ return self.ca.ss.plaintext(self.key)
+
+ @property
+ def cert_path(self):
+ return os.path.join(self.ca.cdir, self.cert)
+
+ @property
+ def cert_exists(self):
+ return os.path.exists(self.cert_path)
+
+ @property
+ def cert_data(self):
+ with open(self.cert_path) as f:
+ return f.read()
+
+ def ensure(self):
+ if self.key_exists and self.cert_exists:
+ return
+
+ logger.info("{}: Generating...".format(self))
+ key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
+ self.ca.sign(csr, save=self.cert)
+
+ def upload(self, c, remote_cert, remote_key, concat_ca=False):
+ logger.info("Uploading Cert {} to {} & {}".format(self, remote_cert, remote_key))
+ if concat_ca:
+ f = StringIO(self.cert_data + self.ca.cert_data)
+ c.put(local=f, remote=remote_cert)
+ else:
+ c.put(local=self.cert_path, remote=remote_cert)
+ c.put(local=self.key_path, remote=remote_key)
+
+ def upload_pki(self, c, pki, concat_ca=False):
+ self.upload(c, pki['cert'], pki['key'], concat_ca)