Storing secrets in Parameter Store for Node.js applications
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.