Skip to content

Commit

Permalink
Merge 364eaf5 into ec24956
Browse files Browse the repository at this point in the history
  • Loading branch information
aceew committed Mar 19, 2017
2 parents ec24956 + 364eaf5 commit 57639dd
Show file tree
Hide file tree
Showing 4 changed files with 377 additions and 19 deletions.
79 changes: 72 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,19 @@

The purpose of this package is the easily decrypt and fetch environment variables in Lambda functions, using KMS for decryption. The package supports getting environment variables that have been encrypted in Lambda using a default service key, however the main purpose is for decrypting variables that were encrypted using a custom KMS key. For more information on Lambda environment variables and encryption keys, see the [AWS Documentation](http://docs.aws.amazon.com/lambda/latest/dg/env_variables.html).

Before implementing it is recommended you read the [notes](#notes) section
Before implementing it is recommended you read the [FAQs](#faqs) section

## Contents
- [Usage](#usage)
- [Notes](#notes)
- [FAQs](#faqs)
- [Contributing](#contributing)

## Usage

### AWS config
When using encrypted environment variables you will need to create a KMS key in IAM and give usage permission to the role that your Lambda function has been assigned. You then need to configure your Lambda function to use the new KMS key by default. This can be found in the Lambda function under
`Configuration -> Advanced settings -> KMS key`.

### Add lambda-env-vars to your project
```console
$ npm install --save lambda-env-vars
Expand Down Expand Up @@ -63,6 +68,59 @@ exports.handler = (event, context, callback) => {

## API Reference

### Setting default config parameters
The methods `getCustomDecryptedValue` and `getCustomDecryptedValueList` both accept a second parameter object to allow users to specify the location ('s3' or 'lambdaConfig') of the environment variables. These parameters can be defined by default by specifying them when the class is initially instanced. For example:
```javascript
import LambdaEnvVars from 'lambda-env-vars';
const lambdaEnvVars = new LambdaEnvVars({
location: 's3',
s3Config: {
bucketName: 'my-env-var-bucket',
fileName: 'my-env-var-filename.json',
},
});
```

The default parameters can be overridden by simple specifying them on function call:
```javascript
const params = { location: 'lambdaConfig' };
lambdaEnvVars.getCustomDecryptedValue('variableName', params)
.then((variableName) => {
console.log(variableName);
// variableName will be fetched from the lambdaConfig
});
```

### <a name="config-params"></a>Config parameters
The default config parameters object specifies that the source of environment variables should be the 'lambdaConfig', this is where users can define lambda env variables in the AWS console. Here's what that object looks like:
```javascript
{
location: 'lambdaConfig',
s3Config: {},
}
```

Optionally the source of environment variables can be a JSON file within an S3 bucket. This is good for situations where your Lambda function config exceeds 4KB and you still need to store more environment variables. Encryption where the Lambda function environment variables are in S3 should be achieved with [SSE-S3](http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingServerSideEncryption.html) or [SSE-KMS](http://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html). I'd personally recommend setting up a bucket policy to stop uploads that aren't encrypted at rest, just to be safe. One further thing to consider is your Lambda function's role will need a policy to allow reads to the bucket the environment variable file is stored in. The parameters required when using S3 as the source of the variables should look like the following:
```javascript
{
location: 's3',
s3Config: {
bucketName: 'name-of-bucket',
fileName: 'filename.json', // Name of the JSON file containing the env vars
},
}
```

The available attributes of the params object are as follows:

| Name | Type | Default | Info |
| --- | --- | --- | --- |
| location | string | 'lambdaConfig' | Can be 'lambdaConfig' or 's3'. The location the environment variables are stored.
| s3Config | object | {} | Required when location is equal to 's3'. Fields that specify the location of the env var JSON file |
| s3Config.bucketName | string | | The name of the bucket containing the env var file. |
| s3Config.fileName | string | | The file name of the env var JSON file. |


### Decrypt an environment variable that uses a custom KMS key
Uses KMS to decrypt the cipher text stored under the environment variable of the specified key name. Caches the decrypted variable in the global scope so it is only decrypted once per container, cutting down on KMS decryption costs.

Expand All @@ -74,6 +132,7 @@ Parameters:
| Name | Type | Default | Info |
| --- | --- | --- | --- |
| variableName | string | '' | The key in process.env to which the variable is stored under. |
| configParams | object | [configParams](#config-params) | Optional. Information that allows the package to know where to get the variable from. For more info see the [config params section](#config-params) |

Returns a promise that resolves the decrypted value, or rejects an error if there were issues connecting to KMS or issues with the encrypted payload.

Expand All @@ -90,6 +149,7 @@ Parameters:
| Name | Type | Default | Info |
| --- | --- | --- | --- |
| variableNames | Array | [] | Keys in process.env to which encrypted environment variables are stored under. |
| configParams | object | [configParams](#config-params) | Optional. Information that allows the package to know where to get the variables from. For more info see the [config params section](#config-params) |

Returns an object containing the decrypted values where the keys are the items specified in the params `variableNames`.

Expand All @@ -108,12 +168,17 @@ Parameters:

Returns the string value of the environment variable. No decryption takes place in code as this is done before Lambda is called.

## <a name="faqs"></a>FAQs

### My Lambda config exceeds 4KB. What do I do?
Lambda imposes a 4KB limit on function config, this is inclusive of environment variables. and by using a few encrypted environment variables it easy to quickly reach this limit. The way this package recommends using environment variables when the Lambda config reaches 4KB is by storing a file in S3 that is encrypted at rest and pulling this down on the first invocation of the Lambda function and caching it for reduced calls to S3. For using environment variables stored in s3, see the [config parameters section](#config-params).

### Why is the aws-sdk a dev dependency?
The package depends on the aws-sdk, however it is not listed as a dependency as it should be installed on your lambda environment by default.

### Doesn't KMS decryption get quite expensive?
Yes, however as it is recommended in AWS's KMS helper code, the decrypted variables are stored in memory so only the first invocation of a Lambda function container incurs a KMS cost. All requests after this point will receive the var stored in memory.

## Notes
- In order to use the decryption feature you'll have to set a KMS encryption key on your lambda function.
- The package depends on the aws-sdk, however it is not listed as a dependency as it should be installed on your lambda environment by default.
- The package stores decrypted variables outside the handler so that variables are only encrypted once per lambda container.
- The current version of the interface relies on Promises, callback support will be added in the future.

## Contributing
- Start a feature branch from master
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"main": "index.js",
"scripts": {
"test": "npm run-script lint && nyc ava test && nyc report --reporter=text-lcov | coveralls",
"test-local": "npm run-script lint && nyc ava test --verbose && nyc report --reporter=lcov",
"lint": "eslint src/*.js test/*.js",
"build": "babel src -d build && cp package.json build/package.json && cp README.md build/README.md",
"deploy": "npm run-script build && cd build && npm publish"
Expand Down
142 changes: 131 additions & 11 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,43 @@
const AWS = require('aws-sdk');

const decryptedVariables = {};
const s3StoredVariables = {};

export default class LambdaEnvVars {
/**
* Injects the node process API and sets the isntance of AWS KMS.
*
* @param {Object} params
* Default params to be sent with each request.
*
* @param {string} params.location
* Location of the environment variables. ENUM ('lambdaConfig', 's3')
*
* @param {Object} params.s3Config
* Config used to get the an env var file from S3.
*
* @param {string} params.s3Config.bucketName
* @param {string} params.s3Config.fileName
*
* @return {Object}
* Instance of EnvVars.
*/
constructor() {
constructor(params = {}) {
this.defaultParams = {
location: 'lambdaConfig',
s3Config: {},
};

this.defaultParams = Object.assign(this.defaultParams, params);
this.process = process;
this.kms = new AWS.KMS();
this.kms = new AWS.KMS({ apiVersion: '2014-11-01' });
this.s3 = new AWS.S3({ apiVersion: '2006-03-01' });
this.decryptedVariables = decryptedVariables;
this.s3Vars = s3StoredVariables;
this.availableStoreLocations = [
's3',
'lambdaConfig',
];
}

/**
Expand All @@ -36,21 +61,113 @@ export default class LambdaEnvVars {
* @param {string} variableName
* The key in process.env to which the variable is stored under.
*
* @param {Object} params
* Params to state where the environment variable is stored.
*
* @param {string} params.location
* Location of the environment variables. ENUM ('lambdaConfig', 's3')
*
* @param {Object} params.s3Config
* Config used to get the an env var file from S3.
*
* @param {string} params.s3Config.bucketName
* @param {string} params.s3Config.fileName
*
* @return {Promise}
* A promise that resolves the value if it is available, else an empty string if it not set in
* the node environment variables, or a rejected promise if KMS couldn't decypt the value.
*/
getCustomDecryptedValue(variableName = '') {
if (this.decryptedVariables[variableName]) {
return Promise.resolve(this.decryptedVariables[variableName]);
getCustomDecryptedValue(variableName = '', params = {}) {
return this.buildParams(params)
.then((builtParams) => {
if (builtParams.location === 's3') {
return this.getVarFromS3File(variableName, builtParams.s3Config);
}

if (this.decryptedVariables[variableName]) {
return Promise.resolve(this.decryptedVariables[variableName]);
}

if (variableName === '' || !this.process.env[variableName]) {
return Promise.resolve('');
}

return this.decryptVariable(variableName)
.then(result => this.setEncryptedVariable(variableName, result));
});
}

/**
* Gets an env var from a file within S3.
*
* @param {string} variableName
* The key in process.env to which the variable is stored under.
*
* @param {Object} s3Config
* Config on filename, bucket name etc.
*
* @return {Promise}
* Promise that resolves the variable name
*/
getVarFromS3File(variableName, s3Config) {
const fileKey = s3Config.bucketName + s3Config.fileName;

if (this.s3Vars[fileKey]) {
return Promise.resolve(this.s3Vars[fileKey][variableName]);
}

const s3Params = {
Key: s3Config.fileName,
Bucket: s3Config.bucketName,
};

return this.s3.getObject(s3Params).promise()
.then(result => JSON.parse(result.Body.toString()))
.then((s3File) => {
this.s3Vars[fileKey] = s3File;
return this.s3Vars[fileKey][variableName];
})
.catch(() => {
const errorMessage = 'Could not successfully load variable from s3 file. Please make sure the file is valid JSON and that the lambda function has the sufficient role to get the S3 file.';
throw new Error(errorMessage);
});
}

/**
* Validates the parameters that should state where to get the environment variables from. Returns
* the built parameters.
*
* @param {Object} params
* Object containing the parameters.
*
* @param {string} params.location
* Location of the environment variables. ENUM ('lambdaConfig', 's3')
*
* @return {Promise}
* Resolves the params for the call or rejects an error.
*/
buildParams(params = {}) {
const callParams = Object.assign({}, this.defaultParams, params);

if (this.availableStoreLocations.indexOf(callParams.location) < 0) {
const availableOptions = this.availableStoreLocations.join(', ');
const errorMessage = `Field 'location' must be one of the following ${availableOptions}`;
return Promise.reject(new Error(errorMessage));
}

if (variableName === '' || !this.process.env[variableName]) {
return Promise.resolve('');
if (
params.location === 's3' &&
(
!params.s3Config ||
!params.s3Config.bucketName ||
!params.s3Config.fileName
)
) {
const errorMessage = 's3Config.bucketName and s3Config.fileName are required when location is \'s3\'';
return Promise.reject(new Error(errorMessage));
}

return this.decryptVariable(variableName)
.then(result => this.setEncryptedVariable(variableName, result));
return Promise.resolve(callParams);
}

/**
Expand All @@ -60,15 +177,18 @@ export default class LambdaEnvVars {
* @param {string[]} variableNames
* An array of environment variable keys to decrypt.
*
* @param {Object} params
* Params to state where the environment variable is stored.
*
* @return {Promise}
* A promise that resolves an object containing the decrypted values where the keys are the items
* specified in the params variableNames.
*/
getCustomDecryptedValueList(variableNames = []) {
getCustomDecryptedValueList(variableNames = [], params = {}) {
const decryptedVariablesObject = {};

const decryptedValuePromiseList = variableNames.map(envVar => (
this.getCustomDecryptedValue(envVar)
this.getCustomDecryptedValue(envVar, params)
.then((decryptedValue) => {
decryptedVariablesObject[envVar] = decryptedValue;
})
Expand Down
Loading

0 comments on commit 57639dd

Please sign in to comment.