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

WIP: feat(ec2): add instance resource #3697

Merged
merged 17 commits into from
Aug 23, 2019
Merged

WIP: feat(ec2): add instance resource #3697

merged 17 commits into from
Aug 23, 2019

Conversation

hoegertn
Copy link
Contributor

Add Instance resource and create bastion host construct based on it.

fixes #3174 and #1713


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@mergify
Copy link
Contributor

mergify bot commented Aug 17, 2019

Pull Request Checklist

  • Testing
  • Unit test added (prefer to add a new test rather than modify existing tests)
  • CLI change? Re-run/add CLI integration tests
  • Documentation
  • Inline docs: make sure all public APIs are documented (copy & paste from official AWS docs)
  • README: update module README
  • Design: for significant features, follow the design process
  • Title uses the format type(scope): text
  • Type: fix, feat, refactor go into CHANGELOG, chore is hidden
  • Scope: name of the module without the aws- or cdk- prefix or postfix (e.g. s3 instead of aws-s3-deployment)
  • Style: use all lower-case, do not end with a period
  • Description
  • Rationale: describe rationale of change and approach taken
  • Issues: Indicate issues fixed via: fixes #xxx or closes #xxx
  • Breaking?: last paragraph: BREAKING CHANGE: <describe what changed + link for details>
  • Sensitive Modules (requires 2 PR approvers)
  • IAM document library (in @aws-cdk/aws-iam)
  • EC2 security groups and ACLs (in @aws-cdk/aws-ec2)
  • Grant APIs (if not based on official documentation with a reference)

Copy link
Contributor

@slipdexic slipdexic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thorsten I think this is great addition.

readonly allowClassicSSH?: boolean;
}

export class BastionHost extends Instance {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if the class name should reflect the operating system, e.g BastionHostLinux .
Sadly a number of us have to use Windows Bastion Hosts
That would then leave scope for someone to create a Windows Version.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sound great. I will change it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't Windows/Linux just be a setting?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need this to be a subclass of Instance? What value do we get from that? Why not encapsulate?

this.addUserData('yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm');

if (props.allowClassicSSH) {
this.connections.allowFromAnyIpv4(Port.tcp(22), 'Allow SSH access from everywhere');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this may be a bad idea; the end-user would be opening this instance up to the whole world by default.
The AWS Well-Architected Framework Security Pillar advises against,
There is also a rule in the CIS AWS Foundation to detect such security groups. ( Can be found in SecurityHub)
4.1 Ensure no security groups allow ingress from 0.0.0.0/0 to port 22

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fully agree with this objection. On the other hand, this is exactly what I see with many AWS users. If the server is up to date and key management is working correctly, it should not be a problem to have 22 open and it is the only way if your clients have dynamic IPs from their ISPs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about requiring the user to explicitly enable this via a setting which will be false by default?

constructor(scope: Construct, id: string, props: InstanceProps) {
super(scope, id);

this.securityGroup = new SecurityGroup(this, 'InstanceSecurityGroup', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of creating a security group here, this could be passed on props, then only create a default group is one is not supplied.
This gives the user ultimate flexibility.
In an enterprise or regulated space, the SecuityGroup generally would have been created upfront by another team.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea because I do not like the SSH TCP access to the bastion server at all. So I will make the group suppliable and by default block any access. For the second part of your comment: I generally do not like the approach of other teams creating IAM roles or sec groups. The whole infrastructure should be defined and created by one team. If you want/have to have four-eye validation here I use the way of merge requests with approvals of the CISO/CSIRT or SCPs to prevent creating bad stuff. In an enterprise context, I would expect NACLs to be in place to prevent users from opening ports they are not allowed to.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point agree, but I work in environments that these rules apply, compliance forbids single team.
I have just created PR for NACLs as they were missing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see the compliance issue with single teams, but the question is if you can solve this on an org level instead of a code level. So by using four-eyes merges instead of separated AWS roles. But this is OT of this PR. Change is on the way.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am with @slipdexic - there are many users who simply are not allowed to create security groups or IAM roles and it makes sense to allow people to pass those in optionally.

Copy link
Contributor

@eladb eladb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job!

packages/@aws-cdk/aws-ec2/lib/bastion-host.ts Show resolved Hide resolved
/**
* In which AZ to place the instance within the VPC
*
* @default - Random zone.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Random bit stable?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the order of props.vpc.selectSubnets() stable? I use the first one.

/**
* Use public subnet instead of private one
*
* @default - false
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't we use the standard SubnetSelection type here?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I map this to a standard SubnetSelection so yes I could change it. I wanted to make the interface easier for users instead of handling subnet selections. Just tell me if you want it public or not. But I can just change it.

*
* @default - false
*/
readonly allowClassicSSH?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use enum with Ssh/Ssm instead of a boolean. Acronyms must be pascal cased (Ssh instead of SSH). Otherwise jsii can't delimit words.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed it regarding the other review comment

readonly allowClassicSSH?: boolean;
}

export class BastionHost extends Instance {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't Windows/Linux just be a setting?

this.addUserData('yum install -y https://s3.amazonaws.com/ec2-downloads-windows/SSMAgent/latest/linux_amd64/amazon-ssm-agent.rpm');

if (props.allowClassicSSH) {
this.connections.allowFromAnyIpv4(Port.tcp(22), 'Allow SSH access from everywhere');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about requiring the user to explicitly enable this via a setting which will be false by default?

this.connections.allowFromAnyIpv4(Port.tcp(22), 'Allow SSH access from everywhere');
}

new CfnOutput(this, 'BastionHostId', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be both for SSH and SSM? (description talks about SSM)

* assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com')
* });
*
* @default A role will automatically be created, it can be accessed via the `role` property
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@default value should be -

constructor(scope: Construct, id: string, props: InstanceProps) {
super(scope, id);

this.securityGroup = new SecurityGroup(this, 'InstanceSecurityGroup', {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am with @slipdexic - there are many users who simply are not allowed to create security groups or IAM roles and it makes sense to allow people to pass those in optionally.

});
this.connections = new Connections({ securityGroups: [this.securityGroup] });
this.securityGroups.push(this.securityGroup);
this.node.applyAspect(new Tag(NAME_TAG, props.instanceName || this.node.path));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Tag.add

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I copied this from AutoScalingGroup but I changed it in this PR now.

@hoegertn
Copy link
Contributor Author

@elad I subclassed Instance from an OOP perspective. BastionHostLinux is a special Instance so it is a sub-class. I think I have seen this style several times in CDK, but I can change it if it is not intended to be used.

@slipdexic
Copy link
Contributor

Great work!

@rix0rrr rix0rrr self-assigned this Aug 19, 2019
Copy link
Contributor

@rix0rrr rix0rrr left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this!

Love the Instance; I wonder if we could make the bastion a little more clear and potentially flexible. What do you think?

*
* @default - false
*/
readonly publicSubnets?: boolean;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't this be a subnetSelection?

And shouldn't public subnets be the default? What use is a Bastion host in a private subnet?

EDIT: Oh I see, it probably has to do with whether you intend to access the bastion directly using SSH or using Session Manager. Could you at least make that clear in the documentation text? Or better yet, maybe it should be a property to select between the two?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there are three scenarios:

  1. Public access using SSH -> public subnet
  2. Access via SSM -> private subnet
  3. Access using SSH from on-prem -> private subnet via VPN/DC

So public subnets should not be used imho...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re use cases, we use all of these and also have use case where we have had to have a baston host, (jump box) in all 3 zones.

When conducting penetration tests we will place a (bastion) host in each zone (public, private, ..), sometime the only way to test a zone get to jump through bastions in each zone using proxy chains.
So in my imho this construct should be able to live in any zone, just default to public.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also you should be ideally able to place this in an explicit subnet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, so I still feel we should expose the subnetSelection (and have it default to something sane, and I think PRIVATE is the best sane default we can have). It would immediately benefit from future extensions we would add to the subnetSelection mechanism (for example, selecting individual subnets such as @slipdexic requested). And it should have explicit documentation on there saying you need to select public subnets if you want to access it from the internet.

Would it make sense to have a top-level helper method for a common use case, bastion.allowSshAccessFrom(peer, peer, ...) or something? The only thing it would do is encapsulate port 22, but it might be just a little nicer.

Also, @hoegertn, could you add a section to the README about the 2 Bastion Host use cases (SSM access and SSH access)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I will change it accordingly.

packages/@aws-cdk/aws-ec2/lib/bastion-host.ts Outdated Show resolved Hide resolved
readonly availabilityZone?: string;

/**
* Whether the instance could initiate connections to anywhere by default
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only used if securityGroup is not supplied.

packages/@aws-cdk/aws-ec2/lib/bastion-host.ts Show resolved Hide resolved
@mergify
Copy link
Contributor

mergify bot commented Aug 21, 2019

Codebuild (Continuous Integration) build failed for current commits. Please check log and resolve before PR is merged.

@mergify
Copy link
Contributor

mergify bot commented Aug 21, 2019

Codebuild (Continuous Integration) build failed for current commits. Please check log and resolve before PR is merged.

*
* @experimental
*/
export class BastionHostLinux extends Instance {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for not seeing this before: I also agree with Elad that this host should probably encapsulate rather than extend Instance.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you elaborate on why? I still think that a bastion host is a special instance and should derive from it, so users have all the options they have with an instance but with some defaults regarding the use case. I thought OOP is one of the benefits of CDK.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We achieve OOP/polymorphism by implementing interfaces. Interfaces declare "a (this) can do the same thing as a (that)", regardless of the implementation. So if you want a Bastion Host to be-an Instance, then declare it like so:

class BastionHostLinux implements IInstance 

Inheriting from a class brings both the "(this) can do the same as (that)" relationship, but it also couples one implementation to the other, and can have unintended implications for the future extensibility of the classes involved (either a change in the implementation of Instance has an unintended side effect in the implementation of BastionHost, or we now face the limitation that BastionHost can only extend one class).

In some situations implementation sharing is a perfectly valid choice (which would usually be made to make it quicker to implement a certain class since you don't have to type so much), but polymorphism through interface implementation is generally a safer choice in the long term, when it's hard to predict future changes.

In this case, I'd rather be safe than sorry later.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, I will change it and see what methods and fields I want to expose from the instance class.

@rix0rrr rix0rrr added the pr/do-not-merge This PR should not be merged at this time. label Aug 23, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
pr/do-not-merge This PR should not be merged at this time.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Create new and import existing EC2 instance
4 participants