Skip to content

Commit

Permalink
Aws ssm parameter store (#55)
Browse files Browse the repository at this point in the history
* AWS SystemsManager ParameterStore support

* Update readme to add paramter store

* Address PR feedback

* Revert __all__ change

* Resave README

* Apply suggestions from code review

Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com>

* Review feedback

* extra s'

* Linting and bad lockfile

* Handle exception else case

* pylint

---------

Co-authored-by: Glenn Matthews <glenn.matthews@networktocode.com>
Co-authored-by: Bryan Culver <bryan.culver@networktocode.com>
Co-authored-by: Bryan Culver <31187+bryanculver@users.noreply.github.com>
  • Loading branch information
4 people committed Apr 19, 2023
1 parent 67bf63c commit cd70bf6
Show file tree
Hide file tree
Showing 6 changed files with 151 additions and 11 deletions.
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ This plugin supports the following popular secrets backends:
| Secrets Backend | Supported Secret Types | Supported Authentication Methods |
| ------------------------------------------------------------ | ------------------------------------------------------------ | ------------------------------------------------------------ |
| [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) | [Other: Key/value pairs](https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html) | [AWS credentials](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html) (see Usage section below) |
| [AWS Systems Manager Parameter Store](https://aws.amazon.com/secrets-manager/) | [Other: Key/value pairs](https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html) | [AWS credentials](https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html) (see Usage section below) |
| [HashiCorp Vault](https://www.vaultproject.io) | [K/V Version 2](https://www.vaultproject.io/docs/secrets/kv/kv-v2) | [Token](https://www.vaultproject.io/docs/auth/token)<br/>[AppRole](https://www.vaultproject.io/docs/auth/approle)<br/>[AWS](https://www.vaultproject.io/docs/auth/aws)<br/>[Kubernetes](https://www.vaultproject.io/docs/auth/kubernetes) |
| [Delinea/Thycotic Secret Server](https://delinea.com/products/secret-server) | [Secret Server Cloud](https://github.com/DelineaXPM/python-tss-sdk#secret-server-cloud)<br/>[Secret Server (on-prem)](https://github.com/DelineaXPM/python-tss-sdk#initializing-secretserver)| [Access Token Authorization](https://github.com/DelineaXPM/python-tss-sdk#access-token-authorization)<br/>[Domain Authorization](https://github.com/DelineaXPM/python-tss-sdk#domain-authorization)<br/>[Password Authorization](https://github.com/DelineaXPM/python-tss-sdk#password-authorization)<br/> |

Expand Down Expand Up @@ -60,9 +61,9 @@ For this plugin to operate you must install at least one of the dependent librar

**You must install the dependencies for at least one of the supported secrets providers or a `RuntimeError` will be raised.**

#### AWS Secrets Manager
#### AWS

The AWS Secrets Manager provider requires the `boto3` library. This can be easily installed along with the plugin using the following command:
AWS Secrets Manager and Systems Manager Parameter Store are supported. Both providers require the `boto3` library. This can be easily installed along with the plugin using the following command:

```no-highlight
pip install nautobot-secrets-providers[aws]
Expand Down Expand Up @@ -111,7 +112,7 @@ Before you proceed, you must have **at least one** of the dependent libaries ins

Please do not enable this plugin until you are able to install the dependencies, as it will block Nautobot from starting.

### AWS Secrets Manager
### AWS

#### Authentication

Expand All @@ -128,7 +129,7 @@ Boto3 credentials can be configured in multiple ways (eight as of this writing)
7. Boto2 config file (`/etc/boto.cfg` and `~/.boto`)
8. Instance metadata service on an Amazon EC2 instance that has an IAM role configured.

**The AWS Secrets Manager provider only supports methods 3-8. Methods 1 and 2 ARE NOT SUPPORTED at this time.**
**The AWS providers only support methods 3-8. Methods 1 and 2 ARE NOT SUPPORTED at this time.**

We highly recommend you defer to using environment variables for your deployment as specified in the credentials documentation linked above. The values specified in the linked documentation should be [set within your `~.bashrc`](https://nautobot.readthedocs.io/en/latest/installation/nautobot/#update-the-nautobot-bashrc) (or similar profile) on your system.

Expand Down
3 changes: 2 additions & 1 deletion nautobot_secrets_providers/providers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
"""Nautobot Secrets Providers."""

from .aws import AWSSecretsManagerSecretsProvider
from .aws import AWSSecretsManagerSecretsProvider, AWSSystemsManagerParameterStore
from .hashicorp import HashiCorpVaultSecretsProvider
from .delinea import ThycoticSecretServerSecretsProviderId, ThycoticSecretServerSecretsProviderPath

__all__ = ( # type: ignore
AWSSecretsManagerSecretsProvider, # pylint: disable=invalid-all-object
AWSSystemsManagerParameterStore, # pylint: disable=invalid-all-object
HashiCorpVaultSecretsProvider, # pylint: disable=invalid-all-object
ThycoticSecretServerSecretsProviderId, # pylint: disable=invalid-all-object
ThycoticSecretServerSecretsProviderPath, # pylint: disable=invalid-all-object
Expand Down
65 changes: 63 additions & 2 deletions nautobot_secrets_providers/providers/aws.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Secrets Provider for AWS Secrets Manager."""
"""Secrets Provider for AWS Secrets Manager and Parameter Store."""

import base64
import json
Expand All @@ -15,7 +15,7 @@
from nautobot.extras.secrets import exceptions, SecretsProvider


__all__ = ("AWSSecretsManagerSecretsProvider",)
__all__ = ("AWSSecretsManagerSecretsProvider", "AWSSystemsManagerParameterStore")


class AWSSecretsManagerSecretsProvider(SecretsProvider):
Expand Down Expand Up @@ -99,3 +99,64 @@ def get_value_for_secret(cls, secret, obj=None, **kwargs):
except KeyError as err:
msg = f"The secret value could not be retrieved using key {err}"
raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err


class AWSSystemsManagerParameterStore(SecretsProvider):
"""
A secrets provider for AWS Systems Manager Parameter Store.
Documentation: https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html
"""

slug = "aws-sm-parameter-store"
name = "AWS Systems Manager Parameter Store"
is_available = boto3 is not None

class ParametersForm(BootstrapMixin, forms.Form):
"""Required parameters for AWS Parameter Store."""

name = forms.CharField(
required=True,
help_text="The name of the AWS Parameter Store secret",
)
region = forms.CharField(
required=True,
help_text="The region name of the AWS Parameter Store secret",
)
key = forms.CharField(
required=True,
help_text="The key name to retrieve from AWS Parameter Store",
)

@classmethod
def get_value_for_secret(cls, secret, obj=None, **kwargs):
"""Return the parameter value by name and region."""
# Extract the parameters from the Nautobot secret.
parameters = secret.rendered_parameters(obj=obj)

# Create a SSM client.
session = boto3.session.Session()
client = session.client(service_name="ssm", region_name=parameters.get("region"))
try:
get_secret_value_response = client.get_parameter(Name=parameters.get("name"), WithDecryption=True)
except ClientError as err:
if err.response["Error"]["Code"] == "ParameterNotFound":
raise exceptions.SecretParametersError(secret, cls, str(err))

if err.response["Error"]["Code"] == "ParameterVersionNotFound":
raise exceptions.SecretValueNotFoundError(secret, cls, str(err))

raise exceptions.SecretProviderError(secret, cls, str(err))
else:
try:
# Fetch the Value field from the parameter which must be a json field.
data = json.loads(get_secret_value_response["Parameter"]["Value"])
except ValueError as err:
msg = "InvalidJson"
raise exceptions.SecretValueNotFoundError(secret, cls, msg) from err
try:
# Return the value of the secret key configured in the nautobot secret.
return data[parameters.get("key")]
except KeyError as err:
msg = f"InvalidKeyName {err}"
raise exceptions.SecretParametersError(secret, cls, msg) from err
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ <h1>{% block title %}Secrets Providers Home{% endblock %}</h1>
<td><a href="https://docs.aws.amazon.com/secretsmanager/latest/userguide/manage_create-basic-secret.html" rel="nofollow">Other: Key/value pairs</a></td>
<td><a href="https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html" rel="nofollow">AWS credentials</a></td>
</tr>
<tr>
<td><a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/systems-manager-parameter-store.html" rel="nofollow">AWS Systems Manager Parameter Store</a></td>
<td><a href="https://docs.aws.amazon.com/systems-manager/latest/userguide/param-create-cli.html" rel="nofollow">Other: Key/value pairs</a></td>
<td><a href="https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html" rel="nofollow">AWS credentials</a></td>
</tr>
<tr>
<td><a href="https://www.vaultproject.io" rel="nofollow">HashiCorp Vault</a></td>
<td><a href="https://www.vaultproject.io/docs/secrets/kv/kv-v2" rel="nofollow">K/V Version 2</a></td>
Expand Down
76 changes: 74 additions & 2 deletions nautobot_secrets_providers/tests/test_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,16 @@
from django.contrib.auth import get_user_model
from django.test import Client, TestCase, tag
from hvac import Client as HVACClient
from moto import mock_secretsmanager
from moto import mock_secretsmanager, mock_ssm
import requests_mock

from nautobot.extras.models import Secret
from nautobot.extras.secrets import exceptions
from nautobot_secrets_providers.providers import AWSSecretsManagerSecretsProvider, HashiCorpVaultSecretsProvider
from nautobot_secrets_providers.providers import (
AWSSecretsManagerSecretsProvider,
AWSSystemsManagerParameterStore,
HashiCorpVaultSecretsProvider,
)


# Use the proper swappable User model
Expand Down Expand Up @@ -448,3 +452,71 @@ def test_get_client_aws(self, requests_mocker):
str(err.exception),
'SecretProviderError: Secret "hello-hashicorp" (provider "HashiCorpVaultSecretsProvider"): HashiCorp Vault Login failed (auth_method: aws). Error: , on post http://localhost:8200/v1/auth/aws/login',
)


class AWSSystemsManagerParameterStoreTestCase(SecretsProviderTestCase):
"""Tests for AWSSystemsManagerParameterStore."""

provider = AWSSystemsManagerParameterStore

def setUp(self):
super().setUp()
self.secret = Secret.objects.create(
name="hello-aws-parameterstore",
slug="hello-aws-parameterstore",
provider=self.provider.slug,
parameters={"name": "hello", "region": "eu-west-3", "key": "location"},
)

@mock_ssm
def test_retrieve_success(self):
"""Retrieve a secret successfully."""
conn = boto3.client("ssm", region_name=self.secret.parameters["region"])
conn.put_parameter(Name="hello", Type="SecureString", Value='{"location":"world"}')
result = self.provider.get_value_for_secret(self.secret)
self.assertEqual(result, "world")

@mock_ssm
def test_retrieve_does_not_exist(self):
"""Try and fail to retrieve a secret that doesn't exist."""
boto3.client("ssm", region_name=self.secret.parameters["region"])

with self.assertRaises(exceptions.SecretParametersError) as err:
self.provider.get_value_for_secret(self.secret)

exc = err.exception
self.assertIn("ParameterNotFound", exc.message)

@mock_ssm
def test_retrieve_invalid_key(self):
"""Try and fail to retrieve a secret from an existing parameter but an invalid key."""
conn = boto3.client("ssm", region_name=self.secret.parameters["region"])
conn.put_parameter(Name="hello", Type="SecureString", Value='{"position":"world"}')
# Try to fetch the secret with key as locatio
with self.assertRaises(exceptions.SecretParametersError) as err:
self.provider.get_value_for_secret(self.secret)
exc = err.exception
self.assertIn(f"InvalidKeyName '{self.secret.parameters['key']}'", exc.message)

@mock_ssm
def test_retrieve_non_valid_json(self):
conn = boto3.client("ssm", region_name=self.secret.parameters["region"])
conn.put_parameter(Name="hello", Type="SecureString", Value="Non Valid JSON")

with self.assertRaises(exceptions.SecretValueNotFoundError) as err:
self.provider.get_value_for_secret(self.secret)

exc = err.exception
self.assertIn("InvalidJson", exc.message)

@mock_ssm
def test_retrieve_invalid_version(self):
"""Try and fail to retrieve a parameter while specifying an invalid version|label."""
conn = boto3.client("ssm", region_name=self.secret.parameters["region"])
conn.put_parameter(Name="hello", Type="SecureString", Value='{"location":"world"}')
# add a non existing version to the Nautobot secret name and try to fetch it
self.secret.parameters["name"] += ":2"
with self.assertRaises(exceptions.SecretValueNotFoundError) as err:
self.provider.get_value_for_secret(self.secret)
exc = err.exception
self.assertIn("ParameterVersionNotFound", exc.message)
4 changes: 2 additions & 2 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit cd70bf6

Please sign in to comment.