---
title: "How To Setup Bastion Host on AWS using AWS CLI"
description: "In this post, we will learn the best practices of setting up a Bastion Host on AWS using the AWS CLI for secure and remote access to EC2 instances within a Virtual Private Cloud (VPC). We will guide you through creating a VPC, subnets, internet gateway, and configuring the Bastion Host with the appropriate permissions. This post is intended for those who are familiar with AWS and have some basic knowledge of networking and SSH."
author: "Mohammad Abu Mattar"
canonical: https://mkabumattar.com/blog/post/how-to-setup-bastion-host-on-aws-using-aws-cli
---

# How To Setup Bastion Host on AWS using AWS CLI

## Introduction

Security is the top priority for any infrastructure and application, and that's why a Bastion host is a must-have in your infrastructure if you want to secure your remote connections. A Bastion host is a special-purpose computer on a network specifically designed and configured to withstand attacks. In this post, we will show you how to set up a Bastion host on AWS using the AWS CLI. We will create a Virtual Private Cloud (VPC) and subnets, create an Internet Gateway and configure the Bastion host with the appropriate permissions to access our EC2 instances. By the end of this post, you will have a secure and easy way to remotely access your EC2 instances.

## Prerequisites

Before starting, make sure that you have the following:

- AWS CLI installed and configured on your local machine. You can follow the instructions on [Installing the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) to install and configure it.
- An IAM user with the following permissions:
  - AmazonVPCFullAccess
  - AmazonEC2FullAccess
- Basic knowledge of networking and SSH

To create an IAM user, follow the instructions on [Creating an IAM User](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_users_create.html)

You will also need to have the AWS CLI configured with your access keys for the IAM user you created above. This can be done by running `aws configure` in the command line and providing your access key and secret key.

## Create VPC

In this section, we will create a Virtual Private Cloud (VPC) and its resources such as subnets, internet gateways, etc. This VPC will be the foundation for our Bastion Host, providing a secure and isolated network environment.

### Step 1: Create VPC

First, we need to create a VPC, run the following command:

```shell
AWS_VPC=$(aws ec2 create-vpc \
  --cidr-block 10.0.0.0/16 \
  --query 'Vpc.VpcId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_VPC \
  --tags Key=Name,Value=vpc
```

The above command will create a VPC with the CIDR block of `10.0.0.0/16` and create a name tag for it. The VpcId of the created VPC will be stored in the variable `$AWS_VPC` for future reference.

### Step 2: Modify your VPC and enable DNS hostname and DNS support

In this step, we will enable both the DNS hostname and DNS support on our VPC. Enabling these features will allow our instances to resolve DNS hostnames and domain names. By default, these features are disabled when you create a VPC.

```shell
aws ec2 modify-vpc-attribute \
  --vpc-id $AWS_VPC \
  --enable-dns-hostnames "{\"Value\":true}"

aws ec2 modify-vpc-attribute \
  --vpc-id $AWS_VPC \
  --enable-dns-support "{\"Value\":true}"
```

The above commands will enable the DNS hostname and DNS support for our VPC. `--vpc-id` `$AWS_VPC` this flag tells the command on which vpc you want to enable these features, where the `$AWS_VPC` is a variable that we set in the previous step when we created the VPC.

### Step 3: Create a Public and a Private subnet

In this step, we will create two subnets: one public and one private. A public subnet is a subnet that's connected to the internet through an internet gateway, whereas a private subnet is a subnet that's isolated from the internet and can only access the internet through a NAT gateway or VPN connection. In this case, we will connect our Bastion Host to the public subnet and the EC2 instances that we want to access remotely to the private subnet.

```shell
AWS_PUBLIC_SUBNET=$(aws ec2 create-subnet \
  --vpc-id $AWS_VPC \
  --cidr-block 10.0.0.0/24 \
  --query 'Subnet.SubnetId' \
  --output text)

AWS_PRIVATE_SUBNET=$(aws ec2 create-subnet \
  --vpc-id $AWS_VPC \
  --cidr-block 10.0.16.0/24 \
  --query 'Subnet.SubnetId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_PUBLIC_SUBNET \
  --tags Key=Name,Value=public-subnet

aws ec2 create-tags \
  --resources $AWS_PRIVATE_SUBNET \
  --tags Key=Name,Value=private-subnet
```

The above command will create two subnets, one with a CIDR block of `10.0.0.0/24` (public) and one with a CIDR block of `10.0.16.0/24` (private) and will associate them with the `$AWS_VPC` VPC we created earlier. Also, it will add names to the subnets for better organization and management. The subnet IDs are stored in the variables `$AWS_PUBLIC_SUBNET` and `$AWS_PRIVATE_SUBNET` for future reference.

### Step 4: Enable Auto-assign Public IP on the Public Subnet

In this step, we will enable Auto-assign Public IP on the public subnet, which will allow instances launched in this subnet to automatically receive a public IP address. This is necessary for instances that need direct internet access.

```shell
aws ec2 modify-subnet-attribute \
  --subnet-id $AWS_PUBLIC_SUBNET \
  --map-public-ip-on-launch
```

The above command will enable the Auto-assign Public IP feature for the public subnet identified by the `$AWS_PUBLIC_SUBNET` variable. This feature tells the AWS to automatically assign a public IP to the instances launched in this subnet.

You can also assign public IP addresses to instances by using Elastic IP addresses, a feature that lets you allocate an IP address to your AWS account and then associate it with an instance. Elastic IP addresses can be moved between instances or released when no longer needed, avoiding the extra charges that come with an automatically assigned public IP address.

### Step 5: Create an Internet Gateway

In this step, we will create an Internet Gateway and associate it with our VPC. An Internet Gateway is a VPC component that allows communication between instances in our VPC and the internet. This is a necessary step for our public subnet instances to have internet access.

```shell
AWS_INTERNET_GATEWAY=$(aws ec2 create-internet-gateway \
  --query 'InternetGateway.InternetGatewayId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_INTERNET_GATEWAY \
  --tags Key=Name,Value=internet-gateway

aws ec2 attach-internet-gateway \
  --vpc-id $AWS_VPC \
  --internet-gateway-id $AWS_INTERNET_GATEWAY
```

The above command will create an Internet Gateway, create a name tag for it and attach it to the VPC identified by the `$AWS_VPC` variable. The Internet Gateway ID is stored in the variable `$AWS_INTERNET_GATEWAY` for future reference.

Internet Gateways are stateful: if a request initiated from your VPC is sent to an Internet Gateway, the response routes back to the source. A Virtual Private Gateway, by contrast, is stateless. It routes traffic but does not hold connection state. Choose the gateway type that matches your use case.

### Step 6: Create an Elastic IP

In this step, we will create an Elastic IP, which is a static public IPv4 address that can be allocated to your AWS account and then associated with an instance. Having an Elastic IP address allows you to mask the failure of an instance by rapidly remapping the address to another instance. This is useful when instances fail or if you want to change instances while keeping the same IP address.

```shell
AWS_ELASTIC_IP=$(aws ec2 allocate-address \
  --domain vpc \
  --query 'AllocationId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_ELASTIC_IP \
  --tags Key=Name,Value=elastic-ip
```

The above command will allocate a new Elastic IP for your AWS account, and create a name tag for it. The Elastic IP address is stored in the variable `$AWS_ELASTIC_IP` for future reference.

Elastic IP addresses are charged for hourly usage when not associated with a running instance, or when associated with a stopped instance or an unattached network interface. If you're not using it, release it to avoid charges.

### Step 7: Create a NAT Gateway

In this step, we will create a NAT Gateway and associate it with our VPC. A NAT Gateway allows instances in a private subnet to access the internet without exposing their private IP address. It is a highly available, managed service that allows outbound internet traffic from instances in a private subnet in your virtual private cloud (VPC). This is a necessary step for our private subnet instances to have internet access.

```shell
AWS_NAT_GATEWAY=$(aws ec2 create-nat-gateway \
  --subnet-id $AWS_PUBLIC_SUBNET \
  --allocation-id $AWS_ELASTIC_IP \
  --query 'NatGateway.NatGatewayId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_NAT_GATEWAY \
  --tags Key=Name,Value=nat-gateway
```

The above command will create a NAT Gateway, associated it with the public subnet identified by the `$AWS_PUBLIC_SUBNET` variable, and using the Elastic IP that we allocated previously identified by `$AWS_ELASTIC_IP`, also, it will create a name tag for it. The NAT Gateway ID is stored in the variable `$AWS_NAT_GATEWAY` for future reference.

The NAT Gateway requires a pre-allocated Elastic IP to be created, and that Elastic IP is consumed by the NAT Gateway and unavailable for other uses. The NAT Gateway is also a managed service, so AWS handles its maintenance and availability.

### Step 8: Create a Public and a Private Route Table

In this step, we will create two route tables: one public and one private. A route table contains a set of rules, called routes, that are used to determine where network traffic is directed. Separate route tables for the public and private subnets keep traffic routed correctly.

```shell
AWS_PUBLIC_ROUTE_TABLE=$(aws ec2 create-route-table \
  --vpc-id $AWS_VPC \
  --query 'RouteTable.RouteTableId' \
  --output text)

AWS_PRIVATE_ROUTE_TABLE=$(aws ec2 create-route-table \
  --vpc-id $AWS_VPC \
  --query 'RouteTable.RouteTableId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_PUBLIC_ROUTE_TABLE \
  --tags Key=Name,Value=public-route-table

aws ec2 create-tags \
  --resources $AWS_PRIVATE_ROUTE_TABLE \
  --tags Key=Name,Value=private-route-table
```

The above command will create two route tables associated with the VPC identified by the $AWS_VPC variable and create names for them "public-route-table" and "private-route-table" and will store the route table IDs in the variables `$AWS_PUBLIC_ROUTE_TABLE`and`$AWS_PRIVATE_ROUTE_TABLE` respectively for future reference.

### Step 9: Create a Route in the Public Route Table for Internet Gateway

In this step, we will create a route in the public route table that directs all traffic to the Internet Gateway. This is necessary for instances in the public subnet to have internet access.

```shell
aws ec2 create-route \
  --route-table-id $AWS_PUBLIC_ROUTE_TABLE \
  --destination-cidr-block 0.0.0.0/0 \
  --gateway-id $AWS_INTERNET_GATEWAY
```

The above command will create a route in the public route table that directs all traffic (destination CIDR block 0.0.0.0/0) to the Internet Gateway identified by the `$AWS_INTERNET_GATEWAY` variable. This will allow instances in the public subnet to access the internet.

When you create a route in a route table, the route propagates to all associated subnets, so make sure you're creating it in the correct route table.

### Step 10: Create a Route in the Private Route Table for NAT Gateway

In this step, we will create a route in the private route table that directs all traffic to the NAT Gateway. This is necessary for instances in the private subnet to have internet access without exposing their private IP addresses.

```shell
aws ec2 create-route \
  --route-table-id $AWS_PRIVATE_ROUTE_TABLE \
  --destination-cidr-block 0.0.0.0/0 \
  --nat-gateway-id $AWS_NAT_GATEWAY
```

The above command will create a route in the private route table that directs all traffic (destination CIDR block 0.0.0.0/0) to the NAT Gateway identified by the `$AWS_NAT_GATEWAY` variable. This will allow instances in the private subnet to access the internet without exposing their private IP addresses.

Instances in the private subnet should not be able to initiate direct internet access. This protects them from malicious internet traffic and reduces the risk of accidental data leaks.

### Step 11: Associate the Subnets with the Route Tables

In this step, we will associate the public and private subnets with the corresponding public and private route tables. This routes traffic correctly.

```shell
aws ec2 associate-route-table \
  --route-table-id $AWS_PUBLIC_ROUTE_TABLE \
  --subnet-id $AWS_PUBLIC_SUBNET

aws ec2 associate-route-table \
  --route-table-id $AWS_PRIVATE_ROUTE_TABLE \
  --subnet-id $AWS_PRIVATE_SUBNET
```

The above command will associate the public subnet identified by the `$AWS_PUBLIC_SUBNET` variable with the public route table identified by the `$AWS_PUBLIC_ROUTE_TABLE` variable, and the private subnet identified by the `$AWS_PRIVATE_SUBNET` variable with the private route table identified by the `$AWS_PRIVATE_ROUTE_TABLE` variable.

This directs traffic to the correct destination based on the subnet it originates from: traffic from the public subnet goes to the Internet Gateway, and traffic from the private subnet goes to the NAT Gateway. Instances in the public subnet get internet access, and instances in the private subnet get internet access without exposing their private IP addresses.

### Step 12: Create Security Groups

In this step, we will create security groups for the bastion host and the instances in the private subnet. Security groups act as a virtual firewall for your instances, controlling inbound and outbound traffic.

```shell
AWS_BASTION_SECURITY_GROUP=$(aws ec2 create-security-group \
  --group-name bastion-security-group \
  --description "Security group for the bastion host" \
  --vpc-id $AWS_VPC \
  --query 'GroupId' \
  --output text)

AWS_PRIVATE_SECURITY_GROUP=$(aws ec2 create-security-group \
  --group-name private-security-group \
  --description "Security group for the private instances" \
  --vpc-id $AWS_VPC \
  --query 'GroupId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_BASTION_SECURITY_GROUP \
  --tags Key=Name,Value=bastion-security-group

aws ec2 create-tags \
  --resources $AWS_PRIVATE_SECURITY_GROUP \
  --tags Key=Name,Value=private-security-group

aws ec2 authorize-security-group-ingress \
  --group-id $AWS_BASTION_SECURITY_GROUP \
  --protocol tcp \
  --port 22 \
  --cidr 0.0.0.0/0 \
  --output text

aws ec2 authorize-security-group-ingress \
  --group-id $AWS_PRIVATE_SECURITY_GROUP \
  --protocol tcp \
  --port 22 \
  --source-group $AWS_BASTION_SECURITY_GROUP \
  --output text
```

The above command will create two security groups: one for the bastion host, and one for the instances in the private subnet.

The security group for the bastion host is named "bastion-security-group", and it is associated with the VPC identified by the `$AWS_VPC` variable. It allows inbound traffic on port 22 (SSH) from any IP address (CIDR block 0.0.0.0/0), which is what lets you connect to instances via the bastion host.

The security group for the instances in the private subnet is named "private-security-group", and it is also associated with the VPC identified by the `$AWS_VPC` variable. It allows inbound SSH traffic on port 22 from the security group for the bastion host only. This way, instances in the private subnet can only be accessed via the bastion host and only from instances that are within the security group for the bastion host.

These security groups help make sure only authorized traffic can reach your instances, and that traffic from your instances to the internet is properly restricted.

## Create two EC2 instances

In this step, we will create two EC2 instances: one for the bastion host and one for the private subnet. Before creating these instances, you need to create an ssh key pair that will be used to access the instances via ssh.

### Step 1: Create a Key Pair

To connect to the instances via SSH, you will need to create a Key Pair that will be used to authenticate the connection. To create a Key Pair, you can use the AWS Management Console, AWS CLI or SDKs.

```shell
AWS_KEY_PAIR=aws-key-pair
aws ec2 create-key-pair \
  --key-name $AWS_KEY_PAIR \
  --query 'KeyMaterial' \
  --output text > $AWS_KEY_PAIR.pem
chmod 400 $AWS_KEY_PAIR.pem
```

The above command will create a new key pair named `aws-key-pair` and the private key will be saved in the file `aws-key-pair.pem`. Make sure to keep this file safe and secure, as it allows you to connect to the instances. The `chmod 400` command will restrict the permissions on the key pair file so that it is only readable by the owner.

Also, you may want to consider using environment variables for the key pair name, this way you can easily change the key pair name without having to search and replace it in the script.

### Step 2: Get the latest AMI ID for Amazon Linux 2

Before launching an EC2 instance, we need to know the Amazon Machine Image (AMI) ID for the specific Operating System we want to use, in this case Amazon Linux 2. The AMI ID is used to specify the image for the instance when it's being created. Instead of hard coding the AMI ID, it's better to programmatically find the latest available AMI ID for the specific Operating System so the script always uses the latest version of the AMI.

```shell
# Get the latest AMI ID for Amazon Linux 2
AWS_AMI=$(aws ec2 describe-images \
  --owners 'amazon' \
  --filters 'Name=name,Values=amzn2-ami-hvm-2.0.*' \
  'Name=state,Values=available' \
  --query 'sort_by(Images, &CreationDate)[-1].[ImageId]' \
  --output 'text')
```

The above command will get the latest available Amazon Linux 2 AMI ID based on the filters provided. The `--owners` option specifies that the AMI should be owned by Amazon. The `--filters` option is used to filter the images returned by the describe-images command. The `Name=name,Values=amzn2-ami-hvm-2.0.*` filter will return only the images whose name starts with `amzn2-ami-hvm-2.0`. The `Name=state,Values=available` filter will return only the images that are in the available state.

### Step 3: Create a Bastion Host

In this step, we will use the `run-instances` command to launch an EC2 instance for our Bastion host. The Bastion host will be used as a jump server to securely access the instances in the private subnet.

```shell
AWS_BASTION_HOST=$(aws ec2 run-instances \
  --image-id $AWS_AMI \
  --count 1 \
  --instance-type t2.micro \
  --key-name $AWS_KEY_PAIR \
  --security-group-ids $AWS_BASTION_SECURITY_GROUP \
  --subnet-id $AWS_PUBLIC_SUBNET \
  --associate-public-ip-address \
  --query 'Instances[0].InstanceId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_BASTION_HOST \
  --tags Key=Name,Value=bastion-host
```

The above command will create a single EC2 instance in the public subnet with the provided AMI ID and Key Pair, will be associated with the security group that we created earlier for the Bastion host, and it will be assigned a public IP address so that it can be accessed over the internet.

The tag specification was added to assign the Name value to the created instances.

Consider specifying the availability zone so the instances are created in one that meets your requirements (for example, one with enough capacity for the instances).

### Step 4: Create a Private Host

In this step, we will use the run-instances command to launch an EC2 instance for our Private host. The private host will be running in a private subnet and it will not have a public IP address.

```shell
AWS_PRIVATE_HOST=$(aws ec2 run-instances \
  --image-id $AWS_AMI \
  --count 1 \
  --instance-type t2.micro \
  --key-name $AWS_KEY_PAIR \
  --security-group-ids $AWS_PRIVATE_SECURITY_GROUP \
  --subnet-id $AWS_PRIVATE_SUBNET \
  --query 'Instances[0].InstanceId' \
  --output text)

aws ec2 create-tags \
  --resources $AWS_PRIVATE_HOST \
  --tags Key=Name,Value=private-host
```

The above command will create a single EC2 instance in the private subnet with the provided AMI ID and Key Pair, will be associated with the security group that we created earlier for the private host, and it will not be assigned a public IP address.

The bastion host will act as the gateway for reaching the instances in the private subnet over SSH.

Consider specifying the availability zone so the instances are created in one that meets your requirements (for example, one with enough capacity for the instances).

## Connect to the private host

In this section, we will show you how to connect to the Private host using the Bastion host as a jump server.

### Step 1: Get the Public IP Address of the Bastion Host

To connect to the private host, we first need to know the public IP address of the Bastion host. We can use the `describe-instances` command to get the public IP address of the Bastion host.

```shell
AWS_BASTION_HOST_PUBLIC_IP=$(aws ec2 describe-instances \
  --instance-ids $AWS_BASTION_HOST \
  --query 'Reservations[0].Instances[0].PublicIpAddress' \
  --output text)
```

The above command will get the public IP address of the Bastion host based on the instance ID.

Once you have the public IP address of the Bastion host, you can use it to establish an SSH connection to the Bastion host.

### Step 2: Connect to the Bastion Host

To connect to the Bastion Host, run the following command:

```shell
ssh -i $AWS_KEY_PAIR.pem ec2-user@$AWS_BASTION_HOST_PUBLIC_IP
```

The above command will connect to the Bastion host using the key pair that we created earlier, and the public IP address of the Bastion host.

![Connect to the Bastion Host](/assets/blog/0034-how-to-setup-bastion-host-on-aws-using-aws-cli/connect-to-the-bastion-host.png)

### Step 3: Get the Private IP Address of the Private Host

To connect to the private host via the Bastion host, we will need to know the private IP address of the private host. We can use the describe-instances command to get the private IP address of the private host.

```shell
AWS_PRIVATE_HOST_PRIVATE_IP=$(aws ec2 describe-instances \
  --instance-ids $AWS_PRIVATE_HOST \
  --query 'Reservations[0].Instances[0].PrivateIpAddress' \
  --output text)
```

The above command will get the private IP address of the private host based on the instance ID. The output is text containing the private IP address, which you can use in the next step.

You can also use the Name tag we created earlier to retrieve the private IP address with the `--filters` option, which gets the private IP address of the private host without needing the instance ID.

```shell
AWS_PRIVATE_HOST_PRIVATE_IP=$(aws ec2 describe-instances \
  --filters "Name=tag:Name,Values=private-host" \
  --query 'Reservations[0].Instances[0].PrivateIpAddress' \
  --output text)
```

### Step 4: Connect to the Private Host

Once you have the private IP address of the private host, you can use the following command to connect to the private host via the Bastion host:

```shell
# add the private key
vi ~/.ssh/private-key.pem

# change the permission of the private key
chmod 400 ~/.ssh/private-key.pem

# connect to the private host
ssh -i ~/.ssh/private-key.pem ec2-user@$AWS_PRIVATE_HOST_PRIVATE_IP
```

The above command will connect to the private host using the private key that we created earlier, and the private IP address of the private host.

### Step 5: Check the Internet Connectivity

Now that you are connected to the private host, you can check if the host has internet connectivity by pinging a public IP or URL:

```shell
ping -c 4 google.com
```

The above command will send 4 ICMP echo requests to the IP address of Google's website, and Google will send back 4 ICMP echo replies if the private host can reach the internet. This verifies that the NAT gateway and route tables are configured correctly.

Alternatively, you can also check internet connectivity by using the curl command to download a webpage:

![Internet Connectivity](/assets/blog/0034-how-to-setup-bastion-host-on-aws-using-aws-cli/internet-connectivity.png)

```shell
curl -s https://www.mkabumattar.com | head -n 10
```

This will download the website's source code and will return it to the terminal, and check if the response is received from the website. If the private host has internet connectivity, it will show the webpage's source code.

If you run these commands from the bastion host, you might not face the same restrictions as the private host. In that case, run the commands from the private host instead, or test against a site that's specifically blocked for your private network.

![Internet Connectivity](/assets/blog/0034-how-to-setup-bastion-host-on-aws-using-aws-cli/internet-connectivity-2.png)

## Conclusion

In this tutorial, we walked through the process of creating a Bastion host on AWS using the AWS CLI. We created a VPC, subnets, an Internet gateway, a NAT gateway, route tables, security groups, and two EC2 instances. We then connected to the private host via the Bastion host and verified internet connectivity on the private host.

This is just a basic setup. You might want to strengthen security with an authentication method other than the key pair, such as IAM roles and SSM Session Manager, and add extra logging, auditing, and monitoring.

Thanks for reading this tutorial. If you have any questions or feedback, leave a comment below.

## Cleanup

When you are finished using the resources created in this tutorial, you should clean them up to avoid incurring unnecessary charges.

You can use the AWS Management Console, or the AWS CLI to delete the resources.

  Be careful when running the following commands, as they will delete the
  resources created in this tutorial

### Delete the EC2 Instances

```shell
aws ec2 terminate-instances --instance-ids $AWS_BASTION_HOST $AWS_PRIVATE_HOST
```

This command will delete the two EC2 instances created in this tutorial.

### Detach and Delete Internet Gateway

```shell
aws ec2 detach-internet-gateway --internet-gateway-id $AWS_INTERNET_GATEWAY --vpc-id $AWS_VPC
aws ec2 delete-internet-gateway --internet-gateway-id $AWS_INTERNET_GATEWAY
```

This command will detach and delete the Internet gateway created in this tutorial.

### Delete Route Tables

```shell
aws ec2 delete-route-table --route-table-id $AWS_PUBLIC_ROUTE_TABLE
aws ec2 delete-route-table --route-table-id $AWS_PRIVATE_ROUTE_TABLE
```

This command will delete the route tables created in this tutorial.

### Delete NAT Gateway

```shell
# Wait for NAT Gateway to be deleted if it's in 'deleting' state
aws ec2 wait nat-gateway-deleted --nat-gateway-ids $AWS_NAT_GATEWAY
aws ec2 delete-nat-gateway --nat-gateway-id $AWS_NAT_GATEWAY
```

This command will delete the NAT gateway created in this tutorial.

### Delete Elastic IP

```shell
# Ensure $AWS_ELASTIC_IP holds the AllocationId for release-address
# If $AWS_ELASTIC_IP was set to the PublicIp string, you'd need the AllocationId
# Assuming $AWS_ELASTIC_IP was set using:
# AWS_ELASTIC_IP_ALLOC_ID=$(aws ec2 allocate-address --domain vpc --query 'AllocationId' --output text)
# Then use:
# aws ec2 release-address --allocation-id $AWS_ELASTIC_IP_ALLOC_ID
# If $AWS_ELASTIC_IP was set to the PublicIp string, this command might not work as expected.
# For the script in the article, $AWS_ELASTIC_IP stores AllocationId, so the original command is fine.
aws ec2 release-address --allocation-id $AWS_ELASTIC_IP
```

### Delete Subnets

```shell
aws ec2 delete-subnet --subnet-id $AWS_PUBLIC_SUBNET
aws ec2 delete-subnet --subnet-id $AWS_PRIVATE_SUBNET
```

This command will delete the subnets created in this tutorial.

### Delete Security Groups

```shell
aws ec2 delete-security-group --group-id $AWS_BASTION_SECURITY_GROUP
aws ec2 delete-security-group --group-id $AWS_PRIVATE_SECURITY_GROUP
```

This command will delete the security groups created in this tutorial.

### Delete VPC

```shell
aws ec2 delete-vpc --vpc-id $AWS_VPC
```

### Delete Key Pair

```shell
aws ec2 delete-key-pair --key-name $AWS_KEY_PAIR
rm -f $AWS_KEY_PAIR.pem
```

This command will delete the key pair created in this tutorial and also the key file from the local system.

Some resources take a while to fully delete, and some depend on others, so you may need to run the deletion commands multiple times and in a specific order.

Also, you should check if there are any other resources that were created outside of the scope of this tutorial but are still associated with the VPC, subnet, security groups, and key pairs, and delete them as well.

## References

- [AWS Command Line Interface (CLI) User Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-welcome.html)
- [AWS Identity and Access Management (IAM) User Guide](https://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html)
- [Amazon Virtual Private Cloud (VPC) User Guide](https://docs.aws.amazon.com/vpc/latest/userguide/what-is-amazon-vpc.html)
- [Amazon EC2 User Guide for Linux Instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/concepts.html)
- [Security Groups for your VPC - Amazon VPC](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_SecurityGroups.html)
- [NAT Gateways - Amazon VPC](https://docs.aws.amazon.com/vpc/latest/userguide/vpc-nat-gateway.html)
- [Internet Gateways - Amazon VPC](https://docs.aws.amazon.com/vpc/latest/userguide/VPC_Internet_Gateway.html)
- [AWS Key Pairs and Amazon EC2 Instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-key-pairs.html)
- [Linux Bastion Hosts on AWS (Quick Start Reference Deployment)](https://aws.amazon.com/quickstart/architecture/linux-bastion/)
- [What is an Elastic IP address? - Amazon EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html)
- [Connect to your Linux instance using SSH - Amazon EC2](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AccessingInstancesLinux.html)
- [AWS CLI Command Reference - ec2 create-vpc](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/create-vpc.html)
- [AWS CLI Command Reference - ec2 run-instances](https://awscli.amazonaws.com/v2/documentation/api/latest/reference/ec2/run-instances.html)
- [AWS Well-Architected Framework - Security Pillar](https://docs.aws.amazon.com/wellarchitected/latest/security-pillar/welcome.html)
