diff --git a/doc/source/cli/cli.rst b/doc/source/cli/cli.rst index b93d6c10..d40c51e6 100644 --- a/doc/source/cli/cli.rst +++ b/doc/source/cli/cli.rst @@ -421,12 +421,36 @@ Usage: ./pegleg.sh site upload --context-marker= Site Secrets Group -================== +------------------ Subgroup of :ref:`site-group`. +A sub-group of site command group, which allows you to perform secrets +level operations for secrets documents of a site. + +.. note:: + + For the CLI commands ``encrypt``, ``decrypt``, ``generate-pki``, and ``wrap`` + in the ``secrets`` command + group, which encrypt or decrypt site secrets, two environment variables, + ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT``, are used to capture the + master passphrase, and the salt needed for encryption and decryption of the + site secrets. The contents of ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT`` + are not generated by Pegleg, but are created externally, and set by + deployment engineers or tooling. + + A minimum length of 24 for master passphrases will be checked by all CLI + commands, which use the ``PEGLEG_PASSPHRASE`` and ``PEGLEG_SALT``. + All other criteria around master passphrase strength are assumed to be + enforced elsewhere. + +:: + + ./pegleg.sh site -r -e secrets + + Generate PKI ------------- +^^^^^^^^^^^^ Generate certificates and keys according to all PKICatalog documents in the site using the PKI module. Regenerating certificates can be @@ -454,7 +478,7 @@ Dashes in the document names will be converted to underscores for consistency. Name of site. Examples -^^^^^^^^ +"""""""" :: @@ -472,31 +496,6 @@ Examples .. _command-line-repository-overrides: -Secrets -------- - -A sub-group of site command group, which allows you to perform secrets -level operations for secrets documents of a site. - -.. note:: - - For the CLI commands ``encrypt`` and ``decrypt`` in the ``secrets`` command - group, which encrypt or decrypt site secrets, two environment variables, - ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT``, are used to capture the - master passphrase, and the salt needed for encryption and decryption of the - site secrets. The contents of ``PEGLEG_PASSPHRASE``, and ``PEGLEG_SALT`` - are not generated by Pegleg, but are created externally, and set by a - deployment engineers or tooling. - - A minimum length of 24 for master passphrases will be checked by all CLI - commands, which use the ``PEGLEG_PASSPHRASE``. All other criteria around - master passphrase strength are assumed to be enforced elsewhere. - -:: - - ./pegleg.sh site -r -e secrets - - Encrypt ^^^^^^^ @@ -612,6 +611,58 @@ Example: secrets decrypt site1 -f \ /opt/security-manifests/site/site1/passwords/password1.yaml +Wrap +^^^^ + +Wrap bare files (e.g. pem or crt) in a PeglegManagedDocument and optionally encrypt them. + +**site_name** (Required). + +Name of site. + +**-a / --author** + +Identifying name of the author generating new certificates. Used +for tracking provenance information in the PeglegManagedDocuments. +An attempt is made to automatically determine this value, +but should be provided. + +**-f / --filename** + +The relative path to the file to be wrapped. + +**-o / --output-path** + +The output path for the wrapped file. (default: input path with the extension +replaced with .yaml) + +**-s / --schema** + +The schema for the document to be wrapped, e.g. deckhand/Certificate/v1 + +**-n / --name** + +The name for the document to be wrapped, e.g. new-cert. + +**-l / --layer** + +The layer for the document to be wrapped, e.g. site. + +**--encrypt / --no-encrypt** + +A flag specifying whether to encrypt the output file. (default: True) + +Examples +"""""""" + +:: + + ./pegleg.sh site -r /home/myuser/myrepo \ + secrets wrap -a myuser -f secrets/certificates/new_cert.crt \ + -o secrets/certificates/new_cert.yaml -s "deckhand/Certificate/v1" \ + -n "new-cert" -l site mysite + + genesis_bundle -------------- diff --git a/pegleg/cli.py b/pegleg/cli.py index f7275d35..1467dbca 100644 --- a/pegleg/cli.py +++ b/pegleg/cli.py @@ -23,6 +23,7 @@ from pegleg import config from pegleg import engine from pegleg.engine import bundle from pegleg.engine import catalog +from pegleg.engine.secrets import wrap_secret from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement from pegleg.engine.util.shipyard_helper import ShipyardHelper @@ -415,6 +416,62 @@ def generate_pki(site_name, author): click.echo("Generated PKI files written to:\n%s" % '\n'.join(output_paths)) +@secrets.command( + 'wrap', + help='Wrap bare files (e.g. pem or crt) in a PeglegManagedDocument ' + 'and encrypt them (by default).') +@click.option( + '-a', + '--author', + 'author', + help='Author for the new wrapped file.') +@click.option( + '-f', + '--filename', + 'file_name', + help='The relative file path for the file to be wrapped.') +@click.option( + '-o', + '--output-path', + 'output_path', + required=False, + help='The output path for the wrapped file. (default: input path with ' + '.yaml)') +@click.option( + '-s', + '--schema', + 'schema', + help='The schema for the document to be wrapped, e.g. ' + 'deckhand/Certificate/v1') +@click.option( + '-n', + '--name', + 'name', + help='The name for the document to be wrapped, e.g. new-cert') +@click.option( + '-l', + '--layer', + 'layer', + help='The layer for the document to be wrapped., e.g. site.') +@click.option( + '--encrypt/--no-encrypt', + 'encrypt', + is_flag=True, + default=True, + help='Whether to encrypt the wrapped file (default: True).') +@click.argument('site_name') +def wrap_secret_cli(*, site_name, author, file_name, output_path, schema, + name, layer, encrypt): + """Wrap a bare secrets file in a YAML and ManagedDocument. + + """ + + engine.repository.process_repositories(site_name, + overwrite_existing=True) + wrap_secret(author, file_name, output_path, schema, + name, layer, encrypt) + + @site.command( 'genesis_bundle', help='Construct the genesis deployment bundle.') diff --git a/pegleg/engine/secrets.py b/pegleg/engine/secrets.py index 5cc20927..b4c79988 100644 --- a/pegleg/engine/secrets.py +++ b/pegleg/engine/secrets.py @@ -14,11 +14,14 @@ import logging import os +import yaml from pegleg.engine.generators.passphrase_generator import PassphraseGenerator from pegleg.engine.util.cryptostring import CryptoString from pegleg.engine.util import definition from pegleg.engine.util import files +from pegleg.engine.util.pegleg_managed_document import \ + PeglegManagedSecretsDocument as PeglegManagedSecret from pegleg.engine.util.pegleg_secret_management import PeglegSecretManagement __all__ = ('encrypt', 'decrypt', 'generate_passphrases') @@ -141,3 +144,45 @@ def generate_crypto_string(length): """ return CryptoString().get_crypto_string(length) + + +def wrap_secret(author, file_name, output_path, schema, + name, layer, encrypt): + """Wrap a bare secrets file in a YAML and ManagedDocument. + + :param author: author for ManagedDocument + :param file_name: file path for input file + :param output_path: file path for output file + :param schema: schema for wrapped document + :param name: name for wrapped document + :param layer: layer for wrapped document + :param encrypt: whether to encrypt the output doc + """ + + if not output_path: + output_path = os.path.splitext(file_name)[0] + ".yaml" + + with open(file_name, "r") as in_fi: + data = in_fi.read() + + inner_doc = { + "schema": schema, + "data": data, + "metadata": { + "layeringDefinition": { + "abstract": False, + "layer": layer + }, + "name": name, + "schema": "metadata/Document/v1", + "storagePolicy": "encrypted" if encrypt else "cleartext" + } + } + managed_secret = PeglegManagedSecret(inner_doc, author=author) + if encrypt: + psm = PeglegSecretManagement(docs=[inner_doc], author=author) + output_doc = psm.get_encrypted_secrets()[0][0] + else: + output_doc = managed_secret.pegleg_document + with open(output_path, "w") as output_fi: + yaml.safe_dump(output_doc, output_fi) diff --git a/tests/unit/test_cli.py b/tests/unit/test_cli.py index 68ab8841..4202d856 100644 --- a/tests/unit/test_cli.py +++ b/tests/unit/test_cli.py @@ -36,6 +36,16 @@ TEST_PARAMS = { "repo_url": "https://github.com/openstack/airship-treasuremap.git", } +test_cert = """ +-----BEGIN CERTIFICATE----- + +DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF +DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF +DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF +DEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEFDEADBEEF + +-----END CERTIFICATE----- +""" @pytest.mark.skipif( not test_utils.is_connected(), @@ -552,6 +562,53 @@ class TestSiteSecretsActions(BaseCLIActionTest): result = self.runner.invoke(cli.site, ['-r', repo_path] + secrets_opts) assert result.exit_code == 0, result.output + @mock.patch.dict(os.environ, { + "PEGLEG_PASSPHRASE": "123456789012345678901234567890", + "PEGLEG_SALT": "123456" + }) + def test_site_secrets_wrap(self): + """Validates ``generate-pki`` action using local repo path.""" + # Scenario: + # + # 1) Encrypt a file in a local repo + + repo_path = self.treasuremap_path + file_dir = os.path.join(repo_path, "site", "airship-seaworthy", + "secrets", "certificates") + file_path = os.path.join(file_dir, "test.crt") + output_path = os.path.join(file_dir, "test.yaml") + + with open(file_path, "w") as test_crt_fi: + test_crt_fi.write(test_cert) + secrets_opts = ['secrets', 'wrap', "-a", "lm734y", "-f", file_path, + "-s", "deckhand/Certificate/v1", + "-n", "test-certificate", "-l", "site", "--no-encrypt", + self.site_name] + result = self.runner.invoke(cli.site, ["-r", repo_path] + secrets_opts) + assert result.exit_code == 0 + + with open(output_path, "r") as output_fi: + doc = yaml.safe_load(output_fi) + assert doc["data"]["managedDocument"]["data"] == test_cert + assert doc["data"]["managedDocument"]["schema"] == "deckhand/Certificate/v1" + assert doc["data"]["managedDocument"]["metadata"]["name"] == "test-certificate" + assert doc["data"]["managedDocument"]["metadata"]["layeringDefinition"]["layer"] == "site" + assert doc["data"]["managedDocument"]["metadata"]["storagePolicy"] == "cleartext" + + os.remove(output_path) + secrets_opts = ['secrets', 'wrap', "-a", "lm734y", "-f", file_path, + "-o", output_path, "-s", "deckhand/Certificate/v1", + "-n", "test-certificate", "-l", "site", + self.site_name] + result = self.runner.invoke(cli.site, ["-r", repo_path] + secrets_opts) + assert result.exit_code == 0 + + with open(output_path, "r") as output_fi: + doc = yaml.safe_load(output_fi) + assert "encrypted" in doc["data"] + assert "managedDocument" in doc["data"] + + class TestTypeCliActions(BaseCLIActionTest): """Tests type-level CLI actions."""