Skip to content

Commit de52ad7

Browse files
committed
feat: add bastion host
1 parent fc7ff20 commit de52ad7

File tree

2 files changed

+194
-0
lines changed

2 files changed

+194
-0
lines changed

lib/bastion-host/index.ts

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import {
2+
Stack,
3+
aws_ec2 as ec2,
4+
aws_iam as iam,
5+
aws_rds as rds,
6+
CfnOutput,
7+
} from "aws-cdk-lib";
8+
import { Construct } from "constructs";
9+
10+
/**
11+
* ## Bastion Host
12+
*
13+
* 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.
14+
*
15+
* ### Configuring
16+
*
17+
* This codebase controls _who_ is allowed to connect to the bastion host. This requires two steps:
18+
*
19+
* 1. Adding the IP address from which you are connecting to the `ipv4Allowlist` array
20+
* 1. Creating a bastion host system user by adding the user's configuration inform to `userdata.yaml`
21+
*
22+
* #### Adding an IP address to the `ipv4Allowlist` array
23+
*
24+
* 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).
25+
*
26+
* #### Creating a user via `userdata.yaml`
27+
*
28+
* 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).
29+
*
30+
* <details>
31+
*
32+
* <summary>Tips & Tricks when using the Bastion Host</summary>
33+
*
34+
* #### Connecting to RDS Instance via SSM
35+
*
36+
* ```sh
37+
* aws ssm start-session --target $INSTANCE_ID \
38+
* --document-name AWS-StartPortForwardingSessionToRemoteHost \
39+
* --parameters '{
40+
* "host": [
41+
* "example-db.c5abcdefghij.us-west-2.rds.amazonaws.com"
42+
* ],
43+
* "portNumber": [
44+
* "5432"
45+
* ],
46+
* "localPortNumber": [
47+
* "9999"
48+
* ]
49+
* }' \
50+
* --profile $AWS_PROFILE
51+
* ```
52+
*
53+
* ```sh
54+
* psql -h localhost -p 9999 # continue adding username (-U) and db (-d) here...
55+
* ```
56+
*
57+
* Connect directly to Bastion Host:
58+
*
59+
* ```sh
60+
* aws ssm start-session --target $INSTANCE_ID --profile $AWS_PROFILE
61+
* ```
62+
*
63+
* #### Setting up an SSH tunnel
64+
*
65+
* In your `~/.ssh/config` file, add an entry like:
66+
*
67+
* ```
68+
* Host db-tunnel
69+
* Hostname {the-bastion-host-address}
70+
* LocalForward 54322 {the-db-hostname}:5432
71+
* ```
72+
*
73+
* Then a tunnel can be opened via:
74+
*
75+
* ```
76+
* ssh -N db-tunnel
77+
* ```
78+
*
79+
* And a connection to the DB can be made via:
80+
*
81+
* ```
82+
* psql -h 127.0.0.1 -p 5433 -U {username} -d {database}
83+
* ```
84+
*
85+
* #### Handling `REMOTE HOST IDENTIFICATION HAS CHANGED!` error
86+
*
87+
* If you've redeployed a bastion host that you've previously connected to, you may see an error like:
88+
*
89+
* ```
90+
* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
91+
* @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
92+
* @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
93+
* IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
94+
* Someone could be eavesdropping on you right now (man-in-the-middle attack)!
95+
* It is also possible that a host key has just been changed.
96+
* The fingerprint for the ECDSA key sent by the remote host is
97+
* SHA256:mPnxAOXTpb06PFgI1Qc8TMQ2e9b7goU8y2NdS5hzIr8.
98+
* Please contact your system administrator.
99+
* Add correct host key in /Users/username/.ssh/known_hosts to get rid of this message.
100+
* Offending ECDSA key in /Users/username/.ssh/known_hosts:28
101+
* ECDSA host key for ec2-12-34-56-789.us-west-2.compute.amazonaws.com has changed and you have requested strict checking.
102+
* Host key verification failed.
103+
* ```
104+
*
105+
* This is due to the server's fingerprint changing. We can scrub the fingerprint from our system with a command like:
106+
*
107+
* ```
108+
* ssh-keygen -R 12.34.56.789
109+
* ```
110+
*
111+
* </details>
112+
*/
113+
export class BastionHost extends Construct {
114+
instance: ec2.Instance;
115+
116+
constructor(scope: Construct, id: string, props: BastionHostProps) {
117+
super(scope, id);
118+
119+
const { stackName } = Stack.of(this);
120+
121+
// Build ec2 instance
122+
this.instance = new ec2.Instance(this, "bastion-host", {
123+
vpc: props.vpc,
124+
vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC },
125+
instanceName: stackName,
126+
instanceType: ec2.InstanceType.of(
127+
ec2.InstanceClass.BURSTABLE4_GRAVITON,
128+
ec2.InstanceSize.NANO
129+
),
130+
machineImage: ec2.MachineImage.latestAmazonLinux({
131+
generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2,
132+
cpuType: ec2.AmazonLinuxCpuType.ARM_64,
133+
}),
134+
userData: props.userData,
135+
userDataCausesReplacement: true,
136+
});
137+
138+
// Assign elastic IP
139+
new ec2.CfnEIP(this, "IP", {
140+
instanceId: this.instance.instanceId,
141+
tags: [{ key: "Name", value: stackName }],
142+
});
143+
144+
// Allow bastion host to connect to db
145+
this.instance.connections.allowTo(
146+
props.db.connections.securityGroups[0],
147+
ec2.Port.tcp(5432),
148+
"Allow connection from bastion host"
149+
);
150+
151+
// Allow IP access to bastion host
152+
for (const ipv4 of props.ipv4Allowlist) {
153+
this.instance.connections.allowFrom(
154+
ec2.Peer.ipv4(ipv4),
155+
ec2.Port.tcp(props.sshPort || 22),
156+
"SSH Access"
157+
);
158+
}
159+
160+
// Integrate with SSM
161+
this.instance.addToRolePolicy(
162+
new iam.PolicyStatement({
163+
actions: [
164+
"ssmmessages:*",
165+
"ssm:UpdateInstanceInformation",
166+
"ec2messages:*",
167+
],
168+
resources: ["*"],
169+
})
170+
);
171+
172+
new CfnOutput(this, "instance-id-output", {
173+
value: this.instance.instanceId,
174+
exportName: `${stackName}-instance-id`,
175+
});
176+
new CfnOutput(this, "instance-public-ip-output", {
177+
value: this.instance.instancePublicIp,
178+
exportName: `${stackName}-instance-public-ip`,
179+
});
180+
new CfnOutput(this, "instance-public-dns-name-output", {
181+
value: this.instance.instancePublicDnsName,
182+
exportName: `${stackName}-public-dns-name`,
183+
});
184+
}
185+
}
186+
187+
export interface BastionHostProps {
188+
readonly vpc: ec2.IVpc;
189+
readonly db: rds.IDatabaseInstance;
190+
readonly userData: ec2.UserData;
191+
readonly ipv4Allowlist: string[];
192+
readonly sshPort?: number;
193+
}

lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
export * from "./bastion-host";
12
export * from "./bootstrapper";
23
export * from "./database";
34
export * from "./stac-api";

0 commit comments

Comments
 (0)