diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..73f69e0 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# Editor-based HTTP Client requests +/httpRequests/ diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..b936b6c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,271 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a2e120d --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..8d25e4b --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vaultpy.iml b/.idea/vaultpy.iml new file mode 100644 index 0000000..d0876a7 --- /dev/null +++ b/.idea/vaultpy.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..3c453c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +Copyright (c) 2021, DirectEmployers Association - https://directemployers.org diff --git a/README.md b/README.md new file mode 100644 index 0000000..1b38767 --- /dev/null +++ b/README.md @@ -0,0 +1,24 @@ +# Vaultpy +A module to parse injected [Vault](https://www.vaultproject.io/) secrets and track their usage with Datadog. + +## Requirements +- Local Datadog agent +- Environment variables to access it +- Set [container annotations](https://www.vaultproject.io/docs/platform/k8s/injector/annotations) to inject Vault secrets in K8s + +## Setup +For production, use the `VAULT_SECRETS_PATH` environment variable to set the path to the secrets that are injected by the Vault Agent in Kubernetes. + +For local development, a `de_secrets.py` can be used to load secrets in a format not unlike Django settings. + +## Usage +Import `vault.secrets` and then access the loaded secrets using by accessing dynamic properties loaded into the `secrets` object (i.e. `secrets.FOO`). + +Example of usage in a settings file: +```python +from vault import secrets + +FOO = secrets.FOO +BAR = getattr(secrets, "BAR", "") +BAZ = getattr(secrets, "BAZ") +``` diff --git a/build/lib/vault/__init__.py b/build/lib/vault/__init__.py new file mode 100644 index 0000000..97b561e --- /dev/null +++ b/build/lib/vault/__init__.py @@ -0,0 +1,85 @@ +import logging +from base64 import b64decode +from importlib import import_module +from json import loads +from os import environ +from typing import Dict + +from datadog import statsd + +logger = logging.getLogger(__name__) + + +def _load_de_secrets() -> Dict: + """ + Imports de_secrets module and returns a dictionary of its attributes. + """ + de_secrets = import_module("de_secrets") + return {k: getattr(de_secrets, k) for k in dir(de_secrets) if not k.startswith("_")} + + +def _load_vault_secrets() -> Dict: + """ + Load Vault injected secrets file located at VAULT_SECRETS_PATH, then perform + base 64 decode followed by JSON decode on file contents. This function + should not be called anywhere except within this module! + """ + with open(environ["VAULT_SECRETS_PATH"]) as file: + contents = file.read() + + json_secrets = b64decode(contents) + return loads(json_secrets) + + +def _get_secrets() -> Dict: + """ + Get secrets from de_secrets.py in local dev, or from Vault injected secrets file + located at path in VAULT_SECRETS_PATH. Performs base 64 decode followed by JSON + decode on file contents. + """ + if not environ.get("USE_VAULT"): + # Use dev secrets when available. + return _load_de_secrets() + + return _load_vault_secrets() + + +class VaultSecretsWrapper: + """ + Provide access to secrets as attributes and send secret-usage analytics to Datadog. + """ + + def __init__(self, secrets: Dict): + self._keys = secrets.keys() + self._env = environ.get("DD_ENV") + + for key, value in secrets.items(): + statsd.increment( + "vault.secrets.access_count", + value=1, + tags=[f"env:{self._env}", f"secret_key:{key}"], + ) + setattr(self, key, value) + + def __getattribute__(self, key: str): + """ + Override the default getattribute method so that we can track secret key + usage with Datadog. Non-secret attributes are passed on to the default method. + """ + if key not in ["_keys", "_env"] and key in self._keys: + try: + statsd.increment( + "vault.secrets.access_count", + value=1, + tags=[f"env:{self._env}", f"secret_key:{key}"], + ) + return super().__getattribute__(key) + except AttributeError as error: + logger.error(f"Requested secret could not be loaded: {key}") + raise error + + return super().__getattribute__(key) + + +secrets = VaultSecretsWrapper(_get_secrets()) +__all__ = ("secrets",) diff --git a/dist/vaultpy-0.0.1-py3-none-any.whl b/dist/vaultpy-0.0.1-py3-none-any.whl new file mode 100644 index 0000000..331a480 Binary files /dev/null and b/dist/vaultpy-0.0.1-py3-none-any.whl differ diff --git a/dist/vaultpy-0.0.1.tar.gz b/dist/vaultpy-0.0.1.tar.gz new file mode 100644 index 0000000..b2dfc67 Binary files /dev/null and b/dist/vaultpy-0.0.1.tar.gz differ diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..374b58c --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..00b77bf --- /dev/null +++ b/setup.cfg @@ -0,0 +1,13 @@ +[metadata] +name = vaultpy +version = 0.0.1 +author = Tim Loyer +author_email = tloyer@apps.directemployers.org +description = A module to parse injected Vault secrets and track their usage with Datadog. +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/DirectEmployers/vaultpy + +[options] +packages = find: +python_requires = >=3.6 diff --git a/vault/__init__.py b/vault/__init__.py new file mode 100644 index 0000000..97b561e --- /dev/null +++ b/vault/__init__.py @@ -0,0 +1,85 @@ +import logging +from base64 import b64decode +from importlib import import_module +from json import loads +from os import environ +from typing import Dict + +from datadog import statsd + +logger = logging.getLogger(__name__) + + +def _load_de_secrets() -> Dict: + """ + Imports de_secrets module and returns a dictionary of its attributes. + """ + de_secrets = import_module("de_secrets") + return {k: getattr(de_secrets, k) for k in dir(de_secrets) if not k.startswith("_")} + + +def _load_vault_secrets() -> Dict: + """ + Load Vault injected secrets file located at VAULT_SECRETS_PATH, then perform + base 64 decode followed by JSON decode on file contents. This function + should not be called anywhere except within this module! + """ + with open(environ["VAULT_SECRETS_PATH"]) as file: + contents = file.read() + + json_secrets = b64decode(contents) + return loads(json_secrets) + + +def _get_secrets() -> Dict: + """ + Get secrets from de_secrets.py in local dev, or from Vault injected secrets file + located at path in VAULT_SECRETS_PATH. Performs base 64 decode followed by JSON + decode on file contents. + """ + if not environ.get("USE_VAULT"): + # Use dev secrets when available. + return _load_de_secrets() + + return _load_vault_secrets() + + +class VaultSecretsWrapper: + """ + Provide access to secrets as attributes and send secret-usage analytics to Datadog. + """ + + def __init__(self, secrets: Dict): + self._keys = secrets.keys() + self._env = environ.get("DD_ENV") + + for key, value in secrets.items(): + statsd.increment( + "vault.secrets.access_count", + value=1, + tags=[f"env:{self._env}", f"secret_key:{key}"], + ) + setattr(self, key, value) + + def __getattribute__(self, key: str): + """ + Override the default getattribute method so that we can track secret key + usage with Datadog. Non-secret attributes are passed on to the default method. + """ + if key not in ["_keys", "_env"] and key in self._keys: + try: + statsd.increment( + "vault.secrets.access_count", + value=1, + tags=[f"env:{self._env}", f"secret_key:{key}"], + ) + return super().__getattribute__(key) + except AttributeError as error: + logger.error(f"Requested secret could not be loaded: {key}") + raise error + + return super().__getattribute__(key) + + +secrets = VaultSecretsWrapper(_get_secrets()) +__all__ = ("secrets",) diff --git a/vaultpy.egg-info/PKG-INFO b/vaultpy.egg-info/PKG-INFO new file mode 100644 index 0000000..ec41023 --- /dev/null +++ b/vaultpy.egg-info/PKG-INFO @@ -0,0 +1,21 @@ +Metadata-Version: 2.1 +Name: vaultpy +Version: 0.0.1 +Summary: A module to parse injected Vault secrets and track their usage with Datadog. +Home-page: https://github.com/DirectEmployers/vaultpy +Author: Tim Loyer +Author-email: tloyer@apps.directemployers.org +License: UNKNOWN +Description: # Vaultpy + A module to parse injected [Vault](https://www.vaultproject.io/) secrets and track their usage with Datadog. + + ## Requirements + - Local Datadog agent + - Environment variables to access it + + ## Usage + + +Platform: UNKNOWN +Requires-Python: >=3.6 +Description-Content-Type: text/markdown diff --git a/vaultpy.egg-info/SOURCES.txt b/vaultpy.egg-info/SOURCES.txt new file mode 100644 index 0000000..0c4a161 --- /dev/null +++ b/vaultpy.egg-info/SOURCES.txt @@ -0,0 +1,8 @@ +README.md +pyproject.toml +setup.cfg +vault/__init__.py +vaultpy.egg-info/PKG-INFO +vaultpy.egg-info/SOURCES.txt +vaultpy.egg-info/dependency_links.txt +vaultpy.egg-info/top_level.txt \ No newline at end of file diff --git a/vaultpy.egg-info/dependency_links.txt b/vaultpy.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/vaultpy.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/vaultpy.egg-info/top_level.txt b/vaultpy.egg-info/top_level.txt new file mode 100644 index 0000000..4c0870e --- /dev/null +++ b/vaultpy.egg-info/top_level.txt @@ -0,0 +1 @@ +vault