Skip to content

Commit ebb7304

Browse files
VirtueMeclaude
andauthored
fix(iam): add kmsKeyArns config option to autogenerated IAM role (#756)
Allow users to specify KMS key ARNs at the state machine level so the plugin adds the required data-key permissions (kms:Decrypt, kms:Encrypt, kms:ReEncrypt*, kms:GenerateDataKey*, kms:DescribeKey) to the autogenerated IAM role. Closes #391 🤖 Generated with Claude Code Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 72fd1ad commit ebb7304

File tree

4 files changed

+97
-0
lines changed

4 files changed

+97
-0
lines changed

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,21 @@ stepFunctions:
559559
enabled: true
560560
```
561561

562+
### KMS Key Permissions
563+
564+
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.
565+
566+
```yaml
567+
stepFunctions:
568+
stateMachines:
569+
hellostepfunc1:
570+
kmsKeyArns:
571+
- arn:aws:kms:us-east-1:123456789012:key/your-key-id
572+
- !Ref MyKMSKey
573+
definition:
574+
...
575+
```
576+
562577
## Current Gotcha
563578

564579
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.

lib/deploy/stepFunctions/compileIamRole.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,13 @@ module.exports = {
135135
});
136136
}
137137

138+
if (stateMachineObj.kmsKeyArns && stateMachineObj.kmsKeyArns.length > 0) {
139+
iamPermissions.push({
140+
action: 'kms:Decrypt,kms:Encrypt,kms:ReEncrypt*,kms:GenerateDataKey*,kms:DescribeKey',
141+
resource: stateMachineObj.kmsKeyArns,
142+
});
143+
}
144+
138145
if (stateMachineObj.encryptionConfig && stateMachineObj.encryptionConfig.KmsKeyId) {
139146
iamPermissions.push({
140147
action: 'kms:Decrypt,kms:Encrypt',

lib/deploy/stepFunctions/compileIamRole.test.js

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1688,4 +1688,76 @@ describe('#compileIamRole', () => {
16881688
expect(statements[0].Action[1]).to.equal('kms:Encrypt');
16891689
expect(statements[0].Resource[0]['Fn::Sub']).to.equal('arn:kms:....');
16901690
});
1691+
1692+
it('should add KMS data-key permissions for each kmsKeyArn', () => {
1693+
serverless.service.stepFunctions = {
1694+
stateMachines: {
1695+
myStateMachine1: {
1696+
id: 'StateMachine1',
1697+
kmsKeyArns: [
1698+
'arn:aws:kms:us-east-1:123456789012:key/key-1',
1699+
{ Ref: 'MyKMSKey' },
1700+
],
1701+
definition: {
1702+
StartAt: 'A',
1703+
States: {
1704+
A: {
1705+
Type: 'Task',
1706+
Resource: 'arn:aws:states:::dynamodb:getItem',
1707+
End: true,
1708+
},
1709+
},
1710+
},
1711+
},
1712+
},
1713+
};
1714+
1715+
serverlessStepFunctions.compileIamRole();
1716+
const statements = serverlessStepFunctions.serverless.service.provider
1717+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
1718+
.PolicyDocument.Statement;
1719+
1720+
const kmsStatements = statements.filter((s) => s.Action.includes('kms:Decrypt'));
1721+
expect(kmsStatements).to.have.lengthOf(1);
1722+
expect(kmsStatements[0].Effect).to.equal('Allow');
1723+
expect(kmsStatements[0].Action).to.deep.equal([
1724+
'kms:Decrypt',
1725+
'kms:Encrypt',
1726+
'kms:ReEncrypt*',
1727+
'kms:GenerateDataKey*',
1728+
'kms:DescribeKey',
1729+
]);
1730+
expect(kmsStatements[0].Resource).to.deep.equal([
1731+
'arn:aws:kms:us-east-1:123456789012:key/key-1',
1732+
{ Ref: 'MyKMSKey' },
1733+
]);
1734+
});
1735+
1736+
it('should not add kms permissions when kmsKeyArns is absent', () => {
1737+
serverless.service.stepFunctions = {
1738+
stateMachines: {
1739+
myStateMachine1: {
1740+
id: 'StateMachine1',
1741+
definition: {
1742+
StartAt: 'A',
1743+
States: {
1744+
A: {
1745+
Type: 'Task',
1746+
Resource: 'arn:aws:states:::dynamodb:getItem',
1747+
End: true,
1748+
},
1749+
},
1750+
},
1751+
},
1752+
},
1753+
};
1754+
1755+
serverlessStepFunctions.compileIamRole();
1756+
const statements = serverlessStepFunctions.serverless.service.provider
1757+
.compiledCloudFormationTemplate.Resources.StateMachine1Role.Properties.Policies[0]
1758+
.PolicyDocument.Statement;
1759+
1760+
const kmsStatements = statements.filter((s) => s.Action.includes('kms:Decrypt'));
1761+
expect(kmsStatements).to.have.lengthOf(0);
1762+
});
16911763
});

lib/deploy/stepFunctions/compileStateMachines.schema.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ const encryptionConfig = Joi.object().keys({
5555
Type: Joi.string().default('AWS_OWNED_KEY'),
5656
});
5757

58+
const kmsKeyArns = Joi.array().items(arn);
59+
5860
const iamRoleStatements = Joi.array().items(
5961
Joi.object({
6062
Effect: Joi.string().valid('Allow', 'Deny'),
@@ -91,6 +93,7 @@ const schema = Joi.object().keys({
9193
encryptionConfig,
9294
inheritGlobalTags,
9395
iamRoleStatements,
96+
kmsKeyArns,
9497
}).oxor('role', 'iamRoleStatements');
9598

9699
module.exports = schema;

0 commit comments

Comments
 (0)