diff --git a/packages/@aws-cdk/aws-kms/README.md b/packages/@aws-cdk/aws-kms/README.md index 548960524c803..c03f4e40a7032 100644 --- a/packages/@aws-cdk/aws-kms/README.md +++ b/packages/@aws-cdk/aws-kms/README.md @@ -29,6 +29,8 @@ key.addAlias('alias/bar'); ### Sharing keys between stacks +> see Trust Account Identities for additional details + To use a KMS key in a different stack in the same CDK application, pass the construct to the other stack: @@ -37,6 +39,8 @@ pass the construct to the other stack: ### Importing existing keys +> see Trust Account Identities for additional details + To use a KMS key that is not defined in this CDK app, but is created through other means, use `Key.fromKeyArn(parent, name, ref)`: @@ -50,3 +54,72 @@ myKeyImported.addAlias('alias/foo'); Note that a call to `.addToPolicy(statement)` on `myKeyImported` will not have an affect on the key's policy because it is not owned by your stack. The call will be a no-op. + +### Trust Account Identities + +KMS keys can be created to trust IAM policies. This is the default behavior in +the console and is described +[here](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html). +This same behavior can be enabled by: + +```ts +new Key(stack, 'MyKey', { trustAccountIdentities: true }); +``` + +Using `trustAccountIdentities` solves many issues around cyclic dependencies +between stacks. The most common use case is creating an S3 Bucket with CMK +default encryption which is later accessed by IAM roles in other stacks. + +stack-1 (bucket and key created) + +```ts +// ... snip +const myKmsKey = new kms.Key(this, 'MyKey', { trustAccountIdentities: true }); + +const bucket = new Bucket(this, 'MyEncryptedBucket', { + bucketName: 'myEncryptedBucket', + encryption: BucketEncryption.KMS, + encryptionKey: myKmsKey +}); +``` + +stack-2 (lambda that operates on bucket and key) + +```ts +// ... snip + +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), +}); + +const bucket = s3.Bucket.fromBucketName(this, 'BucketId', 'myEncryptedBucket'); + +const key = kms.Key.fromKeyArn(this, 'KeyId', 'arn:aws:...'); // key ARN passed via stack props + +bucket.grantReadWrite(fn); +key.grantEncryptDecrypt(fn); +``` + +The challenge in this scenario is the KMS key policy behavior. The simple way to understand +this, is IAM policies for account entities can only grant the permissions granted to the +account root principle in the key policy. When `trustAccountIdentities` is true, +the following policy statement is added: + +```json +{ + "Sid": "Enable IAM User Permissions", + "Effect": "Allow", + "Principal": {"AWS": "arn:aws:iam::111122223333:root"}, + "Action": "kms:*", + "Resource": "*" +} +``` + +As the name suggests this trusts IAM policies to control access to the key. +If account root does not have permissions to the specific actions, then the key +policy and the IAM policy for the entity (e.g. Lambda) both need to grant +permission. + + diff --git a/packages/@aws-cdk/aws-kms/lib/key.ts b/packages/@aws-cdk/aws-kms/lib/key.ts index f8bdb1224f126..f9e12863cfcc9 100644 --- a/packages/@aws-cdk/aws-kms/lib/key.ts +++ b/packages/@aws-cdk/aws-kms/lib/key.ts @@ -73,6 +73,14 @@ abstract class KeyBase extends Resource implements IKey { */ protected abstract readonly policy?: iam.PolicyDocument; + /** + * Optional property to control trusting account identities. + * + * If specified grants will default identity policies instead of to both + * resource and identity policies. + */ + protected abstract readonly trustAccountIdentities: boolean; + /** * Collection of aliases added to the key * @@ -130,19 +138,25 @@ abstract class KeyBase extends Resource implements IKey { const crossAccountAccess = this.isGranteeFromAnotherAccount(grantee); const crossRegionAccess = this.isGranteeFromAnotherRegion(grantee); const crossEnvironment = crossAccountAccess || crossRegionAccess; - return iam.Grant.addToPrincipalAndResource({ + const grantOptions: iam.GrantWithResourceOptions = { grantee, actions, resource: this, - resourcePolicyPrincipal: principal, - - // if the key is used in a cross-environment matter, - // we can't access the Key ARN (they don't have physical names), - // so fall back to using '*'. ToDo we need to make this better... somehow - resourceArns: crossEnvironment ? ['*'] : [this.keyArn], - + resourceArns: [this.keyArn], resourceSelfArns: crossEnvironment ? undefined : ['*'], - }); + }; + if (this.trustAccountIdentities) { + return iam.Grant.addToPrincipalOrResource(grantOptions); + } else { + return iam.Grant.addToPrincipalAndResource({ + ...grantOptions, + // if the key is used in a cross-environment matter, + // we can't access the Key ARN (they don't have physical names), + // so fall back to using '*'. ToDo we need to make this better... somehow + resourceArns: crossEnvironment ? ['*'] : [this.keyArn], + resourcePolicyPrincipal: principal, + }); + } } /** @@ -268,6 +282,18 @@ export interface KeyProps { * @default RemovalPolicy.Retain */ readonly removalPolicy?: RemovalPolicy; + + /** + * Whether the key usage can be granted by IAM policies + * + * Setting this to true adds a default statement which delegates key + * access control completely to the identity's IAM policy (similar + * to how it works for other AWS resources). + * + * @default false + * @see https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default-allow-root-enable-iam + */ + readonly trustAccountIdentities?: boolean; } /** @@ -288,6 +314,10 @@ export class Key extends KeyBase { public readonly keyArn = keyArn; public readonly keyId: string; protected readonly policy?: iam.PolicyDocument | undefined = undefined; + // defaulting true: if we are importing the key the key policy is + // undefined and impossible to change here; this means updating identity + // policies is really the only option + protected readonly trustAccountIdentities: boolean = true; constructor(keyId: string) { super(scope, id); @@ -307,14 +337,16 @@ export class Key extends KeyBase { public readonly keyArn: string; public readonly keyId: string; protected readonly policy?: iam.PolicyDocument; + protected readonly trustAccountIdentities: boolean; constructor(scope: Construct, id: string, props: KeyProps = {}) { super(scope, id); - if (props.policy) { - this.policy = props.policy; + this.policy = props.policy || new iam.PolicyDocument(); + this.trustAccountIdentities = props.trustAccountIdentities || false; + if (this.trustAccountIdentities) { + this.allowAccountIdentitiesToControl(); } else { - this.policy = new iam.PolicyDocument(); this.allowAccountToAdmin(); } @@ -334,8 +366,17 @@ export class Key extends KeyBase { } } + private allowAccountIdentitiesToControl() { + this.addToResourcePolicy(new iam.PolicyStatement({ + resources: ['*'], + actions: ['kms:*'], + principals: [new iam.AccountRootPrincipal()] + })); + + } /** - * Let users from this account admin this key. + * Let users or IAM policies from this account admin this key. + * @link https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default * @link https://aws.amazon.com/premiumsupport/knowledge-center/update-key-policy-future/ */ private allowAccountToAdmin() { diff --git a/packages/@aws-cdk/aws-kms/test/test.key.ts b/packages/@aws-cdk/aws-kms/test/test.key.ts index 46c9e1367837c..a80a5b8ad839a 100644 --- a/packages/@aws-cdk/aws-kms/test/test.key.ts +++ b/packages/@aws-cdk/aws-kms/test/test.key.ts @@ -1,4 +1,5 @@ import { + countResources, exactlyMatchTemplate, expect, haveResource, @@ -12,6 +13,23 @@ import { Test } from 'nodeunit'; import { Key } from '../lib'; // tslint:disable:object-literal-key-quotes +const ACTIONS: string[] = [ + "kms:Create*", + "kms:Describe*", + "kms:Enable*", + "kms:List*", + "kms:Put*", + "kms:Update*", + "kms:Revoke*", + "kms:Disable*", + "kms:Get*", + "kms:Delete*", + "kms:ScheduleKeyDeletion", + "kms:CancelKeyDeletion", + "kms:GenerateDataKey", + "kms:TagResource", + "kms:UntagResource", +]; export = { 'default key'(test: Test) { @@ -27,51 +45,35 @@ export = { KeyPolicy: { Statement: [ { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], - Effect: "Allow", - Principal: { - AWS: { - "Fn::Join": [ - "", - [ - "arn:", - { - Ref: "AWS::Partition" - }, - ":iam::", - { - Ref: "AWS::AccountId" - }, - ":root" - ] - ] + Action: ACTIONS, + Effect: "Allow", + Principal: { + AWS: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":iam::", + { + Ref: "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + Resource: "*" + } + ], + Version: "2012-10-17" } - }, - Resource: "*" - } - ], - Version: "2012-10-17" + }, + DeletionPolicy: "Retain", + UpdateReplacePolicy: "Retain" } - }, - DeletionPolicy: "Retain", - UpdateReplacePolicy: "Retain" - } } })); test.done(); @@ -99,66 +101,50 @@ export = { expect(stack).to(exactlyMatchTemplate({ Resources: { MyKey6AB29FA6: { - Type: "AWS::KMS::Key", - Properties: { - KeyPolicy: { - Statement: [ - { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], - Effect: "Allow", - Principal: { - AWS: { - "Fn::Join": [ - "", - [ - "arn:", + Type: "AWS::KMS::Key", + Properties: { + KeyPolicy: { + Statement: [ { - Ref: "AWS::Partition" + Action: ACTIONS, + Effect: "Allow", + Principal: { + AWS: { + "Fn::Join": [ + "", + [ + "arn:", + { + Ref: "AWS::Partition" + }, + ":iam::", + { + Ref: "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + Resource: '*' }, - ":iam::", { - Ref: "AWS::AccountId" - }, - ":root" - ] - ] - } - }, - Resource: '*' - }, - { - Action: "kms:encrypt", - Effect: "Allow", - Principal: { - AWS: "arn" - }, - Resource: "*" + Action: "kms:encrypt", + Effect: "Allow", + Principal: { + AWS: "arn" + }, + Resource: "*" + } + ], + Version: "2012-10-17" } - ], - Version: "2012-10-17" - } - }, - DeletionPolicy: "Retain", - UpdateReplacePolicy: "Retain", + }, + DeletionPolicy: "Retain", + UpdateReplacePolicy: "Retain", } } - })); + })); test.done(); }, @@ -186,23 +172,7 @@ export = { KeyPolicy: { Statement: [ { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], + Action: ACTIONS, Effect: "Allow", Principal: { AWS: { @@ -273,76 +243,16 @@ export = { const alias = key.addAlias('alias/xoo'); test.ok(alias.aliasName); - expect(stack).toMatch({ - Resources: { - MyKey6AB29FA6: { - Type: "AWS::KMS::Key", - Properties: { - EnableKeyRotation: true, - Enabled: false, - KeyPolicy: { - Statement: [ - { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], - Effect: "Allow", - Principal: { - AWS: { - "Fn::Join": [ - "", - [ - "arn:", - { - Ref: "AWS::Partition" - }, - ":iam::", - { - Ref: "AWS::AccountId" - }, - ":root" - ] - ] - } - }, - Resource: "*" - } - ], - Version: "2012-10-17" - } - }, - DeletionPolicy: "Retain", - UpdateReplacePolicy: "Retain", - }, - MyKeyAlias1B45D9DA: { - Type: "AWS::KMS::Alias", - Properties: { - AliasName: "alias/xoo", - TargetKeyId: { - "Fn::GetAtt": [ - "MyKey6AB29FA6", - "Arn" - ] - } - } - } + expect(stack).to(countResources('AWS::KMS::Alias', 1)); + expect(stack).to(haveResource('AWS::KMS::Alias', { + AliasName: "alias/xoo", + TargetKeyId: { + "Fn::GetAtt": [ + "MyKey6AB29FA6", + "Arn" + ] } - }); - + })); test.done(); }, @@ -360,88 +270,25 @@ export = { test.ok(alias1.aliasName); test.ok(alias2.aliasName); - expect(stack).toMatch({ - Resources: { - MyKey6AB29FA6: { - Type: "AWS::KMS::Key", - Properties: { - EnableKeyRotation: true, - Enabled: false, - KeyPolicy: { - Statement: [ - { - Action: [ - "kms:Create*", - "kms:Describe*", - "kms:Enable*", - "kms:List*", - "kms:Put*", - "kms:Update*", - "kms:Revoke*", - "kms:Disable*", - "kms:Get*", - "kms:Delete*", - "kms:ScheduleKeyDeletion", - "kms:CancelKeyDeletion", - "kms:GenerateDataKey", - "kms:TagResource", - "kms:UntagResource" - ], - Effect: "Allow", - Principal: { - AWS: { - "Fn::Join": [ - "", - [ - "arn:", - { - Ref: "AWS::Partition" - }, - ":iam::", - { - Ref: "AWS::AccountId" - }, - ":root" - ] - ] - } - }, - Resource: "*" - } - ], - Version: "2012-10-17" - } - }, - DeletionPolicy: "Retain", - UpdateReplacePolicy: "Retain", - }, - MyKeyAlias1B45D9DA: { - Type: "AWS::KMS::Alias", - Properties: { - AliasName: "alias/alias1", - TargetKeyId: { - "Fn::GetAtt": [ - "MyKey6AB29FA6", - "Arn" - ] - } - } - }, - MyKeyAliasaliasalias2EC56BD3E: { - Type: "AWS::KMS::Alias", - Properties: { - AliasName: "alias/alias2", - TargetKeyId: { - "Fn::GetAtt": [ - "MyKey6AB29FA6", - "Arn" - ] - } - } - } + expect(stack).to(countResources('AWS::KMS::Alias', 2)); + expect(stack).to(haveResource('AWS::KMS::Alias', { + AliasName: 'alias/alias1', + TargetKeyId: { + "Fn::GetAtt": [ + "MyKey6AB29FA6", + "Arn" + ] } - }); - + })); + expect(stack).to(haveResource('AWS::KMS::Alias', { + AliasName: 'alias/alias2', + TargetKeyId: { + "Fn::GetAtt": [ + "MyKey6AB29FA6", + "Arn" + ] + } + })); test.done(); }, @@ -460,17 +307,16 @@ export = { Statement: [ // This one is there by default { - // tslint:disable-next-line:max-line-length - Action: [ "kms:Create*", "kms:Describe*", "kms:Enable*", "kms:List*", "kms:Put*", "kms:Update*", "kms:Revoke*", "kms:Disable*", "kms:Get*", "kms:Delete*", "kms:ScheduleKeyDeletion", "kms:CancelKeyDeletion", "kms:GenerateDataKey", "kms:TagResource", "kms:UntagResource" ], + Action: ACTIONS, Effect: "Allow", - Principal: { AWS: { "Fn::Join": [ "", [ "arn:", { Ref: "AWS::Partition" }, ":iam::", { Ref: "AWS::AccountId" }, ":root" ] ] } }, + Principal: { AWS: { "Fn::Join": ["", ["arn:", { Ref: "AWS::Partition" }, ":iam::", { Ref: "AWS::AccountId" }, ":root"]] } }, Resource: "*" }, // This is the interesting one { Action: "kms:Decrypt", Effect: "Allow", - Principal: { AWS: { "Fn::GetAtt": [ "User00B015A1", "Arn" ] } }, + Principal: { AWS: { "Fn::GetAtt": ["User00B015A1", "Arn"] } }, Resource: "*" } ], @@ -484,7 +330,7 @@ export = { { Action: "kms:Decrypt", Effect: "Allow", - Resource: { "Fn::GetAtt": [ "Key961B73FD", "Arn" ] } + Resource: { "Fn::GetAtt": ["Key961B73FD", "Arn"] } } ], Version: "2012-10-17" @@ -562,6 +408,33 @@ export = { test.done(); }, + 'enablePolicyControl changes key policy to allow IAM control'(test: Test) { + const stack = new Stack(); + new Key(stack, 'MyKey', { trustAccountIdentities: true }); + expect(stack).to(haveResourceLike('AWS::KMS::Key', { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": ["", [ + "arn:", + { "Ref": "AWS::Partition" }, + ":iam::", + { "Ref": "AWS::AccountId" }, + ":root", + ]], + }, + }, + "Resource": "*", + }, + ], + }, + })); + test.done(); + }, 'imported keys': { 'throw an error when providing something that is not a valid key ARN'(test: Test) {