Storing secrets in Parameter Store for Node.js applications

It's a bad practice to hardcode passwords and secrets in the application code and it still happens from time to time. Secrets are supposed to be kept in secret, and as such, they should be stored in a secure place. In this post I'll write about storing secrets in Parameter Store.

AWS Parameter Store (also known as SSM) is part of the Systems Manager service stack. It allows users to securely store passwords, API keys and URLs as parameter values.

1. About Parameter Store

Parameters are stored as key/value pairs in Parameter Store.

1.1. Types of strings

Three types of strings can be stored: String, StringList and SecureString.

String can be anything, for example an endpoint URL which we don’t want to hardcode in our application.

StringList as its name implies is a comma separated list of multiple strings.

SecureString is an encrypted string, which is a perfect solution for storing passwords, API keys and secrets. I’ll use the SecureString parameter type in this post.

1.2. Supports versioning

Multiple versions of the same parameter can be stored in Parameter Store. If this is the case, the GetParameter API (more on that later) will return the latest version by default. If an earlier version is needed, the version number needs to be specified when making calls to the API.

The size of the parameters cannot exceed 4 kB for a standard parameter.

Standard parameters (like the one used in this post) are available at no cost.

1.3. Encryption

The SecureString parameter values are encrypted using the default AWS-managed KMS Customer Master Key. The encryption key has the aws/ssm alias and it provides a direct encryption of the parameter in case of the default tier.

It’s possible to use a Customer Managed Key (Customer Managed CMK), in this case the key-id needs to be specified when the parameter is created.

2. Create a parameter

Let’s say that we are building an authentication app where JWT is used in each request.

JWTs are signed and verified with a secret which should never be hardcoded in the application but should be stored in a secure place instead.

Parameter Store is a good choice for the role of the secure place, so let’s create the parameter using AWS CLI:

aws ssm put-parameter --name JWT-Secret --type 'SecureString' \\
--value '8547f677-df4b-4fe5-abe6-8fde9962a26b'

This command will create a SecureString named JWT-Secret (because it will be a JWT secret), and the value of the secret is the 8547f677-df4b-4fe5-abe6-8fde9962a26b UUID. UUIDs are good choices for secrets; they are (should be) unique and hard to figure out.

We don’t specify the key-id, so the JWT-Secret will be encrypted with the AWS-managed CMK. As for the version AWS will automatically assign the parameter version 1.

JWT-Secret is also called a root-level parameter. Parameter Store supports path parameters (they have the format of /path/to/parameter), which can be queried based on paths. More on parameter hierarchy will come in a later post.

It’s also a standard secure string parameter. It’s possible to create advanced parameters which use envelope encryption by defining --tier Advanced in the command. Advanced parameters and envelope encryption are out of scope for this post but will be discussed in the future.

3. Check the parameter

The GetParameter API will return the value of a given parameter:

aws ssm get-parameter --name JWT-Secret --with-decryption

The only required option is name. The with-decryption option (as the name implies) will return the parameter’s original, decrypted value:

{
  "Parameter": {
    "Name": "JWT-Secret",
    "Type": "SecureString",
    "Value": "8547f677-df4b-4fe5-abe6-8fde9962a26b",
    "Version": 1,
    "LastModifiedDate": 1575153932.655,
    "ARN": "arn:aws:ssm:us-west-2:123456789123:parameter/JWT-Secret"
  }
}

If the with-decryption option is not specified (or the no-with-decryption one is present), the response will have the Value property encrypted.

4. Apply it in the code

First, the AWS SDK needs to be installed:

npm install aws-sdk

After the installation is complete, we’ll need to require it in the file that manages JWT. In our case this file will be called server.js:

const AWS = require('aws-sdk');

const ssm = new AWS.SSM({
  apiVersion: '2014-11-06',
  region: 'us-west-2',
});

The region needs to be specified (it should be the same region where the put-parameter command was executed, otherwise the parameter won’t be found) and it might be a good idea to lock the apiVersion (although it hardly ever changes).

We can build a simple in-memory cache solution for the JWT secret, so that Parameter Store is not called on every request to the server when JWT is verified:

let jwtSecret = '';

const getSecret = async () => {
  const params = {
    Name: 'JWT-Secret',
  };

  if (!jwtSecret) {
    jwtSecret = (await ssm.getParameter(params).promise()).Parameter.Value;
  }

  return jwtSecret;
};

const authorize = async (req, res, next) => {
  // ...
  try {
    const decoded = jwt.verify(token, await getSecret());
    // ...
  } catch (e) {
    return next(new CustomError('Error while getting secret', 400));
  }
  // ...
};

As with the get-parameter CLI command, the getParameter method only needs the Name property (they use the same GetParameter API). Similarly to most SDK-methods, getParameter comes with the promise method, which allows us to avoid callbacks and use async/await instead.

The GetParameter API responds with an object (see above), and we’ll need the Value property of the Parameter object. When the JWT is verified in the authorize middleware, we can just call getSecret, which will return the cached secret (if exists) or fetch the secret from the Parameter Store and saves it to the cache (if it’s not cached yet).

We can use the getSecret method when the JWT is signed upon logging in:

app.post('/login', async (req, res, next) => {
  // ...
  const signed = jwt.sign({
      username: user,
      exp: Math.floor(Date.now() / 1000) + 3600,
    }, await getSecret());
  // ...
});

The logic is the same, getSecret will return the cached secret or will get it from Parameter Store.

The code samples have been extracted from an earlier post on using JWT in authenticating users. The full code can be found by following the link.

5. Clean up

If the parameter is not used any more, it will be a good idea to delete it:

aws ssm delete-parameter --name JWT-Secret

Again, the only required option is the name of the parameter.

6. Summary

AWS Parameter Store is a convenient way to store passwords, secrets and API keys. It supports encryption, which is done with the AWS-managed KMS CMK by default, but it’s possible to use a Customer Managed Key for encryption.

Storing secrets in Parameter Store increases the level of security of the application.

Thanks for reading and see you next time.