Skip to content

Instantly share code, notes, and snippets.

@konarskis
Last active March 30, 2024 14:30
Show Gist options
  • Select an option

  • Save konarskis/b0b822a3b82f5b5b278ed8ad94c59611 to your computer and use it in GitHub Desktop.

Select an option

Save konarskis/b0b822a3b82f5b5b278ed8ad94c59611 to your computer and use it in GitHub Desktop.
Deploy and Secure a Bastion Host with AWS CDK and Typescript
#!/usr/bin/env node
import * as cdk from 'aws-cdk-lib';
import { aws_ec2, Duration, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {
CfnEIP,
CfnEIPAssociation,
InstanceClass,
InstanceSize,
InstanceType,
ISubnet,
IVpc,
SecurityGroup,
} from 'aws-cdk-lib/aws-ec2';
import { ARecord, RecordTarget } from 'aws-cdk-lib/aws-route53';
// I suggest moving this to a utility, you'll reuse it in many places
export const findHostedZone = (scope: Construct, domain: string) =>
aws_route53.HostedZone.fromLookup(scope, `${domain}-dns-hosted-zone`, {
domainName: domain,
});
export interface BastionProps {
vpc: IVpc;
subnets: ISubnet[];
securityGroup: SecurityGroup;
domain?: string;
subdomain?: string;
}
/**
* Bastion host deployed in a public subnet to gain access to resources in isolated subnets in the VPC.
* Access is controlled by configuring public SSH keys in the Bastion host itself.
* Configuring SSH keys should be the only real reason to connect directly to the bastion, in most cases,
* it should be used as an SSH tunnel to connect to the private services in the VPC.
*
* Additionally, network access is controlled by modifying allowed IP addresses in the Bastion security group.
*
* The Amazon Machine Image (AMI) of the EC2 instance is fixed to prevent replacements when new AMI versions are available.
* It is recommended to look up the latest available AMI every several months and update it months to benefit from security patches.
* However, this causes the public SSH keys to be wiped from the instance.
* Please be sure to back up '.ssh/authorized_keys' before performing the updated and recreate the file manually shortly
* after the upgrade to prevent downtime in access to internal resources.
*
* Request SSH Access (Developer)
* 1. Make sure you have access to your application secret in AWS Secrets manager in the account(s) of your interest
* 2. Open your terminal and run 'ssh-keygen -t rsa', then walk through to generate a keypair, call it '${something}_rsa' with an empty passphrase.
* 3. Run 'cat ${something}_rsa.pub', then copy the result and sent it to one of our AWS admins.
*
* Grant SSH Access (AWS Admin)
* 1. Get the requestor's public key from the procedure above.
* 2. Login to the desired AWS account, go to EC2 -> Instances -> bastion -> Click "Connect" and "Connect".
* 3. Once connected, run 'nano .ssh/authorized_keys', add a line with the public key received from the requestor, then save the file using ⌃O and exit using ⌃X.
*
* Grant security group access (AWS Admin)
* 1. Login to the desired AWS account, go to VPC -> Security Groups -> 'bastion-sg' -> Edit Inbound Rules
* 2. Enable port 22 (SSH) and specify the IP address such as 'X.X.X.X/0'.
*
* For non-production environments with lower security requirements, you can open up the bastion host to
* all IPs to make life easier. To do that, modify the bastion security group to allow all TCP and all ICMP
* communication from 0.0.0.0/0 source IPs.
*/
class Bastion extends Construct {
constructor(parent: Stack, name: string, props: BastionProps) {
super(parent, name);
const host = new aws_ec2.BastionHostLinux(this, 'bastion', {
vpc: props.vpc,
instanceName: 'bastion',
instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.NANO),
machineImage: aws_ec2.MachineImage.lookup({
name: 'amzn2-ami-kernel-5.10-hvm-2.0.20240124.0-arm64-gp2', // otherwise, bastion host gets recreated almost every time
}),
subnetSelection: {
subnets: props.subnets,
},
securityGroup: props.securityGroup,
});
const elasticIp = new CfnEIP(this, 'bastion-elastic-ip');
new CfnEIPAssociation(this, 'bastion-elastic-ip-association', {
eip: elasticIp.ref,
instanceId: host.instanceId,
});
const hostedZone = props.domain ? findHostedZone(this, props.domain) : undefined;
const subdomain = props.subdomain ?? 'bastion';
if (props.domain && hostedZone) {
new ARecord(this, 'bastion-dns-record', {
zone: hostedZone,
target: RecordTarget.fromIpAddresses(elasticIp.attrPublicIp),
recordName: `${subdomain}.${props.domain}`,
ttl: Duration.minutes(5),
});
}
}
}
export class BastionStack extends Stack {
constructor(parent: cdk.App, name: string, props: cdk.StackProps, config: BastionProps) {
super(parent, name, props);
new Bastion(this, 'bastion', config);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment