Skip to content

Commit

Permalink
rename ImdsAspect and add options to require IMDSv2 on Instance and L…
Browse files Browse the repository at this point in the history
…aunchTemplate constructs
  • Loading branch information
jericht committed Oct 20, 2021
1 parent fd992e4 commit 6d95697
Show file tree
Hide file tree
Showing 8 changed files with 124 additions and 60 deletions.
19 changes: 14 additions & 5 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -999,16 +999,25 @@ instance.userData.addCommands(
#### 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 either the `InstanceImdsAspect` for EC2 instances
or the `LaunchTemplateImdsAspect` for EC2 launch templates.
allow both IMDSv1 and IMDSv2 or enforce IMDSv2 when interacting with the IMDS.

The following example demonstrates how to use the `InstanceImdsAspect` to disable IMDSv1 (thus enforcing IMDSv2) for all EC2 instances in a stack:
To do this for a single `Instance`, you can use the `requireImdsv2` property.
The example below demonstrates IMDSv2 being required on a single `Instance`:

```ts
const aspect = new ec2.InstanceImdsAspect({
enableImdsV1: false,
new ec2.Instance(this, 'Instance', {
requireImdsv2: true,
// ...
});
```

You can also use the either the `InstanceRequireImdsv2Aspect` for EC2 instances or the `LaunchTemplateRequireImdsv2Aspect` for EC2 launch templates
to apply the operation to multiple instances or launch templates, respectively.

The following example demonstrates how to use the `InstanceRequireImdsv2Aspect` to require IMDSv2 for all EC2 instances in a stack:

```ts
const aspect = new ec2.InstanceRequireImdsv2Aspect();
Aspects.of(stack).add(aspect);
```

Expand Down
2 changes: 1 addition & 1 deletion packages/@aws-cdk/aws-ec2/lib/aspects/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export * from './imds-aspect';
export * from './require-imdsv2-aspect';
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,9 @@ import { Instance } from '../instance';
import { LaunchTemplate } from '../launch-template';

/**
* Properties for `ImdsAspect`.
* Properties for `RequireImdsv2Aspect`.
*/
interface ImdsAspectProps {
/**
* Whether IMDSv1 should be enabled or not.
*/
readonly enableImdsV1: boolean;

interface RequireImdsv2AspectProps {
/**
* Whether warning annotations from this Aspect should be suppressed or not.
*
Expand All @@ -21,15 +16,13 @@ interface ImdsAspectProps {
}

/**
* Base class for IMDS configuration Aspect.
* Base class for Aspect that makes IMDSv2 required.
*/
abstract class ImdsAspect implements cdk.IAspect {
protected readonly enableImdsV1: boolean;
abstract class RequireImdsv2Aspect implements cdk.IAspect {
protected readonly suppressWarnings: boolean;

constructor(props: ImdsAspectProps) {
this.enableImdsV1 = props.enableImdsV1;
this.suppressWarnings = props.suppressWarnings ?? false;
constructor(props?: RequireImdsv2AspectProps) {
this.suppressWarnings = props?.suppressWarnings ?? false;
}

abstract visit(node: cdk.IConstruct): void;
Expand All @@ -42,15 +35,15 @@ abstract class ImdsAspect implements cdk.IAspect {
*/
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(`${RequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`);
}
}
}

/**
* Properties for `InstanceImdsAspect`.
* Properties for `InstanceRequireImdsv2Aspect`.
*/
export interface InstanceImdsAspectProps extends ImdsAspectProps {
export interface InstanceRequireImdsv2AspectProps extends RequireImdsv2AspectProps {
/**
* Whether warnings that would be raised when an Instance is associated with an existing Launch Template
* should be suppressed or not.
Expand All @@ -73,12 +66,12 @@ export interface InstanceImdsAspectProps extends ImdsAspectProps {
*
* To cover Instances already associated with Launch Templates, use `LaunchTemplateImdsAspect`.
*/
export class InstanceImdsAspect extends ImdsAspect {
export class InstanceRequireImdsv2Aspect extends RequireImdsv2Aspect {
private readonly suppressLaunchTemplateWarning: boolean;

constructor(props: InstanceImdsAspectProps) {
constructor(props?: InstanceRequireImdsv2AspectProps) {
super(props);
this.suppressLaunchTemplateWarning = props.suppressLaunchTemplateWarning ?? false;
this.suppressLaunchTemplateWarning = props?.suppressLaunchTemplateWarning ?? false;
}

visit(node: cdk.IConstruct): void {
Expand All @@ -94,7 +87,7 @@ export class InstanceImdsAspect extends ImdsAspect {
const launchTemplate = new CfnLaunchTemplate(node, 'LaunchTemplate', {
launchTemplateData: {
metadataOptions: {
httpTokens: this.enableImdsV1 ? 'optional' : 'required',
httpTokens: 'required',
},
},
launchTemplateName: name,
Expand All @@ -113,17 +106,17 @@ export class InstanceImdsAspect extends ImdsAspect {
}

/**
* Properties for `LaunchTemplateImdsAspect`.
* Properties for `LaunchTemplateRequireImdsv2Aspect`.
*/
export interface LaunchTemplateImdsAspectProps extends ImdsAspectProps {}
export interface LaunchTemplateRequireImdsv2AspectProps extends RequireImdsv2AspectProps {}

/**
* Aspect that applies IMDS configuration on EC2 Launch Template constructs.
*
* @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ec2-launchtemplate-launchtemplatedata-metadataoptions.html
*/
export class LaunchTemplateImdsAspect extends ImdsAspect {
constructor(props: LaunchTemplateImdsAspectProps) {
export class LaunchTemplateRequireImdsv2Aspect extends RequireImdsv2Aspect {
constructor(props?: LaunchTemplateRequireImdsv2AspectProps) {
super(props);
}

Expand All @@ -149,7 +142,7 @@ export class LaunchTemplateImdsAspect extends ImdsAspect {
...data,
metadataOptions: {
...metadataOptions,
httpTokens: this.enableImdsV1 ? 'optional' : 'required',
httpTokens: 'required',
},
};
launchTemplate.launchTemplateData = newData;
Expand Down
14 changes: 13 additions & 1 deletion packages/@aws-cdk/aws-ec2/lib/instance.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as crypto from 'crypto';
import * as iam from '@aws-cdk/aws-iam';

import { Annotations, Duration, Fn, IResource, Lazy, Resource, Stack, Tags } from '@aws-cdk/core';
import { Annotations, Aspects, Duration, Fn, IResource, Lazy, Resource, Stack, Tags } from '@aws-cdk/core';
import { Construct } from 'constructs';
import { InstanceRequireImdsv2Aspect } from './aspects';
import { CloudFormationInit } from './cfn-init';
import { Connections, IConnectable } from './connections';
import { CfnInstance } from './ec2.generated';
Expand Down Expand Up @@ -230,6 +231,13 @@ export interface InstanceProps {
* @default - default options
*/
readonly initOptions?: ApplyCloudFormationInitOptions;

/**
* Whether IMDSv2 should be required on this instance.
*
* @default - false
*/
readonly requireImdsv2?: boolean;
}

/**
Expand Down Expand Up @@ -408,6 +416,10 @@ export class Instance extends Resource implements IInstance {
return `${originalLogicalId}${digest}`;
},
}));

if (props.requireImdsv2) {
Aspects.of(this).add(new InstanceRequireImdsv2Aspect());
}
}

/**
Expand Down
13 changes: 13 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/launch-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import {
TagType,
Tags,
Token,
Aspects,
} from '@aws-cdk/core';
import { Construct } from 'constructs';
import { LaunchTemplateRequireImdsv2Aspect } from '.';
import { Connections, IConnectable } from './connections';
import { CfnLaunchTemplate } from './ec2.generated';
import { InstanceType } from './instance-types';
Expand Down Expand Up @@ -332,6 +334,13 @@ export interface LaunchTemplateProps {
* @default No security group is assigned.
*/
readonly securityGroup?: ISecurityGroup;

/**
* Whether IMDSv2 should be required on launched instances.
*
* @default - false
*/
readonly requireImdsv2?: boolean;
}

/**
Expand Down Expand Up @@ -637,6 +646,10 @@ export class LaunchTemplate extends Resource implements ILaunchTemplate, iam.IGr
this.latestVersionNumber = resource.attrLatestVersionNumber;
this.launchTemplateId = resource.ref;
this.versionNumber = Token.asString(resource.getAtt('LatestVersionNumber'));

if (props.requireImdsv2) {
Aspects.of(this).add(new LaunchTemplateRequireImdsv2Aspect());
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import {
} from '@aws-cdk/assert-internal';
import '@aws-cdk/assert-internal/jest';
import * as cdk from '@aws-cdk/core';
import * as sinon from 'sinon';
import {
CfnLaunchTemplate,
Instance,
InstanceImdsAspect,
InstanceRequireImdsv2Aspect,
InstanceType,
LaunchTemplate,
LaunchTemplateImdsAspect,
LaunchTemplateRequireImdsv2Aspect,
MachineImage,
Vpc,
} from '../../lib';

describe('ImdsAspect', () => {
describe('RequireImdsv2Aspect', () => {
let app: cdk.App;
let stack: cdk.Stack;
let vpc: Vpc;
Expand All @@ -30,12 +29,11 @@ describe('ImdsAspect', () => {

test('suppresses warnings', () => {
// GIVEN
const aspect = new LaunchTemplateImdsAspect({
enableImdsV1: true,
const aspect = new LaunchTemplateRequireImdsv2Aspect({
suppressWarnings: true,
});
const errmsg = 'ERROR';
const stub = sinon.stub(aspect, 'visit').callsFake((node) => {
const visitMock = jest.spyOn(aspect, 'visit').mockImplementation((node) => {
// @ts-ignore
aspect.warn(node, errmsg);
});
Expand All @@ -45,26 +43,23 @@ describe('ImdsAspect', () => {
aspect.visit(construct);

// THEN
expect(stub.calledOnce).toBeTruthy();
expect(visitMock).toHaveBeenCalled();
expect(construct.node.metadataEntry).not.toContainEqual({
data: expect.stringContaining(errmsg),
type: 'aws:cdk:warning',
trace: undefined,
});
});

describe('InstanceImdsAspect', () => {
test.each([
[true],
[false],
])('toggles IMDSv1 (enabled=%s)', (enableImdsV1: boolean) => {
describe('InstanceRequireImdsv2Aspect', () => {
test('requires IMDSv2', () => {
// GIVEN
const instance = new Instance(stack, 'Instance', {
vpc,
instanceType: new InstanceType('t2.micro'),
machineImage: MachineImage.latestAmazonLinux(),
});
const aspect = new InstanceImdsAspect({ enableImdsV1 });
const aspect = new InstanceRequireImdsv2Aspect();

// WHEN
cdk.Aspects.of(stack).add(aspect);
Expand All @@ -77,7 +72,7 @@ describe('ImdsAspect', () => {
LaunchTemplateName: stack.resolve(launchTemplate.launchTemplateName),
LaunchTemplateData: {
MetadataOptions: {
HttpTokens: enableImdsV1 ? 'optional' : 'required',
HttpTokens: 'required',
},
},
}));
Expand All @@ -99,7 +94,7 @@ describe('ImdsAspect', () => {
launchTemplateName: 'name',
version: 'version',
};
const aspect = new InstanceImdsAspect({ enableImdsV1: false });
const aspect = new InstanceRequireImdsv2Aspect();

// WHEN
cdk.Aspects.of(stack).add(aspect);
Expand All @@ -126,8 +121,7 @@ describe('ImdsAspect', () => {
launchTemplateName: 'name',
version: 'version',
};
const aspect = new InstanceImdsAspect({
enableImdsV1: false,
const aspect = new InstanceRequireImdsv2Aspect({
suppressLaunchTemplateWarning: true,
});

Expand All @@ -143,13 +137,13 @@ describe('ImdsAspect', () => {
});
});

describe('LaunchTemplateImdsAspect', () => {
describe('LaunchTemplateRequireImdsv2Aspect', () => {
test('warns when LaunchTemplateData is a CDK token', () => {
// GIVEN
const launchTemplate = new LaunchTemplate(stack, 'LaunchTemplate');
const cfnLaunchTemplate = launchTemplate.node.tryFindChild('Resource') as CfnLaunchTemplate;
cfnLaunchTemplate.launchTemplateData = fakeToken();
const aspect = new LaunchTemplateImdsAspect({ enableImdsV1: false });
const aspect = new LaunchTemplateRequireImdsv2Aspect();

// WHEN
aspect.visit(launchTemplate);
Expand All @@ -169,7 +163,7 @@ describe('ImdsAspect', () => {
cfnLaunchTemplate.launchTemplateData = {
metadataOptions: fakeToken(),
} as CfnLaunchTemplate.LaunchTemplateDataProperty;
const aspect = new LaunchTemplateImdsAspect({ enableImdsV1: false });
const aspect = new LaunchTemplateRequireImdsv2Aspect();

// WHEN
aspect.visit(launchTemplate);
Expand All @@ -182,13 +176,10 @@ describe('ImdsAspect', () => {
});
});

test.each([
[true],
[false],
])('toggles IMDSv1 (enabled=%s)', (enableImdsV1: boolean) => {
test('requires IMDSv2', () => {
// GIVEN
new LaunchTemplate(stack, 'LaunchTemplate');
const aspect = new LaunchTemplateImdsAspect({ enableImdsV1 });
const aspect = new LaunchTemplateRequireImdsv2Aspect();

// WHEN
cdk.Aspects.of(stack).add(aspect);
Expand All @@ -197,7 +188,7 @@ describe('ImdsAspect', () => {
expectCDK(stack).to(haveResourceLike('AWS::EC2::LaunchTemplate', {
LaunchTemplateData: {
MetadataOptions: {
HttpTokens: enableImdsV1 ? 'optional' : 'required',
HttpTokens: 'required',
},
},
}));
Expand Down
Loading

0 comments on commit 6d95697

Please sign in to comment.