*: 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/tools/BUILD b/tools/BUILD
index cd4642a..2be1cea 100644
--- a/tools/BUILD
+++ b/tools/BUILD
@@ -2,8 +2,8 @@
 load("@py_deps//:requirements.bzl", "requirement")
 load("//bzl:rules.bzl", "copy_go_binary")
 
-py_binary(
-    name = "secretstore",
+py_library(
+    name = "secretstore_lib",
     srcs = ["secretstore.py"],
     visibility = ["//visibility:public"],
     deps = [
@@ -12,11 +12,10 @@
 )
 
 py_binary(
-    name = "clustercfg",
-    srcs = ["clustercfg.py"],
+    name = "secretstore",
+    srcs = ["secretstore.py"],
     visibility = ["//visibility:public"],
     deps = [
-        requirement("fabric"),
         requirement("future"),
     ],
 )
diff --git a/tools/clustercfg.py b/tools/clustercfg.py
deleted file mode 100644
index dda4161..0000000
--- a/tools/clustercfg.py
+++ /dev/null
@@ -1,360 +0,0 @@
-#!/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, san=[]):
-        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,
-            '-days', str(days),
-        ] + ([
-            '-extensions', 'SAN', '-extfile', conf,
-        ] if san else []))
-
-
-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())
-
-    if san:
-        config.seek(0, 2)
-        config.write(b'\n[SAN]\n')
-        config.write(b'subjectAltName = @alt_names\n')
-        config.write(b'basicConstraints = CA:FALSE\nkeyUsage = nonRepudiation, digitalSignature, keyEncipherment\n')
-        config.write(b'[alt_names]\n')
-
-        ipcnt = 1
-        dnscnt = 1
-        for s in san:
-            parts = s.split(':')
-            if s.startswith('DNS'):
-                config.write('DNS.{} = {}\n'.format(dnscnt, parts[1]).encode())
-                dnscnt += 1
-            elif s.startswith('IP'):
-                config.write('IP.{} = {}\n'.format(ipcnt, parts[1]).encode())
-                ipcnt += 1
-
-    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("""nix-shell -p openssl --command "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)
-
-    sanconf = ""
-    if san:
-        sanconf = "-reqexts SAN"
-    c.run("""
-        nix-shell -p openssl --command "openssl req -new -key {remote_key} -out {remote_csr} -subj '{subj}' -config {remote_config} {sanconf}"
-    """.format(remote_key=remote_key, remote_csr=remote_csr, subj=str(subj), remote_config=remote_config, sanconf=sanconf))
-
-    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, san)
-
-    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 generate_cert:
-        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',
-        ] if san else []))
-
-        pki.sign(local_csr, local_cert, local_config, days, san)
-        os.remove(local_csr)
-        os.remove(local_config)
-
-    c.put(local=local_key, remote=remote_key)
-    c.put(local=local_cert, remote=remote_cert)
-
-    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')
-
-    kubectl = os.path.join(local_root, '.kubectl')
-    if not os.path.exists(kubectl):
-        os.mkdir(kubectl)
-
-    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)
-
-    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),
-    ])
-
-    pki.sign(local_csr, local_cert, None, 5, [])
-
-    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()
-
-    local_cacert = os.path.join(local_root, 'cluster/certs/ca.crt')
-    remote_cacert = os.path.join(remote_root, 'ca.crt')
-    c.put(local=local_cacert, remote=remote_cacert)
-
-    remote_cert(p, c, fqdn, "node", Subject(Subject.hswaw, 'Node Certificate', fqdn))
-    remote_cert(p, c, fqdn, "kube-node", Subject('system:nodes', 'Kubelet Certificate', 'system:node:' + fqdn), san=["DNS:"+fqdn,])
-    for component in ['controller-manager', 'proxy', 'scheduler']:
-        o = 'system:kube-{}'.format(component)
-        ou = 'Kuberneter Component {}'.format(component)
-        shared_cert(p, c, fqdn, 'kube-{}'.format(component), Subject(o, ou, o))
-    shared_cert(p, c, fqdn, 'kube-apiserver', Subject(Subject.hswaw, 'Kubernetes API', cluster), san=['IP:10.10.12.1', 'DNS:' + cluster])
-    shared_cert(p, c, fqdn, 'kube-serviceaccounts', Subject(Subject.hswaw, 'Kubernetes Service Account Signer', 'service-accounts'))
-    shared_cert(p, c, fqdn, 'kube-calico', Subject(Subject.hswaw, 'Kubernetes Calico Account', 'calico'))
-
-    #c.run('nixos-rebuild switch')
-
-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)
diff --git a/tools/install.sh b/tools/install.sh
index 82713a2..6a9bec1 100755
--- a/tools/install.sh
+++ b/tools/install.sh
@@ -8,5 +8,5 @@
 cd "${hscloud_root}"
 
 bazel build \
-        //tools:kubectl //tools:kubecfg //tools:clustercfg //tools:secretstore \
+        //tools:kubectl //tools:kubecfg //tools:secretstore \
         //tools:pass //tools:calicoctl.bin
diff --git a/tools/secretstore.py b/tools/secretstore.py
index 1d18142..fc5b3a2 100644
--- a/tools/secretstore.py
+++ b/tools/secretstore.py
@@ -2,6 +2,8 @@
 
 # A little tool to encrypt/decrypt git secrets. Kinda like password-store, but more purpose specific and portable.
 
+import logging
+import os
 import sys
 import subprocess
 
@@ -9,9 +11,13 @@
     "63DFE737F078657CC8A51C00C29ADD73B3563D82", # q3k
     "482FF104C29294AD1CAF827BA43890A3DE74ECC7", # inf
     "F07205946C07EEB2041A72FBC60C64879534F768", # cz2
-    "0879F9FCA1C836677BB808C870FD60197E195C26", # implr
+    #"0879F9FCA1C836677BB808C870FD60197E195C26", # implr (expired 2019-02-09)
 ]
 
+
+logger = logging.getLogger(__name__)
+
+
 def encrypt(src, dst):
     cmd = ['gpg' , '--encrypt', '--armor', '--batch', '--yes', '--output', dst]
     for k in keys:
@@ -24,6 +30,40 @@
     cmd = ['gpg', '--decrypt', '--output', dst, src]
     subprocess.check_call(cmd)
 
+
+class SecretStoreMissing(Exception):
+    pass
+
+
+class SecretStore(object):
+    def __init__(self, plain_root, cipher_root):
+        self.proot = plain_root
+        self.croot = cipher_root
+
+    def exists(self, suffix):
+        p = os.path.join(self.proot, suffix)
+        c = os.path.join(self.croot, suffix)
+        return os.path.exists(c) or os.path.exists(p)
+
+    def plaintext(self, suffix):
+        return os.path.join(self.proot, suffix)
+
+    def open(self, suffix, mode, *a, **kw):
+        p = os.path.join(self.proot, suffix)
+        c = os.path.join(self.croot, suffix)
+        if 'w' in mode:
+            return open(p, mode, *a, *kw)
+
+        if not self.exists(suffix):
+            raise SecretStoreMissing("Secret {} does not exist".format(suffix))
+
+        if not os.path.exists(p) or os.path.getctime(p) < os.path.getctime(c):
+            logger.info("Decrypting {} ({})...".format(suffix, c))
+            decrypt(c, p)
+
+        return open(p, mode, *a, **kw)
+
+
 def main():
     if len(sys.argv) < 3 or sys.argv[1] not in ('encrypt', 'decrypt'):
         sys.stderr.write("Usage: {} encrypt/decrypt file\n".format(sys.argv[0]))