diff --git a/README.md b/README.md index ac8cf63..3ad777e 100644 --- a/README.md +++ b/README.md @@ -559,6 +559,21 @@ stepFunctions: enabled: true ``` +### KMS Key Permissions + +If your state machine accesses KMS-encrypted resources (e.g. a DynamoDB table with a customer-managed KMS key), specify the key ARNs using `kmsKeyArns`. The plugin will add the required KMS permissions (`kms:Decrypt`, `kms:Encrypt`, `kms:ReEncrypt*`, `kms:GenerateDataKey*`, `kms:DescribeKey`) to the autogenerated IAM role. + +```yaml +stepFunctions: + stateMachines: + hellostepfunc1: + kmsKeyArns: + - arn:aws:kms:us-east-1:123456789012:key/your-key-id + - !Ref MyKMSKey + definition: + ... +``` + ## Current Gotcha Please keep this gotcha in mind if you want to reference the `name` from the `resources` section. To generate Logical ID for CloudFormation, the plugin transforms the specified name in serverless.yml based on the following scheme. diff --git a/lib/deploy/stepFunctions/compileIamRole.js b/lib/deploy/stepFunctions/compileIamRole.js index 0d4874a..f5248a0 100644 --- a/lib/deploy/stepFunctions/compileIamRole.js +++ b/lib/deploy/stepFunctions/compileIamRole.js @@ -135,6 +135,13 @@ module.exports = { }); } + if (stateMachineObj.kmsKeyArns && stateMachineObj.kmsKeyArns.length > 0) { + iamPermissions.push({ + action: 'kms:Decrypt,kms:Encrypt,kms:ReEncrypt*,kms:GenerateDataKey*,kms:DescribeKey', + resource: stateMachineObj.kmsKeyArns, + }); + } + if (stateMachineObj.encryptionConfig && stateMachineObj.encryptionConfig.KmsKeyId) { iamPermissions.push({ action: 'kms:Decrypt,kms:Encrypt', diff --git a/lib/deploy/stepFunctions/compileIamRole.test.js b/lib/deploy/stepFunctions/compileIamRole.test.js index 9ab2981..a3acde2 100644 --- a/lib/deploy/stepFunctions/compileIamRole.test.js +++ b/lib/deploy/stepFunctions/compileIamRole.test.js @@ -1688,4 +1688,76 @@ describe('#compileIamRole', () => { expect(statements[0].Action[1]).to.equal('kms:Encrypt'); expect(statements[0].Resource[0]['Fn::Sub']).to.equal('arn:kms:....'); }); + + it('should add KMS data-key permissions for each kmsKeyArn', () => { + serverless.service.stepFunctions = { + stateMachines: { + myStateMachine1: { + id: 'StateMachine1', + kmsKeyArns: [ + 'arn:aws:kms:us-east-1:123456789012:key/key-1', + { Ref: 'MyKMSKey' }, + ], + definition: { + StartAt: 'A', + States: { + A: { + Type: 'Task', + Resource: 'arn:aws:states:::dynamodb:getItem', + End: true, + }, + }, + }, + }, + }, + }; + + serverlessStepFunctions.compileIamRole(); + const statements = serverlessStepFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0] + .PolicyDocument.Statement; + + const kmsStatements = statements.filter((s) => s.Action.includes('kms:Decrypt')); + expect(kmsStatements).to.have.lengthOf(1); + expect(kmsStatements[0].Effect).to.equal('Allow'); + expect(kmsStatements[0].Action).to.deep.equal([ + 'kms:Decrypt', + 'kms:Encrypt', + 'kms:ReEncrypt*', + 'kms:GenerateDataKey*', + 'kms:DescribeKey', + ]); + expect(kmsStatements[0].Resource).to.deep.equal([ + 'arn:aws:kms:us-east-1:123456789012:key/key-1', + { Ref: 'MyKMSKey' }, + ]); + }); + + it('should not add kms permissions when kmsKeyArns is absent', () => { + serverless.service.stepFunctions = { + stateMachines: { + myStateMachine1: { + id: 'StateMachine1', + definition: { + StartAt: 'A', + States: { + A: { + Type: 'Task', + Resource: 'arn:aws:states:::dynamodb:getItem', + End: true, + }, + }, + }, + }, + }, + }; + + serverlessStepFunctions.compileIamRole(); + const statements = serverlessStepFunctions.serverless.service.provider + .compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0] + .PolicyDocument.Statement; + + const kmsStatements = statements.filter((s) => s.Action.includes('kms:Decrypt')); + expect(kmsStatements).to.have.lengthOf(0); + }); }); diff --git a/lib/deploy/stepFunctions/compileStateMachines.schema.js b/lib/deploy/stepFunctions/compileStateMachines.schema.js index e40c772..9314ead 100644 --- a/lib/deploy/stepFunctions/compileStateMachines.schema.js +++ b/lib/deploy/stepFunctions/compileStateMachines.schema.js @@ -55,6 +55,8 @@ const encryptionConfig = Joi.object().keys({ Type: Joi.string().default('AWS_OWNED_KEY'), }); +const kmsKeyArns = Joi.array().items(arn); + const iamRoleStatements = Joi.array().items( Joi.object({ Effect: Joi.string().valid('Allow', 'Deny'), @@ -91,6 +93,7 @@ const schema = Joi.object().keys({ encryptionConfig, inheritGlobalTags, iamRoleStatements, + kmsKeyArns, }).oxor('role', 'iamRoleStatements'); module.exports = schema;