promenade/promenade/pki.py

289 lines
8.2 KiB
Python

from . import config, logging
import json
import os
import shutil
import subprocess
import tempfile
import yaml
__all__ = ['PKI']
LOG = logging.getLogger(__name__)
class PKI:
def __init__(self, cluster_name, *, ca_config=None):
self.certificate_authorities = {}
self.cluster_name = cluster_name
self._ca_config_string = None
if ca_config:
self._ca_config_string = json.dumps(ca_config)
@property
def ca_config(self):
if not self._ca_config_string:
self._ca_config_string = json.dumps({
'signing': {
'default': {
'expiry': '8760h',
'usages': ['signing', 'key encipherment', 'server auth', 'client auth'],
},
},
})
return self._ca_config_string
def generate_ca(self, *, ca_name, cert_target, key_target):
result = self._cfssl(['gencert', '-initca', 'csr.json'],
files={
'csr.json': self.csr(
name='Kubernetes',
groups=['Kubernetes']),
})
LOG.debug('ca_cert=%r', result['cert'])
self.certificate_authorities[ca_name] = result
return (self._wrap('CertificateAuthority', result['cert'],
name=ca_name,
target=cert_target),
self._wrap('CertificateAuthorityKey', result['key'],
name=ca_name,
target=key_target))
def generate_keypair(self, *, alias=None, name, target):
priv_result = self._openssl(['genrsa', '-out', 'priv.pem'])
pub_result = self._openssl(['rsa', '-in', 'priv.pem', '-pubout', '-out', 'pub.pem'],
files={
'priv.pem': priv_result['priv.pem'],
})
if not alias:
alias = name
return (self._wrap('PublicKey', pub_result['pub.pem'],
name=alias,
target=target),
self._wrap('PrivateKey', priv_result['priv.pem'],
name=alias,
target=target))
def generate_certificate(self, *, alias=None, ca_name, groups=[], hosts=[], name, target):
result = self._cfssl(
['gencert',
'-ca', 'ca.pem',
'-ca-key', 'ca-key.pem',
'-config', 'ca-config.json',
'csr.json'],
files={
'ca-config.json': self.ca_config,
'ca.pem': self.certificate_authorities[ca_name]['cert'],
'ca-key.pem': self.certificate_authorities[ca_name]['key'],
'csr.json': self.csr(name=name, groups=groups, hosts=hosts),
})
if not alias:
alias = name
return (self._wrap('Certificate', result['cert'],
name=alias,
target=target),
self._wrap('CertificateKey', result['key'],
name=alias,
target=target))
def csr(self, *, name, groups=[], hosts=[], key={'algo': 'rsa', 'size': 2048}):
return json.dumps({
'CN': name,
'key': key,
'hosts': hosts,
'names': [{'O': g} for g in groups],
})
def _cfssl(self, command, *, files=None):
if not files:
files = {}
with tempfile.TemporaryDirectory() as tmp:
for filename, data in files.items():
with open(os.path.join(tmp, filename), 'w') as f:
f.write(data)
return json.loads(subprocess.check_output(
['cfssl'] + command, cwd=tmp))
def _openssl(self, command, *, files=None):
if not files:
files = {}
with tempfile.TemporaryDirectory() as tmp:
for filename, data in files.items():
with open(os.path.join(tmp, filename), 'w') as f:
f.write(data)
subprocess.check_call(['openssl'] + command, cwd=tmp)
result = {}
for filename in os.listdir(tmp):
if filename not in files:
with open(os.path.join(tmp, filename)) as f:
result[filename] = f.read()
return result
def _wrap(self, kind, data, **metadata):
return config.Document({
'apiVersion': 'promenade/v1',
'kind': kind,
'metadata': {
'cluster': self.cluster_name,
**metadata,
},
'spec': {
'data': block_literal(data),
},
})
class block_literal(str): pass
def block_literal_representer(dumper, data):
return dumper.represent_scalar(u'tag:yaml.org,2002:str', data, style='|')
yaml.add_representer(block_literal, block_literal_representer)
CA_ONLY_MAP = {
'cluster-ca': [
'kubelet',
],
}
FULL_DISTRIBUTION_MAP = {
'apiserver': [
'apiserver',
],
'apiserver-key': [
'apiserver',
],
'controller-manager': [
'controller-manager',
],
'controller-manager-key': [
'controller-manager',
],
'kubelet': [
'kubelet',
],
'kubelet-key': [
'kubelet',
],
'proxy': [
'proxy',
],
'proxy-key': [
'proxy',
],
'scheduler': [
'scheduler',
],
'scheduler-key': [
'scheduler',
],
'cluster-ca': [
'admin',
'apiserver',
'asset-loader',
'controller-manager',
'etcd',
'genesis',
'kubelet',
'proxy',
'scheduler',
],
'cluster-ca-key': [
'controller-manager',
],
'sa': [
'apiserver',
],
'sa-key': [
'controller-manager',
],
'etcd': [
'etcd',
],
'etcd-key': [
'etcd',
],
'admin': [
'admin',
],
'admin-key': [
'admin',
],
'asset-loader': [
'asset-loader',
],
'asset-loader-key': [
'asset-loader',
],
'genesis': [
'genesis',
],
'genesis-key': [
'genesis',
],
}
def generate_keys(*, initial_pki, target_dir):
if os.path.exists(os.path.join(target_dir, 'etc/kubernetes/cfssl')):
with tempfile.TemporaryDirectory() as tmp:
_write_initial_pki(tmp, initial_pki)
_generate_certs(tmp, target_dir)
_distribute_files(tmp, target_dir, FULL_DISTRIBUTION_MAP)
def _write_initial_pki(tmp, initial_pki):
for filename, data in initial_pki.items():
path = os.path.join(tmp, filename + '.pem')
with open(path, 'w') as f:
LOG.debug('Writing data for "%s" to path "%s"', filename, path)
f.write(data)
def _generate_certs(dest, target):
ca_config_path = os.path.join(target, 'etc/kubernetes/cfssl/ca-config.json')
ca_path = os.path.join(dest, 'cluster-ca.pem')
ca_key_path = os.path.join(dest, 'cluster-ca-key.pem')
search_dir = os.path.join(target, 'etc/kubernetes/cfssl/csr-configs')
for filename in os.listdir(search_dir):
name, _ext = os.path.splitext(filename)
LOG.info('Generating cert for %s', name)
path = os.path.join(search_dir, filename)
cfssl_result = subprocess.check_output([
'cfssl', 'gencert', '-ca', ca_path, '-ca-key', ca_key_path,
'-config', ca_config_path, '-profile', 'kubernetes', path])
subprocess.run(['cfssljson', '-bare', name], cwd=dest,
input=cfssl_result, check=True)
def _distribute_files(src, dest, distribution_map):
for filename, destinations in distribution_map.items():
src_path = os.path.join(src, filename + '.pem')
if os.path.exists(src_path):
for destination in destinations:
dest_dir = os.path.join(dest, 'etc/kubernetes/%s/pki' % destination)
os.makedirs(dest_dir, exist_ok=True)
shutil.copy(src_path, dest_dir)