Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ec2): add aspect to require imdsv2 #16051

Merged
merged 4 commits into from
Oct 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions packages/@aws-cdk/aws-ec2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -994,6 +994,33 @@ instance.userData.addCommands(
);
```

### 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 for a single `Instance`, you can use the `requireImdsv2` property.
The example below demonstrates IMDSv2 being required on a single `Instance`:

```ts
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);
```

## VPC Flow Logs

VPC Flow Logs is a feature that enables you to capture information about the IP traffic going to and from network interfaces in your VPC. Flow log data can be published to Amazon CloudWatch Logs and Amazon S3. After you've created a flow log, you can retrieve and view its data in the chosen destination. (<https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html>).
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/lib/aspects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './require-imdsv2-aspect';
150 changes: 150 additions & 0 deletions packages/@aws-cdk/aws-ec2/lib/aspects/require-imdsv2-aspect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import * as cdk from '@aws-cdk/core';
import { CfnLaunchTemplate } from '../ec2.generated';
import { Instance } from '../instance';
import { LaunchTemplate } from '../launch-template';

/**
* Properties for `RequireImdsv2Aspect`.
*/
interface RequireImdsv2AspectProps {
/**
* Whether warning annotations from this Aspect should be suppressed or not.
*
* @default - false
*/
readonly suppressWarnings?: boolean;
}

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

constructor(props?: RequireImdsv2AspectProps) {
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(`${RequireImdsv2Aspect.name} failed on node ${node.node.id}: ${message}`);
}
}
}

/**
* Properties for `InstanceRequireImdsv2Aspect`.
*/
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.
*
* You can set this to `true` if `LaunchTemplateImdsAspect` is being used alongside this Aspect to
* suppress false-positive warnings because any Launch Templates associated with Instances will still be covered.
*
* @default - false
*/
readonly suppressLaunchTemplateWarning?: boolean;
}

/**
* Aspect that applies IMDS configuration on EC2 Instance constructs.
*
* This aspect configures IMDS on an EC2 instance by creating a Launch Template with the
* IMDS configuration and associating that Launch Template with the instance. If an Instance
* is already associated with a Launch Template, a warning will (optionally) be added to the
* construct node and it will be skipped.
*
* To cover Instances already associated with Launch Templates, use `LaunchTemplateImdsAspect`.
*/
export class InstanceRequireImdsv2Aspect extends RequireImdsv2Aspect {
private readonly suppressLaunchTemplateWarning: boolean;

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

visit(node: cdk.IConstruct): void {
if (!(node instanceof Instance)) {
return;
}
if (node.instance.launchTemplate !== undefined) {
this.warn(node, 'Cannot toggle IMDSv1 because this Instance is associated with an existing Launch Template.');
return;
}

const name = `${node.node.id}LaunchTemplate`;
const launchTemplate = new CfnLaunchTemplate(node, 'LaunchTemplate', {
launchTemplateData: {
metadataOptions: {
httpTokens: 'required',
},
},
launchTemplateName: name,
});
node.instance.launchTemplate = {
launchTemplateName: name,
version: launchTemplate.getAtt('LatestVersionNumber').toString(),
};
}

protected warn(node: cdk.IConstruct, message: string) {
if (this.suppressLaunchTemplateWarning !== true) {
super.warn(node, message);
}
}
}

/**
* Properties for `LaunchTemplateRequireImdsv2Aspect`.
*/
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 LaunchTemplateRequireImdsv2Aspect extends RequireImdsv2Aspect {
constructor(props?: LaunchTemplateRequireImdsv2AspectProps) {
super(props);
}

visit(node: cdk.IConstruct): void {
if (!(node instanceof LaunchTemplate)) {
return;
}

const launchTemplate = node.node.tryFindChild('Resource') as CfnLaunchTemplate;
const data = launchTemplate.launchTemplateData;
if (cdk.isResolvableObject(data)) {
this.warn(node, 'LaunchTemplateData is a CDK token.');
return;
}

const metadataOptions = (data as CfnLaunchTemplate.LaunchTemplateDataProperty).metadataOptions;
if (cdk.isResolvableObject(metadataOptions)) {
this.warn(node, 'LaunchTemplateData.MetadataOptions is a CDK token.');
return;
}

const newData: CfnLaunchTemplate.LaunchTemplateDataProperty = {
...data,
metadataOptions: {
...metadataOptions,
httpTokens: 'required',
},
};
launchTemplate.launchTemplateData = newData;
}
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-ec2/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './aspects';
export * from './bastion-host';
export * from './connections';
export * from './cfn-init';
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
Loading