Skip to content

Commit

Permalink
Merge pull request #142 from nautobot/release/3.1.0
Browse files Browse the repository at this point in the history
Release v3.1.0
  • Loading branch information
gsnider2195 authored Aug 1, 2024
2 parents 48c91c6 + b474077 commit 10a6d56
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 59 deletions.
18 changes: 16 additions & 2 deletions development/nautobot_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,22 @@
"username": os.getenv("SECRET_SERVER_USERNAME", ""),
},
"hashicorp_vault": {
"url": os.environ.get("HASHICORP_VAULT_URL"),
"token": os.environ.get("HASHICORP_VAULT_TOKEN"),
# "url": os.environ.get("HASHICORP_VAULT_URL"),
# "token": os.environ.get("HASHICORP_VAULT_TOKEN"),
"vaults": {
"hashicorp_approle": {
"url": os.environ.get("HASHICORP_VAULT_URL"),
"auth_method": "approle",
"role_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_ROLE_ID"),
"secret_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_SECRET_ID"),
},
"hashicorp_v1_custom_mount": {
"url": os.environ.get("HASHICORP_VAULT_URL"),
"token": os.environ.get("HASHICORP_VAULT_TOKEN"),
"kv_version": "v1",
"default_mount_point": "secret_kv",
},
}
},
},
}
1 change: 1 addition & 0 deletions docs/admin/compatibility_matrix.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
| 1.4.X | 1.4.0 | 1.99.99 |
| 2.0.X | 2.0.0 | 2.99.99 |
| 3.0.X | 2.0.0 | 2.99.99 |
| 3.1.X | 2.0.0 | 2.99.99 |
35 changes: 35 additions & 0 deletions docs/admin/providers/hashicorp_setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,38 @@ PLUGINS_CONFIG = {
- `secret_id` - (optional) Required when `"auth_method": "approle"`.As with other sensitive service credentials, we recommend that you provide the secret_id value as an environment variable and retrieve it with `{"secret_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_SECRET_ID")}` rather than hard-coding it in your `nautobot_config.py`.
- `login_kwargs` - (optional) Additional optional parameters to pass to the login method for [`approle`](https://hvac.readthedocs.io/en/stable/source/hvac_api_auth_methods.html#hvac.api.auth_methods.AppRole.login), [`aws`](https://hvac.readthedocs.io/en/stable/source/hvac_api_auth_methods.html#hvac.api.auth_methods.Aws.iam_login) and [`kubernetes`](https://hvac.readthedocs.io/en/stable/source/hvac_api_auth_methods.html#hvac.api.auth_methods.Kubernetes.login) authentication methods.
- `namespace` - (optional) Namespace to use for the [`X-Vault-Namespace` header](https://github.com/hvac/hvac/blob/main/hvac/adapters.py#L287) on all hvac client requests. Required when the [`Namespaces`](https://developer.hashicorp.com/vault/docs/enterprise/namespaces#usage) feature is enabled in Vault Enterprise.

### Multiple Hashicorp Vaults

+++ 3.1.0

Hashicorp Provider now supports using multiple vaults (configurations). You will be able to choose the vault when creating a secret, For example, you could have one vault using `approle` authentication, and a second vault using `token` authentication in combination with a different default mount point:

```python
PLUGINS_CONFIG = {
"nautobot_secrets_providers": {
"hashicorp_vault": {
"vaults": {
"hashicorp_approle": {
"url": os.environ.get("HASHICORP_VAULT_URL"),
"auth_method": "approle",
"role_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_ROLE_ID"),
"secret_id": os.getenv("NAUTOBOT_HASHICORP_VAULT_SECRET_ID"),
},
"hashicorp_v1_custom_mount": {
"url": os.environ.get("HASHICORP_VAULT_URL"),
"token": os.environ.get("HASHICORP_VAULT_TOKEN"),
"kv_version": "v1",
"default_mount_point": "secret_kv",
},
}
}
},
}
```

![Select Secret Configuration](../../images/light/hashicorp_multiple_vaults.png#only-light)
![Select Secret Configuration](../../images/dark/hashicorp_multiple_vaults.png#only-dark)

!!! note
If using this option, you should not have any keys except `vaults` under `hashicorp_vault`.
21 changes: 21 additions & 0 deletions docs/admin/release_notes/version_3.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# v3.1 Release Notes

This document describes all new features and changes in the release `3.1`. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Release Overview

This release adds support for multiple HashiCorp Vault secrets providers.

## [v3.1.0 (2024-08-01)](https://github.com/nautobot/nautobot-app-secrets-providers/releases/tag/v3.1.0)

### Added

- [#67](https://github.com/nautobot/nautobot-app-secrets-providers/issues/67) - Added the ability to choose between multiple vaults (configurations) for HashiCorp.

### Documentation

- [#137](https://github.com/nautobot/nautobot-app-secrets-providers/issues/137) - Updated documentation links for installed apps page.

### Housekeeping

- [#140](https://github.com/nautobot/nautobot-app-secrets-providers/issues/140) - Updated development environment to use `certifi` `2024.7.4`.
Binary file added docs/images/dark/hashicorp_multiple_vaults.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/images/light/hashicorp_multiple_vaults.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions nautobot_secrets_providers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class NautobotSecretsProvidersConfig(NautobotAppConfig):
max_version = "2.9999"
default_settings = {}
caching_config = {}
docs_view_name = "plugins:nautobot_secrets_providers:docs"

# URL reverse lookup names
home_view_name = "plugins:nautobot_secrets_providers:home"
Expand Down
135 changes: 84 additions & 51 deletions nautobot_secrets_providers/providers/hashicorp.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
except ImportError:
hvac = None

from nautobot.core.forms import BootstrapMixin
from nautobot.core.forms import add_blank_choice, BootstrapMixin
from nautobot.extras.secrets import exceptions, SecretsProvider

from .choices import HashicorpKVVersionChoices
Expand All @@ -24,19 +24,17 @@
AUTH_METHOD_CHOICES = ["approle", "aws", "kubernetes", "token"]


# Default mount point for the HVAC client
try:
plugins_config = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
DEFAULT_MOUNT_POINT = plugins_config["hashicorp_vault"]["default_mount_point"]
except KeyError:
DEFAULT_MOUNT_POINT = "secret"
def vault_choices():
"""Generate Choices for vault form field.
# Default kv version for the HVAC client
try:
plugins_config = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
DEFAULT_KV_VERSION = plugins_config["hashicorp_vault"]["default_kv_version"]
except KeyError:
DEFAULT_KV_VERSION = HashicorpKVVersionChoices.KV_VERSION_2
If `vaults` is a key in the vault config,
then we build a form option for each key in vaults.
Otherwise we fall "Default" to make this a non-breaking change.
"""
plugin_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
if "vaults" in plugin_settings["hashicorp_vault"]:
return [(key, key.replace("_", " ").title()) for key in plugin_settings["hashicorp_vault"]["vaults"].keys()]
return [("default", "Default")]


class HashiCorpVaultSecretsProvider(SecretsProvider):
Expand All @@ -59,28 +57,50 @@ class ParametersForm(BootstrapMixin, forms.Form):
required=True,
help_text="The key of the HashiCorp Vault secret",
)
vault = forms.ChoiceField(
required=False, # This should be required, but would be a breaking change
choices=vault_choices,
help_text="HashiCorp Vault to retrieve the secret from.",
)
mount_point = forms.CharField(
required=False,
help_text=f"The path where the secret engine was mounted on (Default: <code>{DEFAULT_MOUNT_POINT}</code>)",
initial=DEFAULT_MOUNT_POINT,
help_text="Override Vault Setting: The path where the secret engine was mounted on.",
label="Mount Point (override)",
)
kv_version = forms.ChoiceField(
required=False,
choices=HashicorpKVVersionChoices,
help_text=f"The version of the kv engine (either v1 or v2) (Default: <code>{DEFAULT_KV_VERSION}</code>)",
initial=DEFAULT_KV_VERSION,
choices=add_blank_choice(HashicorpKVVersionChoices),
help_text="Override Vault Setting: The version of the kv engine (either v1 or v2).",
label="KV Version (override)",
)

@staticmethod
def retrieve_vault_settings(name=None):
"""Retrieve the configuration from settings that matches the provided vault name.
Args:
name (str, optional): Vault name to retrieve from settings. Defaults to None.
Returns:
vault_settings (dict): Hashicorp Vault Settings
"""
vault_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"].get("hashicorp_vault", {})
if name and "vaults" in vault_settings:
return vault_settings["vaults"][name]
return vault_settings

@classmethod
def validate_vault_settings(cls, secret=None):
def validate_vault_settings(cls, secret=None, vault_name=None):
"""Validate the vault settings."""
# This is only required for HashiCorp Vault therefore not defined in
# `required_settings` for the plugin config.
plugin_settings = settings.PLUGINS_CONFIG["nautobot_secrets_providers"]
if "hashicorp_vault" not in plugin_settings:
raise exceptions.SecretProviderError(secret, cls, "HashiCorp Vault is not configured!")
try:
vault_settings = cls.retrieve_vault_settings(vault_name)
except KeyError as err:
raise exceptions.SecretProviderError(
secret, cls, f"HashiCorp Vault {vault_name} is not configured!"
) from err
if not vault_settings:
raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault {vault_name} is not configured!")

vault_settings = plugin_settings.get("hashicorp_vault", {})
auth_method = vault_settings.get("auth_method", "token")
kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)

Expand All @@ -93,33 +113,29 @@ def validate_vault_settings(cls, secret=None):
if kv_version not in HashicorpKVVersionChoices.as_dict():
raise exceptions.SecretProviderError(secret, cls, f"HashiCorp Vault KV version {kv_version} is invalid!")

if auth_method == "aws":
if not boto3:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault AWS Authentication Method requires the boto3 library!"
)
elif auth_method == "token":
if "token" not in vault_settings:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault configuration is missing a token for token authentication!"
)
elif auth_method == "kubernetes":
if "role_name" not in vault_settings:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault configuration is missing a role name for kubernetes authentication!"
)
elif auth_method == "approle":
if "role_id" not in vault_settings or "secret_id" not in vault_settings:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault configuration is missing a role_id and/or secret_id!"
)
if auth_method == "aws" and not boto3:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault AWS Authentication Method requires the boto3 library!"
)
if auth_method == "token" and "token" not in vault_settings:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault configuration is missing a token for token authentication!"
)
if auth_method == "kubernetes" and "role_name" not in vault_settings:
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault configuration is missing a role name for kubernetes authentication!"
)
if auth_method == "approle" and ("role_id" not in vault_settings or "secret_id" not in vault_settings):
raise exceptions.SecretProviderError(
secret, cls, "HashiCorp Vault configuration is missing a role_id and/or secret_id!"
)

return vault_settings

@classmethod
def get_client(cls, secret=None):
def get_client(cls, secret=None, vault_name=None): # pylint: disable-msg=too-many-locals
"""Authenticate and return a hashicorp client."""
vault_settings = cls.validate_vault_settings(secret)
vault_settings = cls.validate_vault_settings(secret, vault_name)
auth_method = vault_settings.get("auth_method", "token")

k8s_token_path = vault_settings.get("k8s_token_path", K8S_TOKEN_DEFAULT_PATH)
Expand All @@ -136,7 +152,10 @@ def get_client(cls, secret=None):
try:
if auth_method == "token":
client = hvac.Client(
url=vault_settings["url"], token=vault_settings["token"], verify=ca_cert, namespace=namespace
url=vault_settings["url"],
token=vault_settings["token"],
verify=ca_cert,
namespace=namespace,
)
else:
client = hvac.Client(url=vault_settings["url"], verify=ca_cert, namespace=namespace)
Expand Down Expand Up @@ -178,16 +197,30 @@ def get_value_for_secret(cls, secret, obj=None, **kwargs):
"""Return the value stored under the secret’s key in the secret’s path."""
# Try to get parameters and error out early.
parameters = secret.rendered_parameters(obj=obj)
try:
vault_name = parameters.get("vault", "default")
vault_settings = cls.retrieve_vault_settings(vault_name)
except KeyError:
vault_settings = {}
# Get the mount_point and kv_version from the Vault configuration. These default to the
# default Vault that HashiCorp provides.
secret_mount_point = vault_settings.get("default_mount_point", "secret")
secret_kv_version = vault_settings.get("kv_version", HashicorpKVVersionChoices.KV_VERSION_2)

try:
secret_path = parameters["path"]
secret_key = parameters["key"]
secret_mount_point = parameters.get("mount_point", DEFAULT_MOUNT_POINT)
secret_kv_version = parameters.get("kv_version", DEFAULT_KV_VERSION)
# If the user does choose to override the Vault settings at their own risk, we will use
# the settings they provide. These are here to support multiple vaults (vault engines) when
# that was not allowed by the settings. Ideally these should be deprecated and removed in
# the future.
secret_mount_point = parameters.get("mount_point", secret_mount_point) or secret_mount_point
secret_kv_version = parameters.get("kv_version", secret_kv_version) or secret_kv_version
except KeyError as err:
msg = f"The secret parameter could not be retrieved for field {err}"
raise exceptions.SecretParametersError(secret, cls, msg) from err

client = cls.get_client(secret)
client = cls.get_client(secret, vault_name)

try:
if secret_kv_version == HashicorpKVVersionChoices.KV_VERSION_1:
Expand Down
Loading

0 comments on commit 10a6d56

Please sign in to comment.