AWS Parameter Store
Blog

AWS Parameter Store

Anyone with a moderate level of AWS experience will have learned that Amazon offers more than one way of doing something. Storing secrets is no exception.

It is possible to spin up Hashicorp Vault on AWS using an official Amazon quick start guide. The down side of this approach is that you have to maintain it.

If you want an “AWS native” approach, you have 2 services to choose from. As the name suggests, Secrets Manager provides some secrets management tools on top of the store. This includes automagic rotation of AWS RDS credentials on a regular schedule. For the first 30 days the service is free, then you start paying per secret per month, plus API calls.

There is a free option, Amazon’s Systems Manager Parameter Store. This is what I’ll be covering today.

Structure

It is easy when you first start out to store all your secrets at the top level. After a while you will regret this decision.

Parameter Store supports hierarchies. I recommend using them from day one. Today I generally use /[appname]-[env]/[KEY]. After some time with this scheme I am finding that /[appname]/[env]/[KEY] feels like it will be easier to manage. IAM permissions support paths and wildcards, so either scheme will work.

If you need to migrate your secrets, use Parameter Store namespace migration script.

Access Controls

Like most Amazon services IAM controls access to Parameter Store.

Parameter Store allows you to store your values as plain text or encrypted using a key using KMS. For encrypted values the user must have have grants on the parameter store value and KMS key. For consistency I recommend encrypting all your parameters.

If you have a monolith a key per application per environment is likely to work well. If you have a collection of microservices having a key per service per environment becomes difficult to manage. In this case share a key between several services in the same environment.

Here is an IAM policy for an Lambda function to access a hierarchy of values in parameter store:

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"ReadParams",
      "Effect":"Allow",
      "Action":[
        "ssm:GetParametersByPath"
      ],
      "Resource":"arn:aws:ssm:us-east-1:1234567890:parameter/my-app/dev/*"
    },
    {
      "Sid":"Decrypt",
      "Effect":"Allow",
      "Action":[
        "kms:Decrypt"
      ],
      "Resource":"arn:aws:kms:us-east-1:1234567890:key/20180823-7311-4ced-bad5-653587846973"
    }
  ]
}

To allow your developers to manage the parameters in dev you will need a policy that looks like this:

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"ManageParams",
      "Effect":"Allow",
      "Action":[
        "ssm:DeleteParameter",
        "ssm:DeleteParameters",
        "ssm:GetParameter",
        "ssm:GetParameterHistory",
        "ssm:GetParametersByPath",
        "ssm:GetParameters",
        "ssm:PutParameter"
      ],
      "Resource":"arn:aws:ssm:us-east-1:1234567890:parameter/my-app/dev/*"
    },
    {
      "Sid":"ListParams",
      "Effect":"Allow",
      "Action":"ssm:DescribeParameters",
      "Resource":"*"
    },
    {
      "Sid":"DecryptEncrypt",
      "Effect":"Allow",
      "Action":[
        "kms:Decrypt",
        "kms:Encrypt"
      ],
      "Resource":"arn:aws:kms:us-east-1:1234567890:key/20180823-7311-4ced-bad5-653587846973"
    }
  ]
}

Amazon has great documentation on controlling access to Parameter Store and KMS.

Adding Parameters

Amazon allows you to store almost any string up to 4Kbs in length in the Parameter store. This gives you a lot of flexibility.

Parameter Store supports deep hierarchies. You will find this becomes annoying to manage. Use hierarchies to group your values by application and environment. Within the hierarchy use a flat structure. I recommend using lower case letters with dashes between words for your paths. For the parameter keys use upper case letters with underscores. This makes it easy to differentiate the two when searching for parameters.

Parameter store encodes everything as strings. There may be cases where you want to store an integer as an integer or a more complex data structure. You could use a naming convention to differentiate your different types. I found it easiest to encode every thing as json. When pulling values from the store I json decode it. The down side is strings must be wrapped in double quotes. This is offset by the flexibility of being able to encode objects and use numbers.

It is possible to add parameters to the store using 3 different methods. I generally find the AWS web console easiest when adding a small number of entries. Rather than walking you through this, Amazon have good documentation on adding values. Remember to always use “secure string” to encrypt your values.

Adding parameters via boto3 is straight forward. Once again it is well documented by Amazon.

Finally you can maintain parameters in with a little bit of code. In this example I do it with Python.

import boto3

namespace = "my-app"
env = "dev"
kms_uuid = "20180823-7311-4ced-bad5-653587846973"
# Objects must be json encoded then wrapped in quotes because they're stored as strings.
parameters = {"key": '"value"', "MY_INT": 1234, "MY_OBJ": '{"name": "value"}'}

ssm = boto3.client("ssm")
for parameter in parameters:
    ssm.put_parameter(
        Name=f"/{namespace}/{env}/{parameter.upper()}",
        # Everything must go in as a string.
        Value=str(parameters[parameter]),
        Type="SecureString",
        KeyId=kms_uuid,
        # Use with caution.
        Overwrite=True,
    )

Using Parameters

I have used Parameter Store from Python and the command line. It is easier to use it from Python.

My example assumes that it a Lambda function running with the policy from earlier. The function is called my-app-dev. This is what my code looks like:

import json

import boto3

def load_params(namespace: str, env: str) -> dict:
    """Load parameters from SSM Parameter Store.
    :namespace: The application namespace.
    :env: The current application environment.
    :return: The config loaded from Parameter Store.
    """
    config = {}
    path = f"/{namespace}/{env}/"
    ssm = boto3.client("ssm", region_name="us-east-1")
    more = None
    args = {"Path": path, "Recursive": True, "WithDecryption": True}
    while more is not False:
        if more:
            args["NextToken"] = more
        params = ssm.get_parameters_by_path(**args)
        for param in params["Parameters"]:
            key = param["Name"].split("/")[3]
            config[key] = json.loads(param["Value"])
        more = params.get("NextToken", False)
    return config

If you want to avoid loading your config each time your Lambda function is called you can store the results in a global variable. This leverages Amazon’s feature that doesn’t clear global variables between function invocations. The catch is that your function won’t pick up parameter changes without a code deployment. Another option is to put in place logic for periodic purging of the cache.

On the command line things are little harder to manage if you have more than 10 parameters. To export a small number of entries as environment variables, you can use this one liner:

$(aws ssm get-parameters-by-path --with-decryption --path /my-app/dev/ | jq -r '.Parameters[] | "export " + (.Name | split("/")[3] | ascii_upcase | gsub("-"; "_")) + "=" + .Value + ";"')

Make sure you have jq installed and the AWS cli installed and configured.

Conclusion

Amazon’s System Manager Parameter Store provides a secure way of storing and managing secrets for your AWS based apps. Unlike Hashicorp Vault, Amazon manages everything for you. If you don’t need the more advanced features of Secrets Manager you don’t have to pay for them. For most users Parameter Store will be adequate.