Side project: simple and secure fetching from AWS Secrets Manager
Background
AWS Secrets Manager provides a great central place to store secrets that can be retrieved by way of Kubernetes CSI drivers, AWS SSM Parameters, and so on and are definitely the recommended way to fetch and interact with secrets in most scenarios.
But inevitably there are those moments where a developer needs to access a secret from within an application or a build script without using environment variable injection or other mechanisms. When that time comes they usually will need to use the AWS SDK's raw client to fetch the secret material, parse it, and then securely dispose of it, ensuring the material isn't accidentally logged all along the way.
And thus @freakyfelt/secrets-js was born.
Goals
But before diving into the library, some goals I had laid out for the implementation
Keep it secure
First and foremost the library aims to make it easy to do the safe thing. This includes hiding the contents of sensitive fields from console output, handling JSON parsing exceptions when parsing the key material, and always returning copies of data to prevent poisoning.
Keep it natural
Next I wanted to make a library that felt natural for modern JavaScript developers. The library takes inspiration from the fetch() API and includes helpers for text(), json(), and bytes() for parsing a secret, handling when the secret payload was returned by SecretString or SecretBinary.
Simultaneously, the library still needs to feel like it's part of the AWS SDK, so it maintains argument names and also a raw() method to get the response that was returned from the SDK.
Keep it simple
Security is hard and adding more layers of abstraction and indirection adds further risk. The library adds complexity where it adds value (such as redacting data), but otherwise tries to stay out of the way.
Getting started
Once the library is installed into your project you will need to initialise an instance of the fetcher.
import { SecretsManager } from '@aws-sdk/client-secrets-manager' import { SecretsFetcher } from '@freakyfelt/secrets-js' const secretsFetcher = new SecretsFetcher(new SecretsManager());`
From there you can use the fetcher to fetch secrets. The response is a wrapped version of the AWS Secrets Manager GetSecretValue response that provides safety from accidental log output, plus helper methods for fetching secret materials
// returns a SecretValue const dbCreds = await secretsFetcher.fetch("arn:aws:secretsmanager:us-east-1:01234567890:secret:my_project/test/pg_credentials-1a2b3c"); // Will output "SecretValue(string) { Name: "...", VersionId: "..." }" instead of the raw input object console.log(dbCreds) dbCreds.ARN // => "arn:aws:secretsmanager:us-east-1:01234567890:secret:my_project/test/pg_credentials-1a2b3c" dbCreds.Name // => "my_project/test/pg_credentials" dbCreds.VersionId // => "${uuid}" dbCreds.VersionStages // => ["AWSCURRENT"] dbCreds.CreatedDate // => Date dbCreds.metadata() // => { ARN, Name, VersionId, VersionStages, CreatedDate } // "string" if the secret was stored as a string, "binary" if the secret was stored as a binary dbCreds.payloadType // => "string" // text() returns the text from the `SecretString` field const str = await dbCreds.text(); // json() method parses the `SecretString` field to JSON or safely throws a SecretParseError with only the ARN if unparseable const { hostname, username, password } = await dbCreds.json(); // => SecretParseError("Could not parse secret as JSON", { arn }) // bytes() returns a Buffer for cases such as X.509 certificates. Will also convert string secrets to a buffer const buf = await dbCreds.bytes();
Only the beginning
The library includes other useful utilities for fetching secrets by VersionStage (useful for rotation Lambda functions) as well as shorthand methods for directly receiving JSON or string objects instead of the intermediate response, but I'll leave that for the README.md.
Some other ideas I have in mind for the library:
- Caching / refresh wrapper: create a wrapper that can periodically check for updated secrets while avoiding latency spikes or thundering herd problems (GitHub issue)
Conclusion
So. Does it look useful to you? See something wrong or have an idea? Open up an issue in the Issues and let me know, otherwise happy to see what people have to say about it.