Skip to content

Commit

Permalink
feat: add bastion host
Browse files Browse the repository at this point in the history
  • Loading branch information
alukach committed Oct 26, 2022
1 parent fc7ff20 commit de52ad7
Show file tree
Hide file tree
Showing 2 changed files with 194 additions and 0 deletions.
193 changes: 193 additions & 0 deletions lib/bastion-host/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
import {
Stack,
aws_ec2 as ec2,
aws_iam as iam,
aws_rds as rds,
CfnOutput,
} from "aws-cdk-lib";
import { Construct } from "constructs";

/**
* ## Bastion Host
*
* The database is located in an isolated subnet, meaning that it is not accessible from the public internet. As such, to interact with the database directly, a user must tunnel through a bastion host.
*
* ### Configuring
*
* This codebase controls _who_ is allowed to connect to the bastion host. This requires two steps:
*
* 1. Adding the IP address from which you are connecting to the `ipv4Allowlist` array
* 1. Creating a bastion host system user by adding the user's configuration inform to `userdata.yaml`
*
* #### Adding an IP address to the `ipv4Allowlist` array
*
* The `BastionHost` construct takes in an `ipv4Allowlist` array as an argument. Find your IP address (eg `curl api.ipify.org`) and add that to the array along with the trailing CIDR block (likely `/32` to indicate that you are adding a single IP address).
*
* #### Creating a user via `userdata.yaml`
*
* Add an entry to the `users` array with a username (likely matching your local systems username, which you can get by running the `whoami` command in your terminal) and a public key (likely your default public key, which you can get by running `cat ~/.ssh/id_*.pub` in your terminal).
*
* <details>
*
* <summary>Tips & Tricks when using the Bastion Host</summary>
*
* #### Connecting to RDS Instance via SSM
*
* ```sh
* aws ssm start-session --target $INSTANCE_ID \
* --document-name AWS-StartPortForwardingSessionToRemoteHost \
* --parameters '{
* "host": [
* "example-db.c5abcdefghij.us-west-2.rds.amazonaws.com"
* ],
* "portNumber": [
* "5432"
* ],
* "localPortNumber": [
* "9999"
* ]
* }' \
* --profile $AWS_PROFILE
* ```
*
* ```sh
* psql -h localhost -p 9999 # continue adding username (-U) and db (-d) here...
* ```
*
* Connect directly to Bastion Host:
*
* ```sh
* aws ssm start-session --target $INSTANCE_ID --profile $AWS_PROFILE
* ```
*
* #### Setting up an SSH tunnel
*
* In your `~/.ssh/config` file, add an entry like:
*
* ```
* Host db-tunnel
* Hostname {the-bastion-host-address}
* LocalForward 54322 {the-db-hostname}:5432
* ```
*
* Then a tunnel can be opened via:
*
* ```
* ssh -N db-tunnel
* ```
*
* And a connection to the DB can be made via:
*
* ```
* psql -h 127.0.0.1 -p 5433 -U {username} -d {database}
* ```
*
* #### Handling `REMOTE HOST IDENTIFICATION HAS CHANGED!` error
*
* If you've redeployed a bastion host that you've previously connected to, you may see an error like:
*
* ```
* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
* IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
* Someone could be eavesdropping on you right now (man-in-the-middle attack)!
* It is also possible that a host key has just been changed.
* The fingerprint for the ECDSA key sent by the remote host is
* SHA256:mPnxAOXTpb06PFgI1Qc8TMQ2e9b7goU8y2NdS5hzIr8.
* Please contact your system administrator.
* Add correct host key in /Users/username/.ssh/known_hosts to get rid of this message.
* Offending ECDSA key in /Users/username/.ssh/known_hosts:28
* ECDSA host key for ec2-12-34-56-789.us-west-2.compute.amazonaws.com has changed and you have requested strict checking.
* Host key verification failed.
* ```
*
* This is due to the server's fingerprint changing. We can scrub the fingerprint from our system with a command like:
*
* ```
* ssh-keygen -R 12.34.56.789
* ```
*
* </details>
*/
export class BastionHost extends Construct {
instance: ec2.Instance;

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

const { stackName } = Stack.of(this);

// Build ec2 instance
this.instance = new ec2.Instance(this, "bastion-host", {
vpc: props.vpc,
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
instanceName: stackName,
instanceType: ec2.InstanceType.of(
ec2.InstanceClass.BURSTABLE4_GRAVITON,
ec2.InstanceSize.NANO
),
machineImage: ec2.MachineImage.latestAmazonLinux({
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
}),
userData: props.userData,
userDataCausesReplacement: true,
});

// Assign elastic IP
new ec2.CfnEIP(this, "IP", {
instanceId: this.instance.instanceId,
tags: [{ key: "Name", value: stackName }],
});

// Allow bastion host to connect to db
this.instance.connections.allowTo(
props.db.connections.securityGroups[0],
ec2.Port.tcp(5432),
"Allow connection from bastion host"
);

// Allow IP access to bastion host
for (const ipv4 of props.ipv4Allowlist) {
this.instance.connections.allowFrom(
ec2.Peer.ipv4(ipv4),
ec2.Port.tcp(props.sshPort || 22),
"SSH Access"
);
}

// Integrate with SSM
this.instance.addToRolePolicy(
new iam.PolicyStatement({
actions: [
"ssmmessages:*",
"ssm:UpdateInstanceInformation",
"ec2messages:*",
],
resources: ["*"],
})
);

new CfnOutput(this, "instance-id-output", {
value: this.instance.instanceId,
exportName: `${stackName}-instance-id`,
});
new CfnOutput(this, "instance-public-ip-output", {
value: this.instance.instancePublicIp,
exportName: `${stackName}-instance-public-ip`,
});
new CfnOutput(this, "instance-public-dns-name-output", {
value: this.instance.instancePublicDnsName,
exportName: `${stackName}-public-dns-name`,
});
}
}

export interface BastionHostProps {
readonly vpc: ec2.IVpc;
readonly db: rds.IDatabaseInstance;
readonly userData: ec2.UserData;
readonly ipv4Allowlist: string[];
readonly sshPort?: number;
}
1 change: 1 addition & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./bastion-host";
export * from "./bootstrapper";
export * from "./database";
export * from "./stac-api";

0 comments on commit de52ad7

Please sign in to comment.