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.
This commit is contained in:
parent
b9a7efe6ba
commit
55623028df
79
terraform/secrets-tool/README.md
Normal file
79
terraform/secrets-tool/README.md
Normal file
@ -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<p|wCW-|%q^"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rotating secrets
|
||||||
|
Rotating passwords is a snap! Just re-run secrets-tool and it will generate and populate new secrets.
|
||||||
|
|
||||||
|
**Be careful!! There is no safeguard to prevent you from accidentally overwriting secrets!! - To be added if desired**
|
||||||
|
|
||||||
|
## Terraform Secrets
|
||||||
|
Terraform typically expects user defined secrets to be stored in either a file, or in another service such as keyvault. The terraform wrapper feature, injects secrets from keyvault in to the environment and then runs terraform.
|
||||||
|
|
||||||
|
This provides a number of security benefits. First, secrets are not on disk. Secondly, users/operators never see the secrets fly by (passerbys or voyeurs that like to look over your shoulder when deploying to production)
|
||||||
|
|
||||||
|
# Setup
|
||||||
|
|
||||||
|
*Requirements*
|
||||||
|
- Python 3.7+
|
||||||
|
- pipenv
|
||||||
|
|
||||||
|
```
|
||||||
|
cd secrets-tool
|
||||||
|
pipenv install
|
||||||
|
pipenv shell
|
||||||
|
```
|
||||||
|
|
||||||
|
You will also need to make sure secrets-tool is in your PATH
|
||||||
|
|
||||||
|
```
|
||||||
|
echo 'PATH=$PATH:<path to secrets-tool>' > ~/.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
|
||||||
|
```
|
@ -1,6 +1,7 @@
|
|||||||
import click
|
import click
|
||||||
import logging
|
import logging
|
||||||
from utils.keyvault.secrets import SecretsClient
|
from utils.keyvault.secrets import SecretsClient
|
||||||
|
from utils.keyvault.secrets import SecretsLoader
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -30,6 +31,17 @@ def list_secrets(ctx):
|
|||||||
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
keyvault = SecretsClient(vault_url=ctx.obj['keyvault'])
|
||||||
click.echo(keyvault.list_secrets())
|
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(create_secret)
|
||||||
secrets.add_command(list_secrets)
|
secrets.add_command(list_secrets)
|
||||||
|
secrets.add_command(load_secrets)
|
7
terraform/secrets-tool/sample-secrets.yaml
Normal file
7
terraform/secrets-tool/sample-secrets.yaml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
- postgres-root-user:
|
||||||
|
type: 'username'
|
||||||
|
length: 30
|
||||||
|
- postgres-root-password:
|
||||||
|
type: 'password'
|
||||||
|
length: 30
|
@ -1,4 +1,8 @@
|
|||||||
import logging
|
import logging
|
||||||
|
import yaml
|
||||||
|
import secrets
|
||||||
|
import string
|
||||||
|
from pathlib import Path
|
||||||
from .auth import Auth
|
from .auth import Auth
|
||||||
from azure.keyvault.secrets import SecretClient
|
from azure.keyvault.secrets import SecretClient
|
||||||
|
|
||||||
@ -23,4 +27,83 @@ class SecretsClient(Auth):
|
|||||||
secret_properties = self.secret_client.list_properties_of_secrets()
|
secret_properties = self.secret_client.list_properties_of_secrets()
|
||||||
for secret in secret_properties:
|
for secret in secret_properties:
|
||||||
secrets.append(secret.name)
|
secrets.append(secret.name)
|
||||||
return secrets
|
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))
|
||||||
|
Loading…
x
Reference in New Issue
Block a user