Skip to content

Commit

Permalink
feat(aws-autoscaling): add aspect to enable/disable imdsv1
Browse files Browse the repository at this point in the history
  • Loading branch information
jericht committed Aug 13, 2021
1 parent 1b29ca8 commit ff3f13d
Show file tree
Hide file tree
Showing 6 changed files with 214 additions and 0 deletions.
17 changes: 17 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
83 changes: 83 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/lib/aspects/imds-aspect.ts
Original file line number Diff line number Diff line change
@@ -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';
}
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-autoscaling/lib/aspects/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './imds-aspect';
1 change: 1 addition & 0 deletions packages/@aws-cdk/aws-autoscaling/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './aspects';
export * from './auto-scaling-group';
export * from './schedule';
export * from './lifecycle-hook';
Expand Down
2 changes: 2 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,14 @@
"license": "Apache-2.0",
"devDependencies": {
"@types/jest": "^26.0.24",
"@types/sinon": "^9.0.11",
"@aws-cdk/cx-api": "0.0.0",
"cdk-build-tools": "0.0.0",
"cdk-integ-tools": "0.0.0",
"cfn2ts": "0.0.0",
"nodeunit-shim": "0.0.0",
"pkglint": "0.0.0",
"sinon": "^9.2.4",
"@aws-cdk/cloud-assembly-schema": "0.0.0",
"@aws-cdk/assert-internal": "0.0.0"
},
Expand Down
110 changes: 110 additions & 0 deletions packages/@aws-cdk/aws-autoscaling/test/aspects/imds-aspect.test.ts
Original file line number Diff line number Diff line change
@@ -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: () => '',
};
}

0 comments on commit ff3f13d

Please sign in to comment.