From 55623028df6e2c2ca3788a85afec65a6bff02927 Mon Sep 17 00:00:00 2001 From: Rob Gil Date: Thu, 16 Jan 2020 21:40:26 -0500 Subject: [PATCH] Adds a secrets generator and loader secrets-tool now has a feature to both generate secrets as well as load the generated secrets in to KeyVault. --- terraform/secrets-tool/README.md | 79 +++++++++++++++++ terraform/secrets-tool/commands/secrets.py | 14 ++- terraform/secrets-tool/sample-secrets.yaml | 7 ++ .../secrets-tool/utils/keyvault/secrets.py | 85 ++++++++++++++++++- 4 files changed, 183 insertions(+), 2 deletions(-) create mode 100644 terraform/secrets-tool/README.md create mode 100644 terraform/secrets-tool/sample-secrets.yaml diff --git a/terraform/secrets-tool/README.md b/terraform/secrets-tool/README.md new file mode 100644 index 00000000..28b44817 --- /dev/null +++ b/terraform/secrets-tool/README.md @@ -0,0 +1,79 @@ +# secrets-tool +secrets-tool is a group of utilities used to manage secrets in Azure environments. + +*Features:* +- Generate secrets based on definitions defined in yaml +- Load secrets in to Azure KeyVault +- Wrapper for terraform to inject KeyVault secrets as environment variables + +# Use Cases +## Populating KeyVault with initial secrets +In many environments, a complete list of secrets is sometimes forgotten or not well defined. With secrets-tool, all those secrets can be defined programatically and generated when creating new environments. This avoids putting in "test" values for passwords and guessible username/password combinations. Even usernames can be generated. + +With both usernames and passwords generated, the application only needs to make a call out to KeyVault for the key that it needs (assuming the application, host, or vm has access to the secret) + +Ex. +``` +{ + 'postgres_root_user': 'EzTEzSNLKQPHuJyPdPloIDCAlcibbl', + 'postgres_root_password': "2+[A@E4:C=ubb/#R#'n' > ~/.bash_profile +. ~/.bash_profile +``` + +`$ which secrets-tool` should show the full path + +# Usage +## Defining secrets +The schema for defining secrets is very simplistic for the moment. +```yaml +--- +- postgres-root-user: + type: 'username' + length: 30 +- postgres-root-password: + type: 'password' + length: 30 +``` +In this example we're randomly generating both the username and password. `secrets-tool` is smart enough to know that a username can't have symbols in it. Passwords contain symbols, upper/lower case, and numbers. This could be made more flexible and configurable in the future. + + +## Populating secrets from secrets definition file +This process is as simple as specifying the keyvault and the definitions file. +``` +secrets-tool secrets --keyvault https://operator-dev-keyvault.vault.azure.net/ load -f ./sample-secrets.yaml +``` + +## Running terraform with KeyVault secrets +This will fetch all secrets from the keyvault specified. `secrets-tool` then converts the keys to a variable name that terraform will look for. Essentially it prepends the keys found in KeyVault with `TF_VAR` and then executes terraform as a subprocess with the injected environment variables. +``` +secrets-tool terraform --keyvault https://operator-dev-keyvault.vault.azure.net/ plan +``` \ No newline at end of file diff --git a/terraform/secrets-tool/commands/secrets.py b/terraform/secrets-tool/commands/secrets.py index 7dbd16e7..bf9d7ff6 100644 --- a/terraform/secrets-tool/commands/secrets.py +++ b/terraform/secrets-tool/commands/secrets.py @@ -1,6 +1,7 @@ import click import logging from utils.keyvault.secrets import SecretsClient +from utils.keyvault.secrets import SecretsLoader logger = logging.getLogger(__name__) @@ -30,6 +31,17 @@ def list_secrets(ctx): keyvault = SecretsClient(vault_url=ctx.obj['keyvault']) click.echo(keyvault.list_secrets()) +@click.command('load') +@click.option('-f', 'file', required=True, help="YAML file with secrets definitions") +@click.pass_context +def load_secrets(ctx, file): + """Generate and load secrets from yaml definition""" + keyvault = SecretsClient(vault_url=ctx.obj['keyvault']) + loader = SecretsLoader(yaml_file=file, keyvault=keyvault) + loader.load_secrets() + + secrets.add_command(create_secret) -secrets.add_command(list_secrets) \ No newline at end of file +secrets.add_command(list_secrets) +secrets.add_command(load_secrets) \ No newline at end of file diff --git a/terraform/secrets-tool/sample-secrets.yaml b/terraform/secrets-tool/sample-secrets.yaml new file mode 100644 index 00000000..fb5f3ace --- /dev/null +++ b/terraform/secrets-tool/sample-secrets.yaml @@ -0,0 +1,7 @@ +--- +- postgres-root-user: + type: 'username' + length: 30 +- postgres-root-password: + type: 'password' + length: 30 diff --git a/terraform/secrets-tool/utils/keyvault/secrets.py b/terraform/secrets-tool/utils/keyvault/secrets.py index 1c554146..af8b5201 100644 --- a/terraform/secrets-tool/utils/keyvault/secrets.py +++ b/terraform/secrets-tool/utils/keyvault/secrets.py @@ -1,4 +1,8 @@ import logging +import yaml +import secrets +import string +from pathlib import Path from .auth import Auth from azure.keyvault.secrets import SecretClient @@ -23,4 +27,83 @@ class SecretsClient(Auth): secret_properties = self.secret_client.list_properties_of_secrets() for secret in secret_properties: secrets.append(secret.name) - return secrets \ No newline at end of file + return secrets + +class SecretsLoader(): + """ + Helper class to load secrets definition, generate + the secrets a defined by the defintion, and then + load the secrets in to keyvault + """ + def __init__(self, yaml_file: str, keyvault: object): + assert Path(yaml_file).exists() + self.yaml_file = yaml_file + self.keyvault = keyvault + self.config = dict() + + self._load_yaml() + self._generate_secrets() + + def _load_yaml(self): + with open(self.yaml_file) as handle: + self.config = yaml.load(handle, Loader=yaml.FullLoader) + + def _generate_secrets(self): + secrets = GenerateSecrets(self.config).process_definition() + self.secrets = secrets + + def load_secrets(self): + for key, val in self.secrets.items(): + print('{} {}'.format(key,val)) + self.keyvault.set_secret(key=key, value=val) + + +class GenerateSecrets(): + """ + Read the secrets definition and generate requiesite + secrets based on the type of secret and arguments + provided + """ + def __init__(self, definitions: dict): + self.definitions = definitions + + def process_definition(self): + """ + Processes a simple definiton such as the following + ``` + - postgres_root_user: + type: 'username' + length: 30 + - postgres_root_password: + type: 'password' + length: 30 + ``` + This should be broken out to a function per definition type + if the scope extends in to tokens, salts, or other specialized + definitions. + """ + try: + secrets = dict() + for definition in self.definitions: + key = list(definition) + def_name = key[0] + secret = definition[key[0]] + assert len(str(secret['length'])) > 0 + method = getattr(self, '_generate_'+secret['type']) + value = method(secret['length']) + #print('{}: {}'.format(key[0], value)) + secrets.update({def_name: value}) + logger.debug('Setting secrets to: {}'.format(secrets)) + return secrets + except KeyError as e: + logger.error('Missing the {} key in the definition'.format(e)) + + # Types. Can be usernames, passwords, or in the future things like salted + # tokens, uuid, or other specialized types + def _generate_password(self, length: int): + self.password_characters = string.ascii_letters + string.digits + string.punctuation + return ''.join(secrets.choice(self.password_characters) for i in range(length)) + + def _generate_username(self, length: int): + self.username_characters = string.ascii_letters + return ''.join(secrets.choice(self.username_characters) for i in range(length))