Last active
March 30, 2024 14:30
-
-
Save konarskis/b0b822a3b82f5b5b278ed8ad94c59611 to your computer and use it in GitHub Desktop.
Deploy and Secure a Bastion Host with AWS CDK and Typescript
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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