Repository: serverless/serverless (JavaScript)
Date: 2017-05-24 08:52:13+00:00
Task type: Feature

Issue:
Add KMS key selection to AWS provider
<!--
1. If you have a question and not a bug/feature request please ask it at http://forum.serverless.com
2. Please check if an issue already exists so there are no duplicates
3. Check out and follow our Guidelines: https://github.com/serverless/serverless/blob/master/CONTRIBUTING.md
4. Fill out the whole template so we have a good overview on the issue
5. Do not remove any section of the template. If something is not applicable leave it empty but leave it in the Issue
6. Please follow the template, otherwise we'll have to ask you to update it
-->

# This is a Feature Proposal

## Description
Add the ability to select a KMS key in the AWS provider and/or function configuration.

AWS Lambdas now support environment variables and selecting a KMS key used at runtime. The KMS key is used by AWS to encrypt environment variables at rest. Serverless configuration should allow selecting a KMS key.

This would only be a small change to allow full configuration of the KMS key.

For feature proposals:
* Use case: Use a KMS key other than the default lambda KMS key.
* Possible configuration example:
```
provider:
  kmsKeyArn: arn:aws:kms:us-east-1:1234567890:key/76aa38ca-17b7-4c96-9a89-38df27cbeafe

function:
  kmsKeyArn: arn:aws:kms:us-east-1:1234567890:key/76aa38ca-17b7-4c96-9a89-38df27cbeafe
```

Similar or dependent issues:
* #472
* [secrets-serverless-plugin issue](https://github.com/ServerlessInc/serverless-secrets-plugin/issues/1)

## Additional Data

* Cloudformation docs: [Lambda KmsKeyArn](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html#cfn-lambda-function-kmskeyarn)



Issue discussion:
Any news on this? It would be an amazing feature.
@aasmoura we had the PR #2998 from @DonMcNamara which needed some additional updates but was almost there. Maybe it's worth to look into it and build a plugin based on it.

There are currently no direct plans to add this feature into core, but some more feedback on the potential use-cases / problems you face without this in place would be really helpful!
Hrmm this is a tough one since the point of encrypting it is to keep it secure, but that point is negated by having stuff you want to encrypt in the yml files, the main benefit I think is as part of a CI pipeline where on deployment to a new stage the CI pipeline pulls from a private repo with the correct config and env variables in it, you'd probably then want to encrypt them via KMS. 

Then I guess that if you allow KMS encryption via serverless.yml then you're encouraging people to put keys in code all over the place - my general feeling is, because this is a security feature then it should be implemented. In the EU currently we're about to get new regulation around making systems and user data as secure possible, and without this companies are going to have reservations about serverless as a framework.



Solution patch:
diff --git a/docs/providers/aws/guide/functions.md b/docs/providers/aws/guide/functions.md
index 06f6d4a8ba2..bc07ab2ba41 100644
--- a/docs/providers/aws/guide/functions.md
+++ b/docs/providers/aws/guide/functions.md
@@ -329,3 +329,33 @@ functions:
 The `onError` config currently only supports SNS topic arns due to a race condition when using SQS queue arns and updating the IAM role.
 
 We're working on a fix so that SQS queue arns are be supported in the future.
+
+## KMS Keys
+
+AWS Lambda uses [AWS Key Management Service (KMS)](https://aws.amazon.com/kms/) to encrypt your environment variables at rest.
+
+The `awsKmsKeyArn` config variable enables you a way to define your own KMS key which should be used for encryption.
+
+```yml
+service:
+  name: service-name
+  awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash
+
+provider:
+  name: aws
+  environment:
+    TABLE_NAME: tableName1
+
+functions:
+  hello: # this function will OVERWRITE the service level environment config above
+    handler: handler.hello
+    awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash
+    environment:
+      TABLE_NAME: tableName2
+  goodbye: # this function will INHERIT the service level environment config above
+    handler: handler.goodbye
+```
+
+### Secrets using environment variables and KMS
+
+When storing secrets in environment variables, AWS [strongly suggests](http://docs.aws.amazon.com/lambda/latest/dg/env_variables.html#env-storing-sensitive-data) encrypting sensitive information. AWS provides a [tutorial](http://docs.aws.amazon.com/lambda/latest/dg/tutorial-env_console.html) on using KMS for this purpose.
diff --git a/docs/providers/aws/guide/serverless.yml.md b/docs/providers/aws/guide/serverless.yml.md
index 05c1bf30188..e4faea41d23 100644
--- a/docs/providers/aws/guide/serverless.yml.md
+++ b/docs/providers/aws/guide/serverless.yml.md
@@ -17,7 +17,9 @@ Here is a list of all available properties in `serverless.yml` when the provider
 ```yml
 # serverless.yml
 
-service: myService
+service:
+  name: myService
+  awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Optional KMS key arn which will be used for encryption for all functions
 
 frameworkVersion: ">=1.0.0 <2.0.0"
 
@@ -73,6 +75,7 @@ functions:
     timeout: 10 # Timeout for this specific function.  Overrides the default set above.
     role: arn:aws:iam::XXXXXX:role/role # IAM role which will be used for this function
     onError: arn:aws:sns:us-east-1:XXXXXX:sns-topic # Optional SNS topic arn which will be used for the DeadLetterConfig
+    awsKmsKeyArn: arn:aws:kms:us-east-1:XXXXXX:key/some-hash # Optional KMS key arn which will be used for encryption (overwrites the one defined on the service level)
     environment: # Function level environment variables
       functionEnvVar: 12345678
     tags: # Function specific tags
diff --git a/lib/plugins/aws/package/compile/functions/index.js b/lib/plugins/aws/package/compile/functions/index.js
index f8ad0622967..a69bab17f04 100644
--- a/lib/plugins/aws/package/compile/functions/index.js
+++ b/lib/plugins/aws/package/compile/functions/index.js
@@ -182,6 +182,51 @@ class AwsCompileFunctions {
       }
     }
 
+    let kmsKeyArn;
+    const serviceObj = this.serverless.service.serviceObject;
+    if ('awsKmsKeyArn' in functionObject) {
+      kmsKeyArn = functionObject.awsKmsKeyArn;
+    } else if (serviceObj && 'awsKmsKeyArn' in serviceObj) {
+      kmsKeyArn = serviceObj.awsKmsKeyArn;
+    }
+
+    if (kmsKeyArn) {
+      const arn = kmsKeyArn;
+
+      if (typeof arn === 'string') {
+        const splittedArn = arn.split(':');
+        if (splittedArn[0] === 'arn' && (splittedArn[2] === 'kms')) {
+          const iamRoleLambdaExecution = this.serverless.service.provider
+            .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution;
+
+          newFunction.Properties.KmsKeyArn = arn;
+
+          const stmt = {
+            Effect: 'Allow',
+            Action: [
+              'kms:Decrypt',
+            ],
+            Resource: [arn],
+          };
+
+          // update the PolicyDocument statements (if default policy is used)
+          if (iamRoleLambdaExecution) {
+            iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement = _.unionWith(
+              iamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement,
+              [stmt],
+              _.isEqual
+            );
+          }
+        } else {
+          const errorMessage = 'awsKmsKeyArn config must be a KMS key arn';
+          throw new this.serverless.classes.Error(errorMessage);
+        }
+      } else {
+        const errorMessage = 'awsKmsKeyArn config must be provided as a string';
+        throw new this.serverless.classes.Error(errorMessage);
+      }
+    }
+
     if (functionObject.environment || this.serverless.service.provider.environment) {
       newFunction.Properties.Environment = {};
       newFunction.Properties.Environment.Variables = Object.assign(


Test patch:
diff --git a/lib/plugins/aws/package/compile/functions/index.test.js b/lib/plugins/aws/package/compile/functions/index.test.js
index dd5cbbab8a3..7dc8c70f3e1 100644
--- a/lib/plugins/aws/package/compile/functions/index.test.js
+++ b/lib/plugins/aws/package/compile/functions/index.test.js
@@ -724,6 +724,282 @@ describe('AwsCompileFunctions', () => {
       });
     });
 
+    describe('when using awsKmsKeyArn config', () => {
+      let s3Folder;
+      let s3FileName;
+
+      beforeEach(() => {
+        s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
+        s3FileName = awsCompileFunctions.serverless.service.package.artifact
+          .split(path.sep).pop();
+      });
+
+      it('should throw an error if config is provided as a number', () => {
+        awsCompileFunctions.serverless.service.functions = {
+          func: {
+            handler: 'func.function.handler',
+            name: 'new-service-dev-func',
+            awsKmsKeyArn: 12,
+          },
+        };
+
+        expect(() => { awsCompileFunctions.compileFunctions(); })
+          .to.throw(Error, 'provided as a string');
+      });
+
+      it('should throw an error if config is provided as an object', () => {
+        awsCompileFunctions.serverless.service.functions = {
+          func: {
+            handler: 'func.function.handler',
+            name: 'new-service-dev-func',
+            awsKmsKeyArn: {
+              foo: 'bar',
+            },
+          },
+        };
+
+        expect(() => { awsCompileFunctions.compileFunctions(); })
+          .to.throw(Error, 'provided as a string');
+      });
+
+      it('should throw an error if config is not a KMS key arn', () => {
+        awsCompileFunctions.serverless.service.functions = {
+          func: {
+            handler: 'func.function.handler',
+            name: 'new-service-dev-func',
+            awsKmsKeyArn: 'foo',
+          },
+        };
+
+        expect(() => { awsCompileFunctions.compileFunctions(); })
+          .to.throw(Error, 'KMS key arn');
+      });
+
+      it('should use a the service KMS key arn if provided', () => {
+        awsCompileFunctions.serverless.service.serviceObject = {
+          name: 'new-service',
+          awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar',
+        };
+
+        awsCompileFunctions.serverless.service.functions = {
+          func: {
+            handler: 'func.function.handler',
+            name: 'new-service-dev-func',
+          },
+        };
+
+        const compiledFunction = {
+          Type: 'AWS::Lambda::Function',
+          DependsOn: [
+            'FuncLogGroup',
+            'IamRoleLambdaExecution',
+          ],
+          Properties: {
+            Code: {
+              S3Key: `${s3Folder}/${s3FileName}`,
+              S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+            },
+            FunctionName: 'new-service-dev-func',
+            Handler: 'func.function.handler',
+            MemorySize: 1024,
+            Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+            Runtime: 'nodejs4.3',
+            Timeout: 6,
+            KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar',
+          },
+        };
+
+        awsCompileFunctions.compileFunctions();
+
+        const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
+          .compiledCloudFormationTemplate;
+        const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
+        expect(functionResource).to.deep.equal(compiledFunction);
+      });
+
+      it('should prefer a function KMS key arn over a service KMS key arn', () => {
+        awsCompileFunctions.serverless.service.serviceObject = {
+          name: 'new-service',
+          awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/service',
+        };
+
+        awsCompileFunctions.serverless.service.functions = {
+          func1: {
+            handler: 'func1.function.handler',
+            name: 'new-service-dev-func1',
+            awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/function',
+          },
+          func2: {
+            handler: 'func2.function.handler',
+            name: 'new-service-dev-func2',
+          },
+        };
+
+        const compiledFunction1 = {
+          Type: 'AWS::Lambda::Function',
+          DependsOn: [
+            'Func1LogGroup',
+            'IamRoleLambdaExecution',
+          ],
+          Properties: {
+            Code: {
+              S3Key: `${s3Folder}/${s3FileName}`,
+              S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+            },
+            FunctionName: 'new-service-dev-func1',
+            Handler: 'func1.function.handler',
+            MemorySize: 1024,
+            Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+            Runtime: 'nodejs4.3',
+            Timeout: 6,
+            KmsKeyArn: 'arn:aws:kms:region:accountid:foo/function',
+          },
+        };
+
+        const compiledFunction2 = {
+          Type: 'AWS::Lambda::Function',
+          DependsOn: [
+            'Func2LogGroup',
+            'IamRoleLambdaExecution',
+          ],
+          Properties: {
+            Code: {
+              S3Key: `${s3Folder}/${s3FileName}`,
+              S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+            },
+            FunctionName: 'new-service-dev-func2',
+            Handler: 'func2.function.handler',
+            MemorySize: 1024,
+            Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+            Runtime: 'nodejs4.3',
+            Timeout: 6,
+            KmsKeyArn: 'arn:aws:kms:region:accountid:foo/service',
+          },
+        };
+
+        awsCompileFunctions.compileFunctions();
+
+        const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
+          .compiledCloudFormationTemplate;
+
+        const function1Resource = compiledCfTemplate.Resources.Func1LambdaFunction;
+        const function2Resource = compiledCfTemplate.Resources.Func2LambdaFunction;
+        expect(function1Resource).to.deep.equal(compiledFunction1);
+        expect(function2Resource).to.deep.equal(compiledFunction2);
+      });
+
+      describe('when IamRoleLambdaExecution is used', () => {
+        beforeEach(() => {
+          // pretend that the IamRoleLambdaExecution is used
+          awsCompileFunctions.serverless.service.provider
+            .compiledCloudFormationTemplate.Resources.IamRoleLambdaExecution = {
+              Properties: {
+                Policies: [
+                  {
+                    PolicyDocument: {
+                      Statement: [],
+                    },
+                  },
+                ],
+              },
+            };
+        });
+
+        it('should create necessary resources if a KMS key arn is provided', () => {
+          awsCompileFunctions.serverless.service.functions = {
+            func: {
+              handler: 'func.function.handler',
+              name: 'new-service-dev-func',
+              awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar',
+            },
+          };
+
+          const compiledFunction = {
+            Type: 'AWS::Lambda::Function',
+            DependsOn: [
+              'FuncLogGroup',
+              'IamRoleLambdaExecution',
+            ],
+            Properties: {
+              Code: {
+                S3Key: `${s3Folder}/${s3FileName}`,
+                S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+              },
+              FunctionName: 'new-service-dev-func',
+              Handler: 'func.function.handler',
+              MemorySize: 1024,
+              Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+              Runtime: 'nodejs4.3',
+              Timeout: 6,
+              KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar',
+            },
+          };
+
+          const compiledKmsStatement = {
+            Effect: 'Allow',
+            Action: [
+              'kms:Decrypt',
+            ],
+            Resource: ['arn:aws:kms:region:accountid:foo/bar'],
+          };
+
+          awsCompileFunctions.compileFunctions();
+
+          const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
+            .compiledCloudFormationTemplate;
+
+          const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
+          const dlqStatement = compiledCfTemplate.Resources
+            .IamRoleLambdaExecution.Properties.Policies[0].PolicyDocument.Statement[0];
+
+          expect(functionResource).to.deep.equal(compiledFunction);
+          expect(dlqStatement).to.deep.equal(compiledKmsStatement);
+        });
+      });
+
+      describe('when IamRoleLambdaExecution is not used', () => {
+        it('should create necessary function resources if a KMS key arn is provided', () => {
+          awsCompileFunctions.serverless.service.functions = {
+            func: {
+              handler: 'func.function.handler',
+              name: 'new-service-dev-func',
+              awsKmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar',
+            },
+          };
+
+          const compiledFunction = {
+            Type: 'AWS::Lambda::Function',
+            DependsOn: [
+              'FuncLogGroup',
+              'IamRoleLambdaExecution',
+            ],
+            Properties: {
+              Code: {
+                S3Key: `${s3Folder}/${s3FileName}`,
+                S3Bucket: { Ref: 'ServerlessDeploymentBucket' },
+              },
+              FunctionName: 'new-service-dev-func',
+              Handler: 'func.function.handler',
+              MemorySize: 1024,
+              Role: { 'Fn::GetAtt': ['IamRoleLambdaExecution', 'Arn'] },
+              Runtime: 'nodejs4.3',
+              Timeout: 6,
+              KmsKeyArn: 'arn:aws:kms:region:accountid:foo/bar',
+            },
+          };
+
+          awsCompileFunctions.compileFunctions();
+
+          const compiledCfTemplate = awsCompileFunctions.serverless.service.provider
+            .compiledCloudFormationTemplate;
+
+          const functionResource = compiledCfTemplate.Resources.FuncLambdaFunction;
+
+          expect(functionResource).to.deep.equal(compiledFunction);
+        });
+      });
+    });
+
     it('should create a function resource with environment config', () => {
       const s3Folder = awsCompileFunctions.serverless.service.package.artifactDirectoryName;
       const s3FileName = awsCompileFunctions.serverless.service.package.artifact