Project URL: roadmap.sh/projects/bastion-host
This project implements a secure bastion host architecture in AWS, providing a secure way to access private infrastructure. The implementation includes VPC setup, security configurations, and SSH hardening measures.
graph LR
Internet((Internet))
subgraph VPC [VPC 10.0.0.0/16]
subgraph Public [Public Subnet 10.0.1.0/24]
BH[Bastion Host\nt2.micro]
end
subgraph Private [Private Subnet 10.0.2.0/24]
PS[Private Server\nt2.micro]
end
IGW[Internet Gateway]
end
Internet --> IGW
IGW --> BH
BH --> PS
style VPC fill:#f5f5f5,stroke:#333,stroke-width:2px
style Public fill:#e1f5fe,stroke:#333,stroke-width:1px
style Private fill:#ffebee,stroke:#333,stroke-width:1px
style BH fill:#b3e5fc,stroke:#333,stroke-width:1px
style PS fill:#ffcdd2,stroke:#333,stroke-width:1px
style IGW fill:#e8f5e9,stroke:#333,stroke-width:1px
-
VPC (10.0.0.0/16)
- Public Subnet: 10.0.1.0/24
- Private Subnet: 10.0.2.0/24
- Internet Gateway
- Route Tables for public and private subnets
-
EC2 Instances
- Bastion Host (Public)
- Instance Type: t2.micro
- AMI: Amazon Linux 2
- Private Server
- Instance Type: t2.micro
- AMI: Amazon Linux 2
- Bastion Host (Public)
-
Security Groups
Bastion Security Group: - Inbound: SSH (22) from 0.0.0.0/0 - Outbound: All traffic Private Server Security Group: - Inbound: SSH (22) from Bastion SG - Outbound: All traffic
# Create VPC
aws ec2 create-vpc --cidr-block 10.0.0.0/16
# Create Subnets
aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.1.0/24
aws ec2 create-subnet --vpc-id $VPC_ID --cidr-block 10.0.2.0/24
# Create and Attach Internet Gateway
aws ec2 create-internet-gateway
aws ec2 attach-internet-gateway --vpc-id $VPC_ID --internet-gateway-id $IGW_ID
# Bastion Security Group
aws ec2 create-security-group --group-name bastion-sg \
--description "Security group for bastion host" --vpc-id $VPC_ID
# Private Server Security Group
aws ec2 create-security-group --group-name private-sg \
--description "Security group for private instance" --vpc-id $VPC_ID
# Launch Bastion Host
aws ec2 run-instances --image-id $AMI_ID --instance-type t2.micro \
--key-name your-key-name --security-group-ids $BASTION_SG_ID \
--subnet-id $PUBLIC_SUBNET_ID
# Launch Private Server
aws ec2 run-instances --image-id $AMI_ID --instance-type t2.micro \
--key-name your-key-name --security-group-ids $PRIVATE_SG_ID \
--subnet-id $PRIVATE_SUBNET_ID
Example SSH config:
Host bastion
HostName <bastion-public-ip>
User ec2-user
IdentityFile ~/.ssh/your-key.pem
Host private-server
HostName <private-server-private-ip>
User ec2-user
ProxyJump bastion
IdentityFile ~/.ssh/your-key.pem
terraform/
├── main.tf # Main Terraform configuration
├── variables.tf # Variable definitions
├── outputs.tf # Output definitions
├── terraform.tfvars.example # Example variables file
└── scripts/
└── bastion_setup.sh # Bastion host setup script
- Terraform installed (v1.0.0 or newer)
- AWS credentials configured
- SSH key pair created in AWS
-
Clone this repository
-
Navigate to the terraform directory:
cd terraform
-
Copy and configure variables:
cp terraform.tfvars.example terraform.tfvars # Edit terraform.tfvars with your values
-
Initialize Terraform:
terraform init
-
Review the execution plan:
terraform plan
-
Apply the configuration:
terraform apply
- VPC with public and private subnets
- Internet Gateway
- Route tables
- Security groups for bastion and private instances
- EC2 instances (bastion host and private server)
- Automatic security configurations:
- fail2ban installation and configuration
- SSH hardening
- Security group rules
You can customize the deployment by modifying:
terraform.tfvars
: Change region, CIDR blocks, instance typesscripts/bastion_setup.sh
: Modify security configurationsmain.tf
: Adjust resource configurations
To destroy all created resources:
terraform destroy
When you're done with the bastion host setup, follow these steps to clean up all AWS resources in the correct order to avoid dependency conflicts:
# Terminate the bastion host
aws ec2 terminate-instances --instance-ids <bastion-instance-id>
# Wait for instance to fully terminate
aws ec2 describe-instances --instance-ids <bastion-instance-id>
# Check that state is "terminated"
Delete components in this specific order to handle dependencies:
- Detach and Delete Internet Gateway:
# Detach from VPC
aws ec2 detach-internet-gateway \
--internet-gateway-id <igw-id> \
--vpc-id <vpc-id>
# Delete the internet gateway
aws ec2 delete-internet-gateway --internet-gateway-id <igw-id>
- Delete Security Groups:
# Delete private server security group
aws ec2 delete-security-group --group-id <private-sg-id>
# Delete bastion host security group
aws ec2 delete-security-group --group-id <bastion-sg-id>
- Delete Subnets:
# Delete public subnet
aws ec2 delete-subnet --subnet-id <public-subnet-id>
# Delete private subnet
aws ec2 delete-subnet --subnet-id <private-subnet-id>
- Delete Route Tables:
# Delete custom route tables
aws ec2 delete-route-table --route-table-id <route-table-id>
- Delete VPC:
# Finally, delete the VPC
aws ec2 delete-vpc --vpc-id <vpc-id>
After deletion, verify that all resources are removed:
# Check VPC
aws ec2 describe-vpcs --vpc-id <vpc-id>
# Should return: InvalidVpcID.NotFound
# Check Security Groups
aws ec2 describe-security-groups --group-ids <sg-id>
# Should return: InvalidGroup.NotFound
# Check Subnets
aws ec2 describe-subnets --subnet-ids <subnet-id>
# Should return: InvalidSubnetID.NotFound
# Check Internet Gateway
aws ec2 describe-internet-gateways --internet-gateway-ids <igw-id>
# Should return: InvalidInternetGatewayID.NotFound
-
Dependency Violations: If you receive a dependency violation error, it means you're trying to delete a resource that other resources still depend on. Follow the order specified above.
-
Resource Still in Use: Sometimes AWS needs a few minutes to fully process the termination of resources. Wait a few minutes and try again.
-
Main Route Table: The main route table associated with the VPC will be automatically deleted when the VPC is deleted.
- Terminating resources stops any ongoing charges
- Check your AWS billing dashboard to confirm no unexpected charges
- Some resources like EBS volumes might need separate deletion if not set to auto-delete
$ aws ec2 describe-vpcs
{
"Vpcs": [
{
"CidrBlock": "10.0.0.0/16",
"State": "available",
"VpcId": "vpc-xxxxx",
"InstanceTenancy": "default",
"CidrBlockAssociationSet": [
{
"CidrBlock": "10.0.0.0/16",
"CidrBlockState": {
"State": "associated"
}
}
],
"Tags": [
{
"Key": "Name",
"Value": "bastion-vpc"
}
]
}
]
}
$ aws ec2 describe-security-groups
{
"SecurityGroups": [
{
"GroupName": "bastion-sg",
"Description": "Security group for bastion host",
"IpPermissions": [
{
"FromPort": 22,
"IpProtocol": "tcp",
"IpRanges": [
{
"CidrIp": "0.0.0.0/0"
}
],
"ToPort": 22
}
]
},
{
"GroupName": "private-sg",
"Description": "Security group for private instance",
"IpPermissions": [
{
"FromPort": 22,
"IpProtocol": "tcp",
"UserIdGroupPairs": [
{
"GroupId": "<bastion-sg-id>"
}
],
"ToPort": 22
}
]
}
]
}
$ aws ec2 describe-instances --query "Reservations[*].Instances[*].[Tags[?Key=='Name'].Value|[0],State.Name]" --output table
---------------------------------
| DescribeInstances |
+---------------+---------------+
| bastion-host | running |
| private-server| running |
+---------------+---------------+
$ cat ~/.ssh/config
Host bastion
HostName <bastion-public-ip>
User ec2-user
IdentityFile ~/.ssh/your-key.pem
Host private-server
HostName <private-ip>
User ec2-user
ProxyJump bastion
IdentityFile ~/.ssh/your-key.pem
$ ssh bastion "systemctl status fail2ban"
● fail2ban.service - Fail2Ban Service
Loaded: loaded
Active: active (running)
$ ssh bastion "sudo sshd -T | grep -E 'permitrootlogin|passwordauthentication|x11forwarding'"
permitrootlogin no
passwordauthentication no
x11forwarding no
$ ssh bastion "echo 'Connection to bastion host successful!'"
Connection to bastion host successful!
$ ssh private-server "echo 'Connection to private server successful!'"
Connection to private server successful!
-
Network Security
- Private server accessible only through bastion host
- Public subnet with Internet Gateway for bastion
- Private subnet for secure server
-
Access Control
- Key-based authentication only
- fail2ban for brute force protection
- No root login allowed
- SSH hardening configurations
-
Monitoring
- SSH session logging
- fail2ban logs for access attempts
-
SSH Access Issues
- Problem: Lost SSH access after implementing strict security measures
- Solution Attempted: Created temporary security group for recovery
- Status: Access recovery in progress
-
Security Implementation
- MFA implementation pending
- iptables configuration pending
- Implement Multi-Factor Authentication (MFA)
- Configure iptables for granular traffic filtering
- Set up CloudWatch monitoring
- Implement automatic security patches
- Add backup and disaster recovery procedures
- Core infrastructure: Complete
- Basic security measures: Complete
- Advanced security (MFA, iptables): Pending
- Documentation: Complete