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
This commit is contained in:
Alexander Hughes 2019-07-17 12:24:41 -05:00
parent ccd05998b2
commit 7018d5941c
5 changed files with 78 additions and 30 deletions

View File

@ -473,8 +473,13 @@ Generate PKI
^^^^^^^^^^^^ ^^^^^^^^^^^^
Generate certificates and keys according to all PKICatalog documents in the Generate certificates and keys according to all PKICatalog documents in the
site using the PKI module. Regenerating certificates can be site using the PKI module. The default behavior is to generate all
accomplished by re-running this command. 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 ``<site>/secrets/passphrases``, Pegleg places generated document files in ``<site>/secrets/passphrases``,
``<site>/secrets/certificates``, or ``<site>/secrets/keypairs`` as ``<site>/secrets/certificates``, or ``<site>/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. 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. 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 Examples
"""""""" """"""""
@ -520,7 +529,8 @@ Examples
secrets generate-pki \ secrets generate-pki \
<site_name> \ <site_name> \
-a <author> \ -a <author> \
-d <days> -d <days> \
--regenerate-all
.. _command-line-repository-overrides: .. _command-line-repository-overrides:

View File

@ -413,9 +413,13 @@ def secrets():
@secrets.command( @secrets.command(
'generate-pki', 'generate-pki',
short_help='Generate certs and keys according to the site PKICatalog',
help='Generate certificates and keys according to all PKICatalog ' help='Generate certificates and keys according to all PKICatalog '
'documents in the site. Regenerating certificates can be ' 'documents in the site using the PKI module. The default behavior is '
'accomplished by re-running this command.') '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( @click.option(
'-a', '-a',
'--author', '--author',
@ -431,8 +435,15 @@ def secrets():
default=365, default=365,
show_default=True, show_default=True,
help='Duration in days generated certificates should be valid.') 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') @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 """Generate certificates, certificate authorities and keypairs for a given
site. site.
@ -440,7 +451,7 @@ def generate_pki(site_name, author, days):
engine.repository.process_repositories(site_name, overwrite_existing=True) engine.repository.process_repositories(site_name, overwrite_existing=True)
pkigenerator = catalog.pki_generator.PKIGenerator( 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() output_paths = pkigenerator.generate()
click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths)) click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths))

View File

@ -21,6 +21,7 @@ from pegleg import config
from pegleg.engine.catalog import pki_utility from pegleg.engine.catalog import pki_utility
from pegleg.engine.common import managed_document as md from pegleg.engine.common import managed_document as md
from pegleg.engine import exceptions from pegleg.engine import exceptions
from pegleg.engine import site
from pegleg.engine import util from pegleg.engine import util
from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement
@ -42,7 +43,12 @@ class PKIGenerator(object):
""" """
def __init__( 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``. """Constructor for ``PKIGenerator``.
:param int duration: Duration in days that generated certificates :param int duration: Duration in days that generated certificates
@ -53,11 +59,12 @@ class PKIGenerator(object):
block-style YAML string. Defaults to true. block-style YAML string. Defaults to true.
:param str author: Identifying name of the author generating new :param str author: Identifying name of the author generating new
certificates. certificates.
:param bool regenerate_all: If Pegleg should regenerate all certs.
""" """
self._regenerate_all = regenerate_all
self._sitename = sitename self._sitename = sitename
self._documents = util.definition.documents_for_site(sitename) self._documents = site.get_rendered_docs(sitename)
self._author = author self._author = author
self.keys = pki_utility.PKIUtility( self.keys = pki_utility.PKIUtility(
@ -126,11 +133,10 @@ class PKIGenerator(object):
def _get_or_gen(self, generator, kinds, document_name, *args, **kwargs): def _get_or_gen(self, generator, kinds, document_name, *args, **kwargs):
docs = self._find_docs(kinds, document_name) docs = self._find_docs(kinds, document_name)
if not docs: if not docs or self._regenerate_all:
docs = generator(document_name, *args, **kwargs) docs = generator(document_name, *args, **kwargs)
else: else:
docs = PeglegSecretManagement(docs=docs) docs = PeglegSecretManagement(docs=docs)
# Adding these to output should be idempotent, so we use a dict. # Adding these to output should be idempotent, so we use a dict.
for wrapper_doc in docs: for wrapper_doc in docs:

View File

@ -106,24 +106,7 @@ def collect(site_name, save_location):
def render(site_name, output_stream, validate): def render(site_name, output_stream, validate):
documents = [] rendered_documents = get_rendered_docs(site_name, validate=validate)
# 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)
if output_stream: if output_stream:
files.dump_all( files.dump_all(
@ -142,6 +125,30 @@ def render(site_name, output_stream, validate):
explicit_end=True)) 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): def list_(output_stream):
"""List site names for a given repository.""" """List site names for a given repository."""

View File

@ -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" _CA_KEY_NAME = "kubernetes"
_CERT_KEY_NAME = "kubelet-n3" _CERT_KEY_NAME = "kubelet-n3"
_KEYPAIR_KEY_NAME = "service-account" _KEYPAIR_KEY_NAME = "service-account"
@ -192,6 +204,8 @@ def create_tmp_pki_structure(tmpdir):
test_structure = copy.deepcopy(_SITE_TEST_STRUCTURE) test_structure = copy.deepcopy(_SITE_TEST_STRUCTURE)
test_structure['files']['site-definition.yaml'] = yaml.safe_load( test_structure['files']['site-definition.yaml'] = yaml.safe_load(
site_definition) site_definition)
test_structure['files']['layering-definition.yaml'] = yaml.safe_load(
_LAYERING_DEFINITION)
test_structure['directories']['pki']['files'][ test_structure['directories']['pki']['files'][
'pki-catalog.yaml'] = yaml.safe_load(pki_catalog) 'pki-catalog.yaml'] = yaml.safe_load(pki_catalog)