From 7018d5941cb193f90d2ffc1e6d3a8b79aea895e6 Mon Sep 17 00:00:00 2001 From: Alexander Hughes Date: Wed, 17 Jul 2019 12:24:41 -0500 Subject: [PATCH] Support regenerating PKI This patch adds functionality Pegleg currently lacks: the ability to regenerate expired certificates. This patch adds: 1. CLI toggle --regenerate-all to generate_pki. Default is False, which means if no certificates are present, generate what is in the pki catalogue. If new certs have been added to the catalogue generate just those. If the --regenerate-all flag is True, then Pegleg will ignore any existing certs and regenerate (or generate for the first time) all certificates defined in the PKI catalogue. 2. Documentation updates for CLI change. 3. Updates to pki_utility to accomodate the new flag. 4. Updates pki_generator methods to use rendered documents to accommodate documents that have to be layered. 5. Updates pki_generator unit tests to include a layering definition which is now required to run the commands. Change-Id: I2d8086770e9226e44598ef40eca790981279f626 --- doc/source/cli/cli.rst | 16 +++++-- pegleg/cli.py | 19 ++++++-- pegleg/engine/catalog/pki_generator.py | 16 ++++--- pegleg/engine/site.py | 43 +++++++++++-------- .../unit/engine/catalog/test_pki_generator.py | 14 ++++++ 5 files changed, 78 insertions(+), 30 deletions(-) diff --git a/doc/source/cli/cli.rst b/doc/source/cli/cli.rst index 9cb80acc..63477d0e 100644 --- a/doc/source/cli/cli.rst +++ b/doc/source/cli/cli.rst @@ -473,8 +473,13 @@ Generate PKI ^^^^^^^^^^^^ Generate certificates and keys according to all PKICatalog documents in the -site using the PKI module. Regenerating certificates can be -accomplished by re-running this command. +site using the PKI module. The default behavior is to generate all +certificates that are not yet present. For example, the first time generate PKI +is run or when new entries are added to the PKICatalogue, only those new +entries will be generated on subsequent runs. + +Pegleg also supports a full regeneration of all certificates at any time, by +using the --regenerate-all flag. Pegleg places generated document files in ``/secrets/passphrases``, ``/secrets/certificates``, or ``/secrets/keypairs`` as @@ -511,6 +516,10 @@ Minimum=0, no maximum. Values less than 0 will raise an exception. NOTE: A generated certificate where days = 0 should only be used for testing. A certificate generated in such a way will be valid for 0 seconds. +**--regenerate-all** (Optional, Default=False). + +Force Pegleg to regenerate all PKI items. + Examples """""""" @@ -520,7 +529,8 @@ Examples secrets generate-pki \ \ -a \ - -d + -d \ + --regenerate-all .. _command-line-repository-overrides: diff --git a/pegleg/cli.py b/pegleg/cli.py index 633c7c91..de04446b 100644 --- a/pegleg/cli.py +++ b/pegleg/cli.py @@ -413,9 +413,13 @@ def secrets(): @secrets.command( 'generate-pki', + short_help='Generate certs and keys according to the site PKICatalog', help='Generate certificates and keys according to all PKICatalog ' - 'documents in the site. Regenerating certificates can be ' - 'accomplished by re-running this command.') + 'documents in the site using the PKI module. The default behavior is ' + 'to generate all certificates that are not yet present. For example, ' + 'the first time generate PKI is run or when new entries are added ' + 'to the PKICatalogue, only those new entries will be generated on ' + 'subsequent runs.') @click.option( '-a', '--author', @@ -431,8 +435,15 @@ def secrets(): default=365, show_default=True, help='Duration in days generated certificates should be valid.') +@click.option( + '--regenerate-all', + 'regenerate_all', + is_flag=True, + default=False, + show_default=True, + help='Force Pegleg to regenerate all PKI items.') @click.argument('site_name') -def generate_pki(site_name, author, days): +def generate_pki(site_name, author, days, regenerate_all): """Generate certificates, certificate authorities and keypairs for a given site. @@ -440,7 +451,7 @@ def generate_pki(site_name, author, days): engine.repository.process_repositories(site_name, overwrite_existing=True) pkigenerator = catalog.pki_generator.PKIGenerator( - site_name, author=author, duration=days) + site_name, author=author, duration=days, regenerate_all=regenerate_all) output_paths = pkigenerator.generate() click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths)) diff --git a/pegleg/engine/catalog/pki_generator.py b/pegleg/engine/catalog/pki_generator.py index 1c796780..d4990089 100644 --- a/pegleg/engine/catalog/pki_generator.py +++ b/pegleg/engine/catalog/pki_generator.py @@ -21,6 +21,7 @@ from pegleg import config from pegleg.engine.catalog import pki_utility from pegleg.engine.common import managed_document as md from pegleg.engine import exceptions +from pegleg.engine import site from pegleg.engine import util from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement @@ -42,7 +43,12 @@ class PKIGenerator(object): """ def __init__( - self, sitename, block_strings=True, author=None, duration=365): + self, + sitename, + block_strings=True, + author=None, + duration=365, + regenerate_all=False): """Constructor for ``PKIGenerator``. :param int duration: Duration in days that generated certificates @@ -53,11 +59,12 @@ class PKIGenerator(object): block-style YAML string. Defaults to true. :param str author: Identifying name of the author generating new certificates. - + :param bool regenerate_all: If Pegleg should regenerate all certs. """ + self._regenerate_all = regenerate_all self._sitename = sitename - self._documents = util.definition.documents_for_site(sitename) + self._documents = site.get_rendered_docs(sitename) self._author = author self.keys = pki_utility.PKIUtility( @@ -126,11 +133,10 @@ class PKIGenerator(object): def _get_or_gen(self, generator, kinds, document_name, *args, **kwargs): docs = self._find_docs(kinds, document_name) - if not docs: + if not docs or self._regenerate_all: docs = generator(document_name, *args, **kwargs) else: docs = PeglegSecretManagement(docs=docs) - # Adding these to output should be idempotent, so we use a dict. for wrapper_doc in docs: diff --git a/pegleg/engine/site.py b/pegleg/engine/site.py index be52d062..66fe2c7f 100644 --- a/pegleg/engine/site.py +++ b/pegleg/engine/site.py @@ -106,24 +106,7 @@ def collect(site_name, save_location): def render(site_name, output_stream, validate): - documents = [] - # Ignore YAML tags, only construct dicts - SafeConstructor.add_multi_constructor( - '', lambda loader, suffix, node: None) - for filename in util.definition.site_files(site_name): - with open(filename, 'r') as f: - documents.extend(list(yaml.safe_load_all(f))) - - rendered_documents, errors = util.deckhand.deckhand_render( - documents=documents, validate=validate) - err_msg = '' - if errors: - for err in errors: - if isinstance(err, tuple) and len(err) > 1: - err_msg += ': '.join(err) + '\n' - else: - err_msg += str(err) + '\n' - raise click.ClickException(err_msg) + rendered_documents = get_rendered_docs(site_name, validate=validate) if output_stream: files.dump_all( @@ -142,6 +125,30 @@ def render(site_name, output_stream, validate): explicit_end=True)) +def get_rendered_docs(site_name, validate=True): + documents = [] + # Ignore YAML tags, only construct dicts + SafeConstructor.add_multi_constructor( + '', lambda loader, suffix, node: None) + for filename in util.definition.site_files(site_name): + with open(filename, 'r') as f: + documents.extend(list(yaml.safe_load_all(f))) + + rendered_documents, errors = util.deckhand.deckhand_render( + documents=documents, validate=validate) + + if errors: + err_msg = '' + for err in errors: + if isinstance(err, tuple) and len(err) > 1: + err_msg += ': '.join(err) + '\n' + else: + err_msg += str(err) + '\n' + raise click.ClickException(err_msg) + + return rendered_documents + + def list_(output_stream): """List site names for a given repository.""" diff --git a/tests/unit/engine/catalog/test_pki_generator.py b/tests/unit/engine/catalog/test_pki_generator.py index 74398cfb..047ec7cb 100644 --- a/tests/unit/engine/catalog/test_pki_generator.py +++ b/tests/unit/engine/catalog/test_pki_generator.py @@ -63,6 +63,18 @@ _SITE_DEFINITION = textwrap.dedent( ... """) +_LAYERING_DEFINITION = textwrap.dedent( + """ + --- + schema: deckhand/LayeringPolicy/v1 + metadata: + schema: metadata/Control/v1 + name: layering-policy + data: + layerOrder: + - site + """) + _CA_KEY_NAME = "kubernetes" _CERT_KEY_NAME = "kubelet-n3" _KEYPAIR_KEY_NAME = "service-account" @@ -192,6 +204,8 @@ def create_tmp_pki_structure(tmpdir): test_structure = copy.deepcopy(_SITE_TEST_STRUCTURE) test_structure['files']['site-definition.yaml'] = yaml.safe_load( site_definition) + test_structure['files']['layering-definition.yaml'] = yaml.safe_load( + _LAYERING_DEFINITION) test_structure['directories']['pki']['files'][ 'pki-catalog.yaml'] = yaml.safe_load(pki_catalog)