| #!/usr/bin/env python |
| |
| from builtins import object |
| |
| import datetime |
| from io import BytesIO |
| import logging |
| import os |
| import tempfile |
| import subprocess |
| import sys |
| |
| from cryptography import x509 |
| from cryptography.hazmat.backends import default_backend |
| import fabric |
| |
| import secretstore |
| |
| |
| cluster = 'k0.hswaw.net' |
| remote_root = '/opt/hscloud' |
| local_root = os.getenv('hscloud_root') |
| |
| if local_root is None: |
| raise Exception("Please source env.sh") |
| |
| logger = logging.getLogger(__name__) |
| logger.setLevel(logging.DEBUG) |
| logger.addHandler(logging.StreamHandler()) |
| |
| |
| def decrypt(base): |
| src = os.path.join(local_root, 'cluster/secrets/cipher', base) |
| dst = os.path.join(local_root, 'cluster/secrets/plain', base) |
| secretstore.decrypt(src, dst) |
| |
| |
| class PKI(object): |
| def __init__(self): |
| self.cacert = os.path.join(local_root, 'cluster/certs/ca.crt') |
| self.cakey = os.path.join(local_root, 'cluster/secrets/plain/ca.key') |
| |
| if not os.path.exists(self.cakey): |
| decrypt('ca.key') |
| |
| def sign(self, csr, crt, conf, days=365): |
| logger.info('pki: signing {} for {} days'.format(csr, days)) |
| subprocess.check_call([ |
| 'openssl', 'x509', '-req', |
| '-in', csr, |
| '-CA', self.cacert, |
| '-CAkey', self.cakey, |
| '-out', crt, |
| '-extensions', 'SAN', '-extfile', conf, |
| '-days', str(days), |
| ]) |
| |
| |
| class Subject(object): |
| hswaw = "Stowarzyszenie Warszawski Hackerspace" |
| def __init__(self, o, ou, cn): |
| self.c = 'PL' |
| self.st = 'Mazowieckie' |
| self.l = 'Warszawa' |
| self.o = o |
| self.ou = ou |
| self.cn = cn |
| |
| @property |
| def parts(self): |
| return { |
| 'C': self.c, |
| 'ST': self.st, |
| 'L': self.l, |
| 'O': self.o, |
| 'OU': self.ou, |
| 'CN': self.cn, |
| } |
| |
| def __str__(self): |
| parts = self.parts |
| res = [] |
| for p in ['C', 'ST', 'L', 'O', 'OU', 'CN']: |
| res.append('/{}={}'.format(p, parts[p])) |
| return ''.join(res) |
| |
| def _file_exists(c, filename): |
| res = c.run('stat "{}"'.format(filename), warn=True, hide=True) |
| return res.exited == 0 |
| |
| def openssl_config(san): |
| with open(os.path.join(local_root, 'cluster/openssl.cnf'), 'rb') as f: |
| config = BytesIO(f.read()) |
| |
| config.seek(0, 2) |
| config.write(b'\n[SAN]\n') |
| for s in san: |
| config.write('subjectAltName=DNS:{}\n'.format(s).encode()) |
| |
| f = tempfile.NamedTemporaryFile(delete=False) |
| path = f.name |
| f.write(config.getvalue()) |
| f.close() |
| |
| return path |
| |
| def remote_cert(pki, c, fqdn, cert_name, subj, san=[], days=365): |
| logger.info("{}/{}: remote cert".format(fqdn, cert_name)) |
| |
| remote_key = os.path.join(remote_root, '{}.key'.format(cert_name)) |
| remote_cert = os.path.join(remote_root, '{}.crt'.format(cert_name)) |
| remote_csr = os.path.join(remote_root, '{}.csr'.format(cert_name)) |
| remote_config = os.path.join(remote_root, 'openssl.cnf') |
| |
| generate_cert = False |
| if not _file_exists(c, remote_key): |
| logger.info("{}/{}: generating key".format(fqdn, cert_name)) |
| c.run('openssl genrsa -out "{}" 4096'.format(remote_key), hide=True) |
| genereate_cert = True |
| |
| b = BytesIO() |
| try: |
| c.get(local=b, remote=remote_cert) |
| cert = x509.load_pem_x509_certificate(b.getvalue(), default_backend()) |
| delta = cert.not_valid_after - datetime.datetime.now() |
| logger.info("{}/{}: existing cert expiry: {}".format(fqdn, cert_name, delta)) |
| if delta.total_seconds() < 3600 * 24 * 60: |
| logger.info("{}/{}: expires soon, regenerating".format(fqdn, cert_name)) |
| generate_cert = True |
| except (FileNotFoundError, ValueError): |
| generate_cert = True |
| |
| if not generate_cert: |
| return False |
| |
| |
| local_config = openssl_config(san) |
| c.put(local=local_config, remote=remote_config) |
| |
| c.run(""" |
| nix-shell -p openssl --command "openssl req -new -key {remote_key} -out {remote_csr} -subj '{subj}' -config {remote_config} -reqexts SAN" |
| """.format(remote_key=remote_key, remote_csr=remote_csr, subj=str(subj), remote_config=remote_config)) |
| |
| local_csr_f = tempfile.NamedTemporaryFile(delete=False) |
| local_csr = local_csr_f.name |
| local_csr_f.close() |
| |
| local_cert = os.path.join(local_root, 'cluster/certs', '{}-{}.crt'.format(fqdn, cert_name)) |
| |
| c.get(local=local_csr, remote=remote_csr) |
| |
| pki.sign(local_csr, local_cert, local_config, days) |
| |
| c.put(local=local_cert, remote=remote_cert) |
| |
| os.remove(local_csr) |
| os.remove(local_config) |
| |
| return True |
| |
| |
| def shared_cert(pki, c, fqdn, cert_name, subj, san=[], days=365): |
| logger.info("{}/{}: shared cert".format(fqdn, cert_name)) |
| |
| local_key = os.path.join(local_root, 'cluster/secrets/plain', '{}.key'.format(cert_name)) |
| local_cert = os.path.join(local_root, 'cluster/certs', '{}.crt'.format(cert_name)) |
| remote_key = os.path.join(remote_root, '{}.key'.format(cert_name)) |
| remote_cert = os.path.join(remote_root, '{}.crt'.format(cert_name)) |
| |
| generate_cert = False |
| if not os.path.exists(local_key): |
| try: |
| decrypt('{}.key'.format(cert_name)) |
| except subprocess.CalledProcessError: |
| logger.info("{}/{}: generating key".format(fqdn, cert_name)) |
| subprocess.check_call([ |
| 'openssl', 'genrsa', '-out', local_key, '4096', |
| ]) |
| generate_cert = True |
| |
| if os.path.exists(local_cert): |
| with open(local_cert, 'rb') as f: |
| b = f.read() |
| cert = x509.load_pem_x509_certificate(b, default_backend()) |
| delta = cert.not_valid_after - datetime.datetime.now() |
| logger.info("{}/{}: existing cert expiry: {}".format(fqdn, cert_name, delta)) |
| if delta.total_seconds() < 3600 * 24 * 60: |
| logger.info("{}/{}: expires soon, regenerating".format(fqdn, cert_name)) |
| generate_cert = True |
| else: |
| generate_cert = True |
| |
| if not generate_cert: |
| return False |
| |
| local_csr_f = tempfile.NamedTemporaryFile(delete=False) |
| local_csr = local_csr_f.name |
| local_csr_f.close() |
| |
| local_config = openssl_config(san) |
| |
| subprocess.check_call([ |
| 'openssl', 'req', '-new', |
| '-key', local_key, |
| '-out', local_csr, |
| '-subj', str(subj), |
| '-config', local_config, |
| '-reqexts', 'SAN', |
| ]) |
| |
| pki.sign(local_csr, local_cert, local_config, days) |
| |
| c.put(local=local_key, remote=remote_key) |
| c.put(local=local_cert, remote=remote_cert) |
| |
| os.remove(local_csr) |
| os.remove(local_config) |
| return True |
| |
| |
| def configure_k8s(username, ca, cert, key): |
| subprocess.check_call([ |
| 'kubectl', 'config', |
| 'set-cluster', cluster, |
| '--certificate-authority=' + ca, |
| '--embed-certs=true', |
| '--server=https://' + cluster + ':4001', |
| ]) |
| subprocess.check_call([ |
| 'kubectl', 'config', |
| 'set-credentials', username, |
| '--client-certificate=' + cert, |
| '--client-key=' + key, |
| '--embed-certs=true', |
| ]) |
| subprocess.check_call([ |
| 'kubectl', 'config', |
| 'set-context', cluster, |
| '--cluster=' + cluster, |
| '--user=' + username, |
| ]) |
| subprocess.check_call([ |
| 'kubectl', 'config', |
| 'use-context', cluster, |
| ]) |
| |
| def admincreds(args): |
| if len(args) != 1: |
| sys.stderr.write("Usage: admincreds q3k\n") |
| return 1 |
| username = args[0] |
| |
| pki = PKI() |
| |
| local_key = os.path.join(local_root, '.kubectl/admin.key') |
| local_cert = os.path.join(local_root, '.kubectl/admin.crt') |
| local_csr = os.path.join(local_root, '.kubectl/admin.csr') |
| |
| generate_cert = False |
| if not os.path.exists(local_key): |
| subprocess.check_call([ |
| 'openssl', 'genrsa', '-out', local_key, '4096', |
| ]) |
| generate_cert = True |
| |
| if os.path.exists(local_cert): |
| with open(local_cert, 'rb') as f: |
| b = f.read() |
| cert = x509.load_pem_x509_certificate(b, default_backend()) |
| delta = cert.not_valid_after - datetime.datetime.now() |
| logger.info("admin: existing cert expiry: {}".format(delta)) |
| if delta.total_seconds() < 3600 * 24: |
| logger.info("admin: expires soon, regenerating") |
| generate_cert = True |
| else: |
| generate_cert = True |
| |
| if not generate_cert: |
| return configure_k8s(username, pki.cacert, local_cert, local_key) |
| |
| local_config = openssl_config([]) |
| subj = Subject('system:masters', "Kubernetes Admin Account for {}".format(username), username) |
| |
| subprocess.check_call([ |
| 'openssl', 'req', '-new', |
| '-key', local_key, |
| '-out', local_csr, |
| '-subj', str(subj), |
| '-config', local_config, |
| '-reqexts', 'SAN', |
| ]) |
| |
| pki.sign(local_csr, local_cert, local_config, 5) |
| os.remove(local_config) |
| |
| configure_k8s(username, pki.cacert, local_cert, local_key) |
| |
| |
| def nodestrap(args): |
| if len(args) != 1: |
| sys.stderr.write("Usage: nodestrap bc01n01.hswaw.net\n") |
| return 1 |
| fqdn = args[0] |
| |
| logger.info("Nodestrapping {}...".format(fqdn)) |
| |
| c = fabric.Connection('root@{}'.format(fqdn)) |
| p = PKI() |
| |
| modified = False |
| modified |= remote_cert(p, c, fqdn, "node", Subject(Subject.hswaw, 'Node Certificate', fqdn)) |
| modified |= remote_cert(p, c, fqdn, "kube-node", Subject('system:nodes', 'Kubelet Certificate', 'system:node:' + fqdn), san=[fqdn,]) |
| for component in ['controller-manager', 'proxy', 'scheduler']: |
| o = 'system:kube-{}'.format(component) |
| ou = 'Kuberneter Component {}'.format(component) |
| modified |= shared_cert(p, c, fqdn, 'kube-{}'.format(component), Subject(o, ou, o)) |
| modified |= shared_cert(p, c, fqdn, 'kube-apiserver', Subject(Subject.hswaw, 'Kubernetes API', cluster)) |
| modified |= shared_cert(p, c, fqdn, 'kube-serviceaccounts', Subject(Subject.hswaw, 'Kubernetes Service Account Signer', 'service-accounts')) |
| |
| if modified: |
| logger.info('{}: cert(s) modified, restarting services...'.format(fqdn)) |
| |
| services = [ |
| 'kubelet', 'kube-proxy', |
| 'kube-apiserver', 'kube-controller-manager', 'kube-scheduler', |
| 'etcd' |
| ] |
| |
| for s in services: |
| c.run('systemctl stop {}'.format(s)) |
| for s in services[::-1]: |
| c.run('systemctl start {}'.format(s)) |
| |
| def usage(): |
| sys.stderr.write("Usage: {} <nodestrap|admincreds>\n".format(sys.argv[0])) |
| |
| def main(): |
| if len(sys.argv) < 2: |
| usage() |
| return 1 |
| |
| mode = sys.argv[1] |
| if mode == "nodestrap": |
| return nodestrap(sys.argv[2:]) |
| elif mode == "admincreds": |
| return admincreds(sys.argv[2:]) |
| else: |
| usage() |
| return 1 |
| |
| if __name__ == '__main__': |
| sys.exit(main() or 0) |