Skip to main content

Aero meets SSM

·5 mins

Aero is a small configuration library from JUXT that extends Clojure's EDN reader with support for tags to interpolate values from environment variables, coerce these values to a specific type, use simple conditionals, and several other useful features. It is also easily extensible through the aero.core/reader multimethod.

We have been using EDN files to configure our Clojure applications at Metail for some time, so aero is a natural fit for us. One of the questions we have been avoiding is what to do with secrets (tokens, passwords, API keys, etc.). The aero README suggests that you hide passwords in a local private file. While that works well for local development (and keeps secrets out of version control), it does not help us orchestrate deployments to cloud servers.

We are already using Consul to store key/value data and considered deploying Vault for the secret storage. We could then have used consul-template to interpolate values from Consul and Vault into our configuration files. While this would have worked, it would have been yet another service to deploy and manage, and it felt a bit heavyweight for our needs.

Last year one of my colleagues evaluated CredStash, a simpler secret store built on Amazon DynamoDB and KMS (Key Management Service). This looked very promising as it is a serverless solution that integrates well with AWS IAM. I was looking to migrate our secrets into CredStash when I stumbled across this blog post, describing an alternative solution using AWS Systems Manager.

AWS Systems Manager (SSM) #

The feature of AWS Systems Manager we are interested in is the Parameter Store:

AWS Systems Manager provides a centralized store to manage your configuration data, whether plain-text data such as database strings or secrets such as passwords. This allows you to separate your secrets and configuration data from your code. Parameters can be tagged and organized into hierarchies, helping you manage parameters more easily. For example, you can use the same parameter name, "db-string", with a different hierarchical path, "dev/db-string” or “prod/db-string", to store different values. Systems Manager is integrated with AWS Key Management Service (KMS), allowing you to automatically encrypt the data you store. You can also control user and resource access to parameters using AWS Identity and Access Management (IAM).

Getting started with the parameter store #

We can use the AWS CLI to add and retrieve parameters. First let's create a KMS key so we can store encrypted parameters:

aws kms create-key --description my-test-key

Make a note of the returned key id - you will need it soon.

To create unencrypted parameters we specify --type String:

aws ssm put-paremeter --name "/adserver/test/db/type" --value "postgresql" --type "String"
aws ssm put-parameter --name "/adserver/test/db/name" --value "adserver_test" --type "String"
aws ssm put-parameter --name "/adserver/test/db/host" --value "localhost" --type "String"
aws ssm put-parameter --name "/adserver/test/db/port" --value "5432" --type "String"
aws ssm put-parameter --name "/adserver/test/db/user" --value "adserver_web" --type "String"

For an encrypted parameter, we specify --type SecureString and the key id:

aws ssm put-parameter --name "/adserver/test/db/password" --value "HbGKSEJIz7FvrFFC" \
  --type "SecureString" --key-id "f9870095-fc23-4b99-b11f-69252e714a1f"

To list all parameters:

aws ssm describe-parameters

To retrieve a parameter:

aws ssm get-parameter --name "/adserver/test/db/user"

Finally, to retrieve an encrypted parameter:

aws ssm get-parameter --name "/adserver/test/db/password" --with-decryption

A Gotcha #

The AWS CLI has an interesting feature: if you pass a URL as a value, the CLI will perform an HTTP GET on the URL to resolve the value - see Github Issue #2507. If you actually want to store a URL using the CLI, the work-around is to use --cli-input-json:

aws ssm put-parameter --cli-input-json '{"Name": "/adserver/test/homepage", "Value": "https://example.com/adserver/", "Type": "String"}'

Back to aero #

Now that we have our parameters in the parameter store, we would like to be able to write our application configuration as:

{:homepage #parameter "/adserver/test/homepage"
 :db-spec {:dbtype   #parameter "/adserver/test/db/type"
           :dbname   #parameter "/adserver/test/db/name"
           :host     #parameter "/adserver/test/db/host"
           :port     #long #parameter "/adserver/test/db/port"
           :user     #parameter "/adserver/test/db/user"
           :password #credential "/adserver/test/db/password"}}

The tag #long is built in to aero and will coerce the string value of the parameter to a long, as expected by clojure.java.jdbc/get-connection. The #parameter and #credential tags don't exist yet; we would like them to lookup a String or SecureString parameter in the parameter store and interpolate this value into the configuration.

We implement these new tags by extending the aero.core/reader multimethod. The excellent amazonica library makes our interactions with the Amazon API straightforward, so our entire implementation looks like:

(ns metail.aero
  (:require [aero.core]
            [amazonica.aws.simplesystemsmanagement :as ssm]))

(defmethod aero.core/reader 'credential
  [{:keys [profile] :as opts} tag value]
  (let [{:keys [parameter]} (ssm/get-parameter {:name value :with-decryption true})]
    (:value parameter)))

(defmethod aero.core/reader 'parameter
  [{:keys [profile] :as opts} tag value]
  (let [{:keys [parameter]} (ssm/get-parameter {:name value :with-decryption false})]
    (:value parameter)))

(def read-config aero.core/read-config)

We have included read-config only for convenience; it allows users to import just one namespace to read a configuration with our new tags.

Access Management #

The AWS parameter store and KMS are fully integrated with AWS IAM, so there is lots of flexibility around how you grant access to keys and parameters. We will create a KMS key for each application and grant the application kms:Decrypt permission on its key through an IAM policy. We will also restrict the parameters it can read to just those it needs through the same policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "Statement1",
      "Effect": "Allow",
      "Action": [
        "kms:Decrypt",
        "ssm:GetParameters",
        "ssm:GetParameter",
        "ssm:GetParametersByPath"
      ],
      "Resource": [
        "arn:aws:ssm:*:*:parameter/adserver/*",
        "arn:aws:kms:*:*:key/f9870095-fc23-4b99-b11f-69252e714a1f"
      ]
    }
  ]
}

The AWS blog post mentioned earlier shows how to configure a policy for ECS applications. We are currently deloying directly to EC2, so we also create an EC2 instance profile and attach the application role. This profile is attached to the EC2 instance when it is launched and the application can retrieve and decrypt its configuration without further ado.

Conclusion #

Using aero, we can configure our applications from a static file, embedded environment variables, or parameters stored securely and retrieved from AWS SSM. We can switch between these modes simply by changing the configuration file: the application itself does not need to change. This allow us to switch seamlessly between a static configuration during local development and a secure parameter store when deploying to the cloud.