Skip to content

Commit 2eb026a

Browse files
njlynchflochaz
authored andcommitted
feat(kms): change default key policy to align with KMS best practices (under feature flag) (aws#11918)
In aws#5575, a new flag (`trustAccountIdentities`) was introduced which -- when set -- changes the default key policy from a custom key admin policy to one that grants all access to the key to the root account user. This key policy matches the default policy when a key is created via the KMS APIs or console. For backwards-compatibility reasons, the default for `trustAccountIdentities` had to be set to `false`. Without the flag explicitly set, the default key policy is one that (a) doesn't match the KMS-recommended admin policy and (b) doesn't explicitly enable IAM principal policies to acccess the key. This means that all usage operations (e.g., Encrypt, GenerateDataKey) must be added to both the key policy and to the principal policy. This change introduces a new feature flag to flip the default behavior of the `trustAccountIdentities` flag, so new keys created will have the sane defaults matching the KMS recommended best practices. As a related change, this feature flag also changes the behavior when a user passes in `policy` when creating a Key. Without the feature flag set, the policy is always appended to the default key policy. With the feature flag set, the policy will *override* the default key policy, enabling users to opt-out of the default key policy to introduce a more restrictive policy if desired. This also matches the KMS API behavior, where a policy provided by the user will override the defaults. Marking this PR as `requires-two-approvers` to ensure this PR gets an appropriately-critical review. BREAKING CHANGE: change the default value of trustAccountIdentities to true, which will result in the key getting the KMS-recommended default key policy. This is enabled through the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag. fixes aws#8977 fixes aws#10575 fixes aws#11309 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 48c6278 commit 2eb026a

File tree

6 files changed

+728
-345
lines changed

6 files changed

+728
-345
lines changed

packages/@aws-cdk/aws-kms/README.md

Lines changed: 75 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,6 @@ key.addAlias('alias/bar');
3131

3232
## Sharing keys between stacks
3333

34-
> see Trust Account Identities for additional details
35-
3634
To use a KMS key in a different stack in the same CDK application,
3735
pass the construct to the other stack:
3836

@@ -41,8 +39,6 @@ pass the construct to the other stack:
4139

4240
## Importing existing keys
4341

44-
> see Trust Account Identities for additional details
45-
4642
To use a KMS key that is not defined in this CDK app, but is created through other means, use
4743
`Key.fromKeyArn(parent, name, ref)`:
4844

@@ -72,71 +68,97 @@ Note that calls to `addToResourcePolicy` and `grant*` methods on `myKeyAlias` wi
7268
no-ops, and `addAlias` and `aliasTargetKey` will fail, as the imported alias does not
7369
have a reference to the underlying KMS Key.
7470

75-
## Trust Account Identities
76-
77-
KMS keys can be created to trust IAM policies. This is the default behavior in
78-
the console and is described
79-
[here](https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html).
80-
This same behavior can be enabled by:
71+
## Key Policies
8172

82-
```ts
83-
new Key(stack, 'MyKey', { trustAccountIdentities: true });
84-
```
73+
Controlling access and usage of KMS Keys requires the use of key policies (resource-based policies attached to the key);
74+
this is in contrast to most other AWS resources where access can be entirely controlled with IAM policies,
75+
and optionally complemented with resource policies. For more in-depth understanding of KMS key access and policies, see
8576

86-
Using `trustAccountIdentities` solves many issues around cyclic dependencies
87-
between stacks. The most common use case is creating an S3 Bucket with CMK
88-
default encryption which is later accessed by IAM roles in other stacks.
77+
* https://docs.aws.amazon.com/kms/latest/developerguide/control-access-overview.html
78+
* https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html
8979

90-
stack-1 (bucket and key created)
80+
KMS keys can be created to trust IAM policies. This is the default behavior for both the KMS APIs and in
81+
the console. This behavior is enabled by the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag,
82+
which is set for all new projects; for existing projects, this same behavior can be enabled by
83+
passing the `trustAccountIdentities` property as `true` when creating the key:
9184

9285
```ts
93-
// ... snip
94-
const myKmsKey = new kms.Key(this, 'MyKey', { trustAccountIdentities: true });
95-
96-
const bucket = new Bucket(this, 'MyEncryptedBucket', {
97-
bucketName: 'myEncryptedBucket',
98-
encryption: BucketEncryption.KMS,
99-
encryptionKey: myKmsKey
100-
});
86+
new kms.Key(stack, 'MyKey', { trustAccountIdentities: true });
10187
```
10288

103-
stack-2 (lambda that operates on bucket and key)
104-
105-
```ts
106-
// ... snip
107-
108-
const fn = new lambda.Function(this, 'MyFunction', {
109-
runtime: lambda.Runtime.NODEJS_10_X,
110-
handler: 'index.handler',
111-
code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')),
112-
});
113-
114-
const bucket = s3.Bucket.fromBucketName(this, 'BucketId', 'myEncryptedBucket');
115-
116-
const key = kms.Key.fromKeyArn(this, 'KeyId', 'arn:aws:...'); // key ARN passed via stack props
117-
118-
bucket.grantReadWrite(fn);
119-
key.grantEncryptDecrypt(fn);
120-
```
121-
122-
The challenge in this scenario is the KMS key policy behavior. The simple way to understand
123-
this, is IAM policies for account entities can only grant the permissions granted to the
124-
account root principle in the key policy. When `trustAccountIdentities` is true,
125-
the following policy statement is added:
89+
With either the `@aws-cdk/aws-kms:defaultKeyPolicies` feature flag set,
90+
or the `trustAccountIdentities` prop set, the Key will be given the following default key policy:
12691

12792
```json
12893
{
129-
"Sid": "Enable IAM User Permissions",
13094
"Effect": "Allow",
13195
"Principal": {"AWS": "arn:aws:iam::111122223333:root"},
13296
"Action": "kms:*",
13397
"Resource": "*"
13498
}
13599
```
136100

137-
As the name suggests this trusts IAM policies to control access to the key.
138-
If account root does not have permissions to the specific actions, then the key
139-
policy and the IAM policy for the entity (e.g. Lambda) both need to grant
140-
permission.
101+
This policy grants full access to the key to the root account user.
102+
This enables the root account user -- via IAM policies -- to grant access to other IAM principals.
103+
With the above default policy, future permissions can be added to either the key policy or IAM principal policy.
141104

105+
```ts
106+
const key = new kms.Key(stack, 'MyKey');
107+
const user = new iam.User(stack, 'MyUser');
108+
key.grantEncrypt(user); // Adds encrypt permissions to user policy; key policy is unmodified.
109+
```
110+
111+
Adopting the default KMS key policy (and so trusting account identities)
112+
solves many issues around cyclic dependencies between stacks.
113+
Without this default key policy, future permissions must be added to both the key policy and IAM principal policy,
114+
which can cause cyclic dependencies if the permissions cross stack boundaries.
115+
(For example, an encrypted bucket in one stack, and Lambda function that accesses it in another.)
116+
117+
### Appending to or replacing the default key policy
118+
119+
The default key policy can be amended or replaced entirely, depending on your use case and requirements.
120+
A common addition to the key policy would be to add other key admins that are allowed to administer the key
121+
(e.g., change permissions, revoke, delete). Additional key admins can be specified at key creation or after
122+
via the `grantAdmin` method.
123+
124+
```ts
125+
const myTrustedAdminRole = iam.Role.fromRoleArn(stack, 'TrustedRole', 'arn:aws:iam:....');
126+
const key = new kms.Key(stack, 'MyKey', {
127+
admins: [myTrustedAdminRole],
128+
});
129+
130+
const secondKey = new kms.Key(stack, 'MyKey2');
131+
secondKey.grantAdmin(myTrustedAdminRole);
132+
```
133+
134+
Alternatively, a custom key policy can be specified, which will replace the default key policy.
135+
136+
> **Note**: In applications without the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag set
137+
and with `trustedAccountIdentities` set to false (the default), specifying a policy at key creation _appends_ the
138+
provided policy to the default key policy, rather than _replacing_ the default policy.
139+
140+
```ts
141+
const myTrustedAdminRole = iam.Role.fromRoleArn(stack, 'TrustedRole', 'arn:aws:iam:....');
142+
// Creates a limited admin policy and assigns to the account root.
143+
const myCustomPolicy = new iam.PolicyDocument({
144+
statements: [new iam.PolicyStatement({
145+
actions: [
146+
'kms:Create*',
147+
'kms:Describe*',
148+
'kms:Enable*',
149+
'kms:List*',
150+
'kms:Put*',
151+
],
152+
principals: [new iam.AccountRootPrincipal()],
153+
resources: ['*'],
154+
})],
155+
});
156+
const key = new kms.Key(stack, 'MyKey', {
157+
policy: myCustomPolicy,
158+
});
159+
```
142160

161+
> **Warning:** Replacing the default key policy with one that only grants access to a specific user or role
162+
runs the risk of the key becoming unmanageable if that user or role is deleted.
163+
It is highly recommended that the key policy grants access to the account root, rather than specific principals.
164+
See https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html for more information.

packages/@aws-cdk/aws-kms/lib/key.ts

Lines changed: 83 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import * as iam from '@aws-cdk/aws-iam';
2-
import { IResource, RemovalPolicy, Resource, Stack } from '@aws-cdk/core';
2+
import { FeatureFlags, IResource, RemovalPolicy, Resource, Stack } from '@aws-cdk/core';
3+
import * as cxapi from '@aws-cdk/cx-api';
34
import { IConstruct, Construct } from 'constructs';
45
import { Alias } from './alias';
56
import { CfnKey } from './kms.generated';
7+
import * as perms from './private/perms';
68

79
/**
810
* A KMS Key, either managed by this CDK app, or imported.
@@ -77,8 +79,9 @@ abstract class KeyBase extends Resource implements IKey {
7779
/**
7880
* Optional property to control trusting account identities.
7981
*
80-
* If specified grants will default identity policies instead of to both
81-
* resource and identity policies.
82+
* If specified, grants will default identity policies instead of to both
83+
* resource and identity policies. This matches the default behavior when creating
84+
* KMS keys via the API or console.
8285
*/
8386
protected abstract readonly trustAccountIdentities: boolean;
8487

@@ -168,35 +171,24 @@ abstract class KeyBase extends Resource implements IKey {
168171
}
169172

170173
/**
171-
* Grant decryption permisisons using this key to the given principal
174+
* Grant decryption permissions using this key to the given principal
172175
*/
173176
public grantDecrypt(grantee: iam.IGrantable): iam.Grant {
174-
return this.grant(grantee,
175-
'kms:Decrypt',
176-
);
177+
return this.grant(grantee, ...perms.DECRYPT_ACTIONS);
177178
}
178179

179180
/**
180-
* Grant encryption permisisons using this key to the given principal
181+
* Grant encryption permissions using this key to the given principal
181182
*/
182183
public grantEncrypt(grantee: iam.IGrantable): iam.Grant {
183-
return this.grant(grantee,
184-
'kms:Encrypt',
185-
'kms:ReEncrypt*',
186-
'kms:GenerateDataKey*',
187-
);
184+
return this.grant(grantee, ...perms.ENCRYPT_ACTIONS);
188185
}
189186

190187
/**
191-
* Grant encryption and decryption permisisons using this key to the given principal
188+
* Grant encryption and decryption permissions using this key to the given principal
192189
*/
193190
public grantEncryptDecrypt(grantee: iam.IGrantable): iam.Grant {
194-
return this.grant(grantee,
195-
'kms:Decrypt',
196-
'kms:Encrypt',
197-
'kms:ReEncrypt*',
198-
'kms:GenerateDataKey*',
199-
);
191+
return this.grant(grantee, ...[...perms.DECRYPT_ACTIONS, ...perms.ENCRYPT_ACTIONS]);
200192
}
201193

202194
/**
@@ -293,11 +285,27 @@ export interface KeyProps {
293285
/**
294286
* Custom policy document to attach to the KMS key.
295287
*
288+
* NOTE - If the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag is set (the default for new projects),
289+
* this policy will *override* the default key policy and become the only key policy for the key. If the
290+
* feature flag is not set, this policy will be appended to the default key policy.
291+
*
296292
* @default - A policy document with permissions for the account root to
297293
* administer the key will be created.
298294
*/
299295
readonly policy?: iam.PolicyDocument;
300296

297+
/**
298+
* A list of principals to add as key administrators to the key policy.
299+
*
300+
* Key administrators have permissions to manage the key (e.g., change permissions, revoke), but do not have permissions
301+
* to use the key in cryptographic operations (e.g., encrypt, decrypt).
302+
*
303+
* These principals will be added to the default key policy (if none specified), or to the specified policy (if provided).
304+
*
305+
* @default []
306+
*/
307+
readonly admins?: iam.IPrincipal[];
308+
301309
/**
302310
* Whether the encryption key should be retained when it is removed from the Stack. This is useful when one wants to
303311
* retain access to data that was encrypted with a key that is being retired.
@@ -311,10 +319,15 @@ export interface KeyProps {
311319
*
312320
* Setting this to true adds a default statement which delegates key
313321
* access control completely to the identity's IAM policy (similar
314-
* to how it works for other AWS resources).
322+
* to how it works for other AWS resources). This matches the default behavior
323+
* when creating KMS keys via the API or console.
315324
*
316-
* @default false
325+
* If the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag is set (the default for new projects),
326+
* this flag will always be treated as 'true' and does not need to be explicitly set.
327+
*
328+
* @default - false, unless the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag is set.
317329
* @see https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default-allow-root-enable-iam
330+
* @deprecated redundant with the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag
318331
*/
319332
readonly trustAccountIdentities?: boolean;
320333
}
@@ -365,12 +378,26 @@ export class Key extends KeyBase {
365378
constructor(scope: Construct, id: string, props: KeyProps = {}) {
366379
super(scope, id);
367380

368-
this.policy = props.policy || new iam.PolicyDocument();
369-
this.trustAccountIdentities = props.trustAccountIdentities || false;
370-
if (this.trustAccountIdentities) {
371-
this.allowAccountIdentitiesToControl();
381+
const defaultKeyPoliciesFeatureEnabled = FeatureFlags.of(this).isEnabled(cxapi.KMS_DEFAULT_KEY_POLICIES);
382+
383+
this.policy = props.policy ?? new iam.PolicyDocument();
384+
if (defaultKeyPoliciesFeatureEnabled) {
385+
if (props.trustAccountIdentities === false) {
386+
throw new Error('`trustAccountIdentities` cannot be false if the @aws-cdk/aws-kms:defaultKeyPolicies feature flag is set');
387+
}
388+
389+
this.trustAccountIdentities = true;
390+
// Set the default key policy if one hasn't been provided by the user.
391+
if (!props.policy) {
392+
this.addDefaultAdminPolicy();
393+
}
372394
} else {
373-
this.allowAccountToAdmin();
395+
this.trustAccountIdentities = props.trustAccountIdentities ?? false;
396+
if (this.trustAccountIdentities) {
397+
this.addDefaultAdminPolicy();
398+
} else {
399+
this.addLegacyAdminPolicy();
400+
}
374401
}
375402

376403
const resource = new CfnKey(this, 'Resource', {
@@ -384,25 +411,49 @@ export class Key extends KeyBase {
384411
this.keyId = resource.ref;
385412
resource.applyRemovalPolicy(props.removalPolicy);
386413

414+
(props.admins ?? []).forEach((p) => this.grantAdmin(p));
415+
387416
if (props.alias !== undefined) {
388417
this.addAlias(props.alias);
389418
}
390419
}
391420

392-
private allowAccountIdentitiesToControl() {
421+
/**
422+
* Grant admins permissions using this key to the given principal
423+
*
424+
* Key administrators have permissions to manage the key (e.g., change permissions, revoke), but do not have permissions
425+
* to use the key in cryptographic operations (e.g., encrypt, decrypt).
426+
*/
427+
public grantAdmin(grantee: iam.IGrantable): iam.Grant {
428+
return this.grant(grantee, ...perms.ADMIN_ACTIONS);
429+
}
430+
431+
/**
432+
* Adds the default key policy to the key. This policy gives the AWS account (root user) full access to the CMK,
433+
* which reduces the risk of the CMK becoming unmanageable and enables IAM policies to allow access to the CMK.
434+
* This is the same policy that is default when creating a Key via the KMS API or Console.
435+
* @see https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default
436+
*/
437+
private addDefaultAdminPolicy() {
393438
this.addToResourcePolicy(new iam.PolicyStatement({
394439
resources: ['*'],
395440
actions: ['kms:*'],
396441
principals: [new iam.AccountRootPrincipal()],
397442
}));
398-
399443
}
444+
400445
/**
401-
* Let users or IAM policies from this account admin this key.
446+
* Grants the account admin privileges -- not full account access -- plus the GenerateDataKey action.
447+
* The GenerateDataKey action was added for interop with S3 in https://github.com/aws/aws-cdk/issues/3458.
448+
*
449+
* This policy is discouraged and deprecated by the '@aws-cdk/aws-kms:defaultKeyPolicies' feature flag.
450+
*
402451
* @link https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html#key-policy-default
403-
* @link https://aws.amazon.com/premiumsupport/knowledge-center/update-key-policy-future/
452+
* @deprecated
404453
*/
405-
private allowAccountToAdmin() {
454+
private addLegacyAdminPolicy() {
455+
// This is equivalent to `[...perms.ADMIN_ACTIONS, 'kms:GenerateDataKey']`,
456+
// but keeping this explicit ordering for backwards-compatibility (changing the ordering causes resource updates)
406457
const actions = [
407458
'kms:Create*',
408459
'kms:Describe*',
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html
2+
3+
export const ADMIN_ACTIONS = [
4+
'kms:Create*',
5+
'kms:Describe*',
6+
'kms:Enable*',
7+
'kms:List*',
8+
'kms:Put*',
9+
'kms:Update*',
10+
'kms:Revoke*',
11+
'kms:Disable*',
12+
'kms:Get*',
13+
'kms:Delete*',
14+
'kms:TagResource',
15+
'kms:UntagResource',
16+
'kms:ScheduleKeyDeletion',
17+
'kms:CancelKeyDeletion',
18+
];
19+
20+
export const ENCRYPT_ACTIONS = [
21+
'kms:Encrypt',
22+
'kms:ReEncrypt*',
23+
'kms:GenerateDataKey*',
24+
];
25+
26+
export const DECRYPT_ACTIONS = [
27+
'kms:Decrypt',
28+
];

0 commit comments

Comments
 (0)