forked from hswaw/hscloud
This makes clustercfg ensure certificates are valid for at least 30 days, and renew them otherwise. We use this to bump all the certs that were about to expire in a week. They are now valid until 2021. There's still some certs that expire in 2020. We need to figure out a better story for this, especially as the next expiry is 2021 - todays prod rollout was somewhat disruptive (basically this was done by a full cluster upgrade-like rollout flow, via clustercfg). We also drive-by bump the number of mons in ceph-waw3 to 3, as it shouls be (this gets rid of a nasty SPOF that would've bitten us during this upgrade otherwise). Change-Id: Iee050b1b9cba4222bc0f3c7bce9e4cf9b25c8bdc
315 lines
8.3 KiB
Python
315 lines
8.3 KiB
Python
# encoding: utf-8
|
|
from datetime import datetime, timezone
|
|
import json
|
|
import logging
|
|
import os
|
|
from six import StringIO
|
|
import subprocess
|
|
import tempfile
|
|
|
|
|
|
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": {
|
|
"intermediate": {
|
|
"expiry": "8760h",
|
|
"usages": [
|
|
"signing",
|
|
"key encipherment",
|
|
"cert sign",
|
|
"crl sign",
|
|
"server auth",
|
|
"client auth",
|
|
],
|
|
"ca_constraint": {
|
|
"is_ca": True,
|
|
},
|
|
},
|
|
"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 _cfssl_call(self, args, obj=None, stdin=None):
|
|
p = subprocess.Popen(['cfssl'] + args,
|
|
stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE)
|
|
if obj is not None:
|
|
stdin = json.dumps(obj)
|
|
|
|
outs, errs = p.communicate(stdin.encode())
|
|
if p.returncode != 0:
|
|
raise Exception(
|
|
'cfssl failed. stderr: %r, stdout: %r, code: %r' % (
|
|
errs, outs, p.returncode))
|
|
|
|
out = json.loads(outs)
|
|
return out
|
|
|
|
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))
|
|
out = self._cfssl_call(['gencert', '-initca', '-'], obj=ca_csr)
|
|
|
|
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))
|
|
out = self._cfssl_call(['genkey', '-'], obj=cfg)
|
|
|
|
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 gen_csr(self, key, hosts, o=_std_subj['O'], ou=_std_subj['OU']):
|
|
"""
|
|
Generate a CSR while already having a private key - for renewals, etc.
|
|
|
|
TODO(q3k): this shouldn't be a CA method, but a cert method.
|
|
"""
|
|
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 CSR for {}".format(self, hosts))
|
|
out = self._cfssl_call(['gencsr', '-key', key, '-'], obj=cfg)
|
|
|
|
return out['csr']
|
|
|
|
def sign(self, csr, save=None, profile='client-server'):
|
|
logging.info("{}: Signing CSR".format(self))
|
|
ca = self._cert
|
|
cakey = self.ss.plaintext(self._secret_key)
|
|
|
|
config = tempfile.NamedTemporaryFile(mode='w')
|
|
json.dump(_ca_config, config)
|
|
config.flush()
|
|
|
|
out = self._cfssl_call(['sign', '-ca=' + ca, '-ca-key=' + cakey,
|
|
'-profile='+profile, '-config='+config.name, '-'], stdin=csr)
|
|
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()
|
|
|
|
config.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, profile='client-server'):
|
|
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.profile = profile
|
|
|
|
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()
|
|
|
|
@property
|
|
def cert_expires_soon(self):
|
|
if not self.cert_exists:
|
|
return False
|
|
|
|
out = self.ca._cfssl_call(['certinfo', '-cert', self.cert_path], stdin="")
|
|
not_after = datetime.strptime(out['not_after'], '%Y-%m-%dT%H:%M:%SZ').replace(tzinfo=timezone.utc)
|
|
until = not_after - datetime.now(timezone.utc)
|
|
if until.days < 30:
|
|
return True
|
|
return False
|
|
|
|
def ensure(self):
|
|
if self.key_exists and self.cert_exists and not self.cert_expires_soon:
|
|
return
|
|
|
|
key = None
|
|
if not self.key_exists:
|
|
logger.info("{}: Generating key...".format(self))
|
|
key, csr = self.ca.gen_key(self.hosts, o=self.o, ou=self.ou, save=self.key)
|
|
else:
|
|
logger.info("{}: Renewing certificate...".format(self))
|
|
# Use already existing key
|
|
csr = self.ca.gen_csr(self.key_path, self.hosts, o=self.o, ou=self.ou)
|
|
self.ca.sign(csr, save=self.cert, profile=self.profile)
|
|
|
|
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)
|