From f12fe197983f877a308e61f10e47d667eecf8152 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino Date: Fri, 13 Aug 2021 22:54:48 +0000 Subject: [PATCH 1/5] feat(aws-autoscaling): add aspect to enable/disable imdsv1 --- packages/@aws-cdk/aws-autoscaling/README.md | 17 +++ .../lib/aspects/imds-aspect.ts | 83 +++++++++++++ .../aws-autoscaling/lib/aspects/index.ts | 1 + .../@aws-cdk/aws-autoscaling/lib/index.ts | 1 + .../test/aspects/imds-aspect.test.ts | 110 ++++++++++++++++++ 5 files changed, 212 insertions(+) create mode 100644 packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts create mode 100644 packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts create mode 100644 packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index 67e55eee91a9f..d1a75f16e493b 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -378,6 +378,23 @@ new autoscaling.AutoScalingGroup(stack, 'ASG', { }); ``` +## Configuring Instance Metadata Service (IMDS) + +### Toggling IMDSv1 + +You can configure [EC2 Instance Metadata Service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) options to either +allow both IMDSv1 and IMDSv2 or enforce IMDSv2 when interacting with the IMDS. To do this, you can use the `AutoScalingGroupImdsAspect`. + +The following example demonstrates how to use the `AutoScalingGroupImdsAspect` to disable IMDSv1 (thus enforcing IMDSv2) for all AutoScalingGroups in a stack: + +```ts +const aspect = new autoscaling.AutoScalingGroupImdsAspect({ + enableImdsV1: false, +}); + +Aspects.of(stack).add(aspect); +``` + ## Future work * [ ] CloudWatch Events (impossible to add currently as the AutoScalingGroup ARN is diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts new file mode 100644 index 0000000000000..1a50ddbfac6b0 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts @@ -0,0 +1,83 @@ +import * as cdk from '@aws-cdk/core'; +import { AutoScalingGroup } from '../auto-scaling-group'; +import { CfnLaunchConfiguration } from '../autoscaling.generated'; + +/** + * Properties for `ImdsAspect`. + */ +interface ImdsAspectProps { + /** + * Whether IMDSv1 should be enabled or not. + */ + readonly enableImdsV1: boolean; + + /** + * Whether warning annotations from this Aspect should be suppressed or not. + * @default false + */ + readonly suppressWarnings?: boolean; +} + +/** + * Base class for IMDS configuration Aspect. + */ +abstract class ImdsAspect implements cdk.IAspect { + protected readonly enableImdsV1: boolean; + protected readonly suppressWarnings: boolean; + + constructor(props: ImdsAspectProps) { + this.enableImdsV1 = props.enableImdsV1; + this.suppressWarnings = props.suppressWarnings ?? false; + } + + abstract visit(node: cdk.IConstruct): void; + + /** + * Adds a warning annotation to a node, unless `suppressWarnings` is true. + * @param node The scope to add the warning to. + * @param message The warning message. + */ + protected warn(node: cdk.IConstruct, message: string) { + if (this.suppressWarnings !== true) { + cdk.Annotations.of(node).addWarning(`${ImdsAspect.name} failed on node ${node.node.id}: ${message}`); + } + } +} + +/** + * Properties for `AutoScalingGroupImdsAspect`. + */ +export interface AutoScalingGroupImdsAspectProps extends ImdsAspectProps {} + +/** + * Aspect that applies IMDS configuration to AutoScalingGroups. + */ +export class AutoScalingGroupImdsAspect extends ImdsAspect { + constructor(props: AutoScalingGroupImdsAspectProps) { + super(props); + } + + visit(node: cdk.IConstruct): void { + /* istanbul ignore next */ + if (node === undefined || !(node instanceof AutoScalingGroup)) { + return; + } + + const launchConfig = node.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; + if (launchConfig.metadataOptions !== undefined && implementsIResolvable(launchConfig.metadataOptions)) { + this.warn(node, 'CfnLaunchConfiguration.MetadataOptions field is a CDK token.'); + return; + } + + launchConfig.metadataOptions = { + ...launchConfig.metadataOptions, + httpTokens: this.enableImdsV1 ? 'optional' : 'required', + }; + } +} + +function implementsIResolvable(obj: any): boolean { + return 'resolve' in obj && typeof(obj.resolve) === 'function' && + 'creationStack' in obj && Array.isArray(obj.creationStack) && + 'toString' in obj && typeof(obj.toString) === 'function'; +} diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts new file mode 100644 index 0000000000000..25a82e046399c --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts @@ -0,0 +1 @@ +export * from './imds-aspect'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/lib/index.ts b/packages/@aws-cdk/aws-autoscaling/lib/index.ts index 69fede92e300b..186d1a3058fae 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/index.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/index.ts @@ -1,3 +1,4 @@ +export * from './aspects'; export * from './auto-scaling-group'; export * from './schedule'; export * from './lifecycle-hook'; diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts new file mode 100644 index 0000000000000..875fe372a8d75 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts @@ -0,0 +1,110 @@ +import { + expect as expectCDK, + haveResourceLike, +} from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as sinon from 'sinon'; +import { + AutoScalingGroup, + AutoScalingGroupImdsAspect, + CfnLaunchConfiguration, +} from '../../lib'; + +describe('ImdsAspect', () => { + let app: cdk.App; + let stack: cdk.Stack; + let vpc: ec2.Vpc; + + beforeEach(() => { + app = new cdk.App(); + stack = new cdk.Stack(app, 'Stack'); + vpc = new ec2.Vpc(stack, 'Vpc'); + }); + + test('suppresses warnings', () => { + // GIVEN + const aspect = new AutoScalingGroupImdsAspect({ + enableImdsV1: true, + suppressWarnings: true, + }); + const errmsg = 'ERROR'; + const stub = sinon.stub(aspect, 'visit').callsFake((node) => { + // @ts-ignore + aspect.warn(node, errmsg); + }); + const construct = new cdk.Construct(stack, 'Construct'); + + // WHEN + aspect.visit(construct); + + // THEN + expect(stub.calledOnce).toBeTruthy(); + expect(construct.node.metadataEntry).not.toContainEqual({ + data: expect.stringContaining(errmsg), + type: 'aws:cdk:warning', + trace: undefined, + }); + }); + + describe('AutoScalingGroupImdsAspect', () => { + test('warns when metadataOptions is a token', () => { + // GIVEN + const asg = new AutoScalingGroup(stack, 'AutoScalingGroup', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + }); + const launchConfig = asg.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; + launchConfig.metadataOptions = fakeToken(); + const aspect = new AutoScalingGroupImdsAspect({ enableImdsV1: false }); + + // WHEN + aspect.visit(asg); + + // THEN + expect(asg.node.metadataEntry).toContainEqual({ + data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), + type: 'aws:cdk:warning', + trace: undefined, + }); + expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + })); + }); + + test.each([ + [true], + [false], + ])('toggles IMDSv1 (enabled=%s)', (enableImdsV1: boolean) => { + // GIVEN + const asg = new AutoScalingGroup(stack, 'AutoScalingGroup', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + }); + const aspect = new AutoScalingGroupImdsAspect({ enableImdsV1 }); + + // WHEN + aspect.visit(asg); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: enableImdsV1 ? 'optional' : 'required', + }, + })); + }); + }); +}); + +function fakeToken(): cdk.IResolvable { + return { + creationStack: [], + resolve: (_c) => {}, + toString: () => '', + }; +} From fd328445edb61f63073635aadaaec84303d83e69 Mon Sep 17 00:00:00 2001 From: jericht Date: Tue, 28 Sep 2021 00:41:38 +0000 Subject: [PATCH 2/5] use aspects api in unit test and use isResolvableObject in core --- .../aws-autoscaling/lib/aspects/imds-aspect.ts | 15 +++++---------- .../test/aspects/imds-aspect.test.ts | 16 ++++++++-------- 2 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts index 1a50ddbfac6b0..184343a89f8ab 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts @@ -13,7 +13,8 @@ interface ImdsAspectProps { /** * Whether warning annotations from this Aspect should be suppressed or not. - * @default false + * + * @default - false */ readonly suppressWarnings?: boolean; } @@ -34,6 +35,7 @@ abstract class ImdsAspect implements cdk.IAspect { /** * Adds a warning annotation to a node, unless `suppressWarnings` is true. + * * @param node The scope to add the warning to. * @param message The warning message. */ @@ -58,13 +60,12 @@ export class AutoScalingGroupImdsAspect extends ImdsAspect { } visit(node: cdk.IConstruct): void { - /* istanbul ignore next */ - if (node === undefined || !(node instanceof AutoScalingGroup)) { + if (!(node instanceof AutoScalingGroup)) { return; } const launchConfig = node.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; - if (launchConfig.metadataOptions !== undefined && implementsIResolvable(launchConfig.metadataOptions)) { + if (cdk.isResolvableObject(launchConfig.metadataOptions)) { this.warn(node, 'CfnLaunchConfiguration.MetadataOptions field is a CDK token.'); return; } @@ -75,9 +76,3 @@ export class AutoScalingGroupImdsAspect extends ImdsAspect { }; } } - -function implementsIResolvable(obj: any): boolean { - return 'resolve' in obj && typeof(obj.resolve) === 'function' && - 'creationStack' in obj && Array.isArray(obj.creationStack) && - 'toString' in obj && typeof(obj.toString) === 'function'; -} diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts index 875fe372a8d75..879250a6374af 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts @@ -61,19 +61,19 @@ describe('ImdsAspect', () => { const aspect = new AutoScalingGroupImdsAspect({ enableImdsV1: false }); // WHEN - aspect.visit(asg); + cdk.Aspects.of(stack).add(aspect); // THEN - expect(asg.node.metadataEntry).toContainEqual({ - data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), - type: 'aws:cdk:warning', - trace: undefined, - }); expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { MetadataOptions: { HttpTokens: 'required', }, })); + expect(asg.node.metadataEntry).toContainEqual({ + data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), + type: 'aws:cdk:warning', + trace: undefined, + }); }); test.each([ @@ -81,7 +81,7 @@ describe('ImdsAspect', () => { [false], ])('toggles IMDSv1 (enabled=%s)', (enableImdsV1: boolean) => { // GIVEN - const asg = new AutoScalingGroup(stack, 'AutoScalingGroup', { + new AutoScalingGroup(stack, 'AutoScalingGroup', { vpc, instanceType: new ec2.InstanceType('t2.micro'), machineImage: ec2.MachineImage.latestAmazonLinux(), @@ -89,7 +89,7 @@ describe('ImdsAspect', () => { const aspect = new AutoScalingGroupImdsAspect({ enableImdsV1 }); // WHEN - aspect.visit(asg); + cdk.Aspects.of(stack).add(aspect); // THEN expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { From ca7cc7b895e5258e092805f42025e2ec71f16670 Mon Sep 17 00:00:00 2001 From: jericht Date: Tue, 28 Sep 2021 00:59:05 +0000 Subject: [PATCH 3/5] add option to disable imdsv1 --- .../aws-autoscaling/lib/auto-scaling-group.ts | 13 ++++++++++++ .../test/auto-scaling-group.test.ts | 21 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 027034249c4dd..1a99446e5cc0d 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -7,6 +7,7 @@ import * as sns from '@aws-cdk/aws-sns'; import { Annotations, + Aspects, Aws, CfnAutoScalingRollingUpdate, CfnCreationPolicy, CfnUpdatePolicy, Duration, Fn, IResource, Lazy, PhysicalName, Resource, Stack, Tags, @@ -14,6 +15,7 @@ import { Tokenization, withResolved, } from '@aws-cdk/core'; import { Construct } from 'constructs'; +import { AutoScalingGroupImdsAspect } from './aspects'; import { CfnAutoScalingGroup, CfnAutoScalingGroupProps, CfnLaunchConfiguration } from './autoscaling.generated'; import { BasicLifecycleHookProps, LifecycleHook } from './lifecycle-hook'; import { BasicScheduledActionProps, ScheduledAction } from './scheduled-action'; @@ -384,6 +386,13 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { * @default - default options */ readonly initOptions?: ApplyCloudFormationInitOptions; + + /** + * Whether IMDSv1 should be disabled on launched instances. + * + * @default - false + */ + readonly disableImdsv1?: boolean; } /** @@ -1065,6 +1074,10 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements } this.spotPrice = props.spotPrice; + + if (props.disableImdsv1 === true) { + Aspects.of(this).add(new AutoScalingGroupImdsAspect({ enableImdsV1: false })); + } } /** diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 64795593e8ec4..02d81aeb5c6fa 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1364,6 +1364,27 @@ describe('auto scaling group', () => { }); + + test('disables imdsv1', () => { + // GIVEN + const stack = new cdk.Stack(); + const vpc = mockVpc(stack); + + // WHEN + new autoscaling.AutoScalingGroup(stack, 'MyASG', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + disableImdsv1: true, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + }); + }); }); function mockVpc(stack: cdk.Stack) { From b2ed9d724a48ed069eb477e368369603dac85336 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino Date: Tue, 5 Oct 2021 00:35:21 +0000 Subject: [PATCH 4/5] remove suppressWarnings option --- packages/@aws-cdk/aws-autoscaling/README.md | 15 +++++++++-- .../lib/aspects/imds-aspect.ts | 15 ++--------- .../test/aspects/imds-aspect.test.ts | 26 ------------------- 3 files changed, 15 insertions(+), 41 deletions(-) diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index d1a75f16e493b..9d4924cc1b547 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -383,9 +383,20 @@ new autoscaling.AutoScalingGroup(stack, 'ASG', { ### Toggling IMDSv1 You can configure [EC2 Instance Metadata Service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) options to either -allow both IMDSv1 and IMDSv2 or enforce IMDSv2 when interacting with the IMDS. To do this, you can use the `AutoScalingGroupImdsAspect`. +allow both IMDSv1 and IMDSv2 or enforce IMDSv2 when interacting with the IMDS. -The following example demonstrates how to use the `AutoScalingGroupImdsAspect` to disable IMDSv1 (thus enforcing IMDSv2) for all AutoScalingGroups in a stack: +To do this for a single `AutoScalingGroup`, you can use set the `disableImdsv1` property. +The example below demonstrates IMDSv1 being disabled on a single `AutoScalingGroup`: + +```ts +new autoscaling.AutoScalingGroup(stack, 'ASG', { + disableImdsv1: true, + // ... +}); +``` + +You can also use `AutoScalingGroupImdsAspect` to apply the operation to multiple AutoScalingGroups. +The example below demonstrates the `AutoScalingGroupImdsAspect` being used to disable IMDSv1 for all AutoScalingGroups in a stack: ```ts const aspect = new autoscaling.AutoScalingGroupImdsAspect({ diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts index 184343a89f8ab..17e9ff1551c4f 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts @@ -10,13 +10,6 @@ interface ImdsAspectProps { * Whether IMDSv1 should be enabled or not. */ readonly enableImdsV1: boolean; - - /** - * Whether warning annotations from this Aspect should be suppressed or not. - * - * @default - false - */ - readonly suppressWarnings?: boolean; } /** @@ -24,25 +17,21 @@ interface ImdsAspectProps { */ abstract class ImdsAspect implements cdk.IAspect { protected readonly enableImdsV1: boolean; - protected readonly suppressWarnings: boolean; constructor(props: ImdsAspectProps) { this.enableImdsV1 = props.enableImdsV1; - this.suppressWarnings = props.suppressWarnings ?? false; } abstract visit(node: cdk.IConstruct): void; /** - * Adds a warning annotation to a node, unless `suppressWarnings` is true. + * Adds a warning annotation to a node. * * @param node The scope to add the warning to. * @param message The warning message. */ protected warn(node: cdk.IConstruct, message: string) { - if (this.suppressWarnings !== true) { - cdk.Annotations.of(node).addWarning(`${ImdsAspect.name} failed on node ${node.node.id}: ${message}`); - } + cdk.Annotations.of(node).addWarning(`${ImdsAspect.name} failed on node ${node.node.id}: ${message}`); } } diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts index 879250a6374af..3e3a2d929f9f7 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts @@ -5,7 +5,6 @@ import { import '@aws-cdk/assert-internal/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import * as sinon from 'sinon'; import { AutoScalingGroup, AutoScalingGroupImdsAspect, @@ -23,31 +22,6 @@ describe('ImdsAspect', () => { vpc = new ec2.Vpc(stack, 'Vpc'); }); - test('suppresses warnings', () => { - // GIVEN - const aspect = new AutoScalingGroupImdsAspect({ - enableImdsV1: true, - suppressWarnings: true, - }); - const errmsg = 'ERROR'; - const stub = sinon.stub(aspect, 'visit').callsFake((node) => { - // @ts-ignore - aspect.warn(node, errmsg); - }); - const construct = new cdk.Construct(stack, 'Construct'); - - // WHEN - aspect.visit(construct); - - // THEN - expect(stub.calledOnce).toBeTruthy(); - expect(construct.node.metadataEntry).not.toContainEqual({ - data: expect.stringContaining(errmsg), - type: 'aws:cdk:warning', - trace: undefined, - }); - }); - describe('AutoScalingGroupImdsAspect', () => { test('warns when metadataOptions is a token', () => { // GIVEN From 336da33ec4c642badca67325a77aa76e220f5eb2 Mon Sep 17 00:00:00 2001 From: Jericho Tolentino Date: Wed, 13 Oct 2021 00:33:46 +0000 Subject: [PATCH 5/5] remove base class and rename variable to requireImdsv2 --- packages/@aws-cdk/aws-autoscaling/README.md | 14 ++-- .../lib/aspects/imds-aspect.ts | 67 --------------- .../aws-autoscaling/lib/aspects/index.ts | 2 +- .../lib/aspects/require-imdsv2-aspect.ts | 38 +++++++++ .../aws-autoscaling/lib/auto-scaling-group.ts | 10 +-- .../test/aspects/imds-aspect.test.ts | 84 ------------------- .../aspects/require-imdsv2-aspect.test.ts | 79 +++++++++++++++++ .../test/auto-scaling-group.test.ts | 4 +- 8 files changed, 131 insertions(+), 167 deletions(-) delete mode 100644 packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts create mode 100644 packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts delete mode 100644 packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts create mode 100644 packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts diff --git a/packages/@aws-cdk/aws-autoscaling/README.md b/packages/@aws-cdk/aws-autoscaling/README.md index 9d4924cc1b547..75aa4f66807e2 100644 --- a/packages/@aws-cdk/aws-autoscaling/README.md +++ b/packages/@aws-cdk/aws-autoscaling/README.md @@ -385,23 +385,21 @@ new autoscaling.AutoScalingGroup(stack, 'ASG', { You can configure [EC2 Instance Metadata Service](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) options to either allow both IMDSv1 and IMDSv2 or enforce IMDSv2 when interacting with the IMDS. -To do this for a single `AutoScalingGroup`, you can use set the `disableImdsv1` property. -The example below demonstrates IMDSv1 being disabled on a single `AutoScalingGroup`: +To do this for a single `AutoScalingGroup`, you can use set the `requireImdsv2` property. +The example below demonstrates IMDSv2 being required on a single `AutoScalingGroup`: ```ts new autoscaling.AutoScalingGroup(stack, 'ASG', { - disableImdsv1: true, + requireImdsv2: true, // ... }); ``` -You can also use `AutoScalingGroupImdsAspect` to apply the operation to multiple AutoScalingGroups. -The example below demonstrates the `AutoScalingGroupImdsAspect` being used to disable IMDSv1 for all AutoScalingGroups in a stack: +You can also use `AutoScalingGroupRequireImdsv2Aspect` to apply the operation to multiple AutoScalingGroups. +The example below demonstrates the `AutoScalingGroupRequireImdsv2Aspect` being used to require IMDSv2 for all AutoScalingGroups in a stack: ```ts -const aspect = new autoscaling.AutoScalingGroupImdsAspect({ - enableImdsV1: false, -}); +const aspect = new autoscaling.AutoScalingGroupRequireImdsv2Aspect(); Aspects.of(stack).add(aspect); ``` diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts deleted file mode 100644 index 17e9ff1551c4f..0000000000000 --- a/packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts +++ /dev/null @@ -1,67 +0,0 @@ -import * as cdk from '@aws-cdk/core'; -import { AutoScalingGroup } from '../auto-scaling-group'; -import { CfnLaunchConfiguration } from '../autoscaling.generated'; - -/** - * Properties for `ImdsAspect`. - */ -interface ImdsAspectProps { - /** - * Whether IMDSv1 should be enabled or not. - */ - readonly enableImdsV1: boolean; -} - -/** - * Base class for IMDS configuration Aspect. - */ -abstract class ImdsAspect implements cdk.IAspect { - protected readonly enableImdsV1: boolean; - - constructor(props: ImdsAspectProps) { - this.enableImdsV1 = props.enableImdsV1; - } - - abstract visit(node: cdk.IConstruct): void; - - /** - * Adds a warning annotation to a node. - * - * @param node The scope to add the warning to. - * @param message The warning message. - */ - protected warn(node: cdk.IConstruct, message: string) { - cdk.Annotations.of(node).addWarning(`${ImdsAspect.name} failed on node ${node.node.id}: ${message}`); - } -} - -/** - * Properties for `AutoScalingGroupImdsAspect`. - */ -export interface AutoScalingGroupImdsAspectProps extends ImdsAspectProps {} - -/** - * Aspect that applies IMDS configuration to AutoScalingGroups. - */ -export class AutoScalingGroupImdsAspect extends ImdsAspect { - constructor(props: AutoScalingGroupImdsAspectProps) { - super(props); - } - - visit(node: cdk.IConstruct): void { - if (!(node instanceof AutoScalingGroup)) { - return; - } - - const launchConfig = node.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; - if (cdk.isResolvableObject(launchConfig.metadataOptions)) { - this.warn(node, 'CfnLaunchConfiguration.MetadataOptions field is a CDK token.'); - return; - } - - launchConfig.metadataOptions = { - ...launchConfig.metadataOptions, - httpTokens: this.enableImdsV1 ? 'optional' : 'required', - }; - } -} diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts index 25a82e046399c..31fc534776144 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts @@ -1 +1 @@ -export * from './imds-aspect'; \ No newline at end of file +export * from './require-imdsv2-aspect'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts b/packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts new file mode 100644 index 0000000000000..e399dce585d79 --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/lib/aspects/require-imdsv2-aspect.ts @@ -0,0 +1,38 @@ +import * as cdk from '@aws-cdk/core'; +import { AutoScalingGroup } from '../auto-scaling-group'; +import { CfnLaunchConfiguration } from '../autoscaling.generated'; + +/** + * Aspect that makes IMDSv2 required on instances deployed by AutoScalingGroups. + */ +export class AutoScalingGroupRequireImdsv2Aspect implements cdk.IAspect { + constructor() { + } + + public visit(node: cdk.IConstruct): void { + if (!(node instanceof AutoScalingGroup)) { + return; + } + + const launchConfig = node.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; + if (cdk.isResolvableObject(launchConfig.metadataOptions)) { + this.warn(node, 'CfnLaunchConfiguration.MetadataOptions field is a CDK token.'); + return; + } + + launchConfig.metadataOptions = { + ...launchConfig.metadataOptions, + httpTokens: 'required', + }; + } + + /** + * Adds a warning annotation to a node. + * + * @param node The scope to add the warning to. + * @param message The warning message. + */ + protected warn(node: cdk.IConstruct, message: string) { + cdk.Annotations.of(node).addWarning(`${AutoScalingGroupRequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`); + } +} diff --git a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts index 1a99446e5cc0d..45fd06c478dfc 100644 --- a/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts +++ b/packages/@aws-cdk/aws-autoscaling/lib/auto-scaling-group.ts @@ -15,7 +15,7 @@ import { Tokenization, withResolved, } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { AutoScalingGroupImdsAspect } from './aspects'; +import { AutoScalingGroupRequireImdsv2Aspect } from './aspects'; import { CfnAutoScalingGroup, CfnAutoScalingGroupProps, CfnLaunchConfiguration } from './autoscaling.generated'; import { BasicLifecycleHookProps, LifecycleHook } from './lifecycle-hook'; import { BasicScheduledActionProps, ScheduledAction } from './scheduled-action'; @@ -388,11 +388,11 @@ export interface AutoScalingGroupProps extends CommonAutoScalingGroupProps { readonly initOptions?: ApplyCloudFormationInitOptions; /** - * Whether IMDSv1 should be disabled on launched instances. + * Whether IMDSv2 should be required on launched instances. * * @default - false */ - readonly disableImdsv1?: boolean; + readonly requireImdsv2?: boolean; } /** @@ -1075,8 +1075,8 @@ export class AutoScalingGroup extends AutoScalingGroupBase implements this.spotPrice = props.spotPrice; - if (props.disableImdsv1 === true) { - Aspects.of(this).add(new AutoScalingGroupImdsAspect({ enableImdsV1: false })); + if (props.requireImdsv2) { + Aspects.of(this).add(new AutoScalingGroupRequireImdsv2Aspect()); } } diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts deleted file mode 100644 index 3e3a2d929f9f7..0000000000000 --- a/packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { - expect as expectCDK, - haveResourceLike, -} from '@aws-cdk/assert-internal'; -import '@aws-cdk/assert-internal/jest'; -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as cdk from '@aws-cdk/core'; -import { - AutoScalingGroup, - AutoScalingGroupImdsAspect, - CfnLaunchConfiguration, -} from '../../lib'; - -describe('ImdsAspect', () => { - let app: cdk.App; - let stack: cdk.Stack; - let vpc: ec2.Vpc; - - beforeEach(() => { - app = new cdk.App(); - stack = new cdk.Stack(app, 'Stack'); - vpc = new ec2.Vpc(stack, 'Vpc'); - }); - - describe('AutoScalingGroupImdsAspect', () => { - test('warns when metadataOptions is a token', () => { - // GIVEN - const asg = new AutoScalingGroup(stack, 'AutoScalingGroup', { - vpc, - instanceType: new ec2.InstanceType('t2.micro'), - machineImage: ec2.MachineImage.latestAmazonLinux(), - }); - const launchConfig = asg.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; - launchConfig.metadataOptions = fakeToken(); - const aspect = new AutoScalingGroupImdsAspect({ enableImdsV1: false }); - - // WHEN - cdk.Aspects.of(stack).add(aspect); - - // THEN - expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { - MetadataOptions: { - HttpTokens: 'required', - }, - })); - expect(asg.node.metadataEntry).toContainEqual({ - data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), - type: 'aws:cdk:warning', - trace: undefined, - }); - }); - - test.each([ - [true], - [false], - ])('toggles IMDSv1 (enabled=%s)', (enableImdsV1: boolean) => { - // GIVEN - new AutoScalingGroup(stack, 'AutoScalingGroup', { - vpc, - instanceType: new ec2.InstanceType('t2.micro'), - machineImage: ec2.MachineImage.latestAmazonLinux(), - }); - const aspect = new AutoScalingGroupImdsAspect({ enableImdsV1 }); - - // WHEN - cdk.Aspects.of(stack).add(aspect); - - // THEN - expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { - MetadataOptions: { - HttpTokens: enableImdsV1 ? 'optional' : 'required', - }, - })); - }); - }); -}); - -function fakeToken(): cdk.IResolvable { - return { - creationStack: [], - resolve: (_c) => {}, - toString: () => '', - }; -} diff --git a/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts new file mode 100644 index 0000000000000..22a58f097a98b --- /dev/null +++ b/packages/@aws-cdk/aws-autoscaling/test/aspects/require-imdsv2-aspect.test.ts @@ -0,0 +1,79 @@ +import { + expect as expectCDK, + haveResourceLike, +} from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import { + AutoScalingGroup, + AutoScalingGroupRequireImdsv2Aspect, + CfnLaunchConfiguration, +} from '../../lib'; + +describe('AutoScalingGroupRequireImdsv2Aspect', () => { + let app: cdk.App; + let stack: cdk.Stack; + let vpc: ec2.Vpc; + + beforeEach(() => { + app = new cdk.App(); + stack = new cdk.Stack(app, 'Stack'); + vpc = new ec2.Vpc(stack, 'Vpc'); + }); + + test('warns when metadataOptions is a token', () => { + // GIVEN + const asg = new AutoScalingGroup(stack, 'AutoScalingGroup', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + }); + const launchConfig = asg.node.tryFindChild('LaunchConfig') as CfnLaunchConfiguration; + launchConfig.metadataOptions = fakeToken(); + const aspect = new AutoScalingGroupRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(stack).add(aspect); + + // THEN + expectCDK(stack).notTo(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + })); + expect(asg.node.metadataEntry).toContainEqual({ + data: expect.stringContaining('CfnLaunchConfiguration.MetadataOptions field is a CDK token.'), + type: 'aws:cdk:warning', + trace: undefined, + }); + }); + + test('requires IMDSv2', () => { + // GIVEN + new AutoScalingGroup(stack, 'AutoScalingGroup', { + vpc, + instanceType: new ec2.InstanceType('t2.micro'), + machineImage: ec2.MachineImage.latestAmazonLinux(), + }); + const aspect = new AutoScalingGroupRequireImdsv2Aspect(); + + // WHEN + cdk.Aspects.of(stack).add(aspect); + + // THEN + expectCDK(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + MetadataOptions: { + HttpTokens: 'required', + }, + })); + }); +}); + +function fakeToken(): cdk.IResolvable { + return { + creationStack: [], + resolve: (_c) => {}, + toString: () => '', + }; +} diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 02d81aeb5c6fa..d74860638fd30 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1365,7 +1365,7 @@ describe('auto scaling group', () => { }); - test('disables imdsv1', () => { + test('requires imdsv2', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1375,7 +1375,7 @@ describe('auto scaling group', () => { vpc, instanceType: new ec2.InstanceType('t2.micro'), machineImage: ec2.MachineImage.latestAmazonLinux(), - disableImdsv1: true, + requireImdsv2: true, }); // THEN