SSH Gateway Server on VPC
June 30, 2013My latest project at Gobbler has been the trasition of our infrastrcture from “EC2 Classic” to Virtual Private Cloud (VPC).
The AWS docs do a pretty good job explaining VPC concepts, and the web console does a great job launching a basic VPC setup for you. But, for me, I am best able to understand a concept when I see the code, step-by-step.
All of my examples will be in ruby using the aws-sdk
gem, but the same
results can be achieved using any of their other SDKs.
Assumptions
- You have a basic undertsanding of AWS
- You already have an active AWS account
- You have a keypair called
key-lab
located at~/.ec2/key-lab.pem
(rename where appropriate) - All commands are probably best done in
irb
, this way you can inspect objects as you go along. Make sure to look at the aws-sdk docs.
Creating the VPC
First, let’s set up our AWS credentials
require 'aws-sdk'
AWS.config(access_key_id: "....", secret_access_key: "....")
$ec2 = AWS::EC2.new
Next, let’s create a basic VPC (this will also create a route table entry)
$vpc = $ec2.vpcs.create("10.0.0.0/16")
Create an Internet Gateway, this allows servers to have a way to get back to the internet.
gateway = $ec2.internet_gateways.create
gateway.attach($vpc)
Add a default route
public_route = $vpc.route_tables.first
public_route.create_route("0.0.0.0/0", internet_gateway: gateway.id)
Now it’s time to create 2 subnets, one public, one private
public_subnet = $vpc.subnets.create("10.0.0.0/24", availability_zone: "us-east-1d")
private_subnet = $vpc.subnets.create("10.0.1.0/24", availability_zone: "us-east-1d")
We need to create 2 security groups, one for public access, and one for private.
public_security_group = $ec2.security_groups.create("public", vpc_id: $vpc.id)
private_security_group = $ec2.security_groups.create("private", vpc_id: $vpc.id)
The public security group should be allowed to talk to the world on tcp/22 (ssh) and the private one should only be allowed to talk to members of the public security group over tcp/22
public_security_group.authorize_ingress(:tcp, 22, "0.0.0.0/0")
private_security_group.authorize_ingress(:tcp, 22, {group_id: public_security_group.id})
Launching the Instances
Create a network interface that you will attach a public IP to. This will be on the public subnet and use the public security group.
interface = $ec2.network_interfaces.create(
subnet: public_subnet,
security_groups: public_security_group
)
sleep 2 until interface.status == :available
elastic_ip = $ec2.elastic_ips.create(vpc: true)
elastic_ip.associate(network_interface: interface)
## Use this to fill in ELASTIC_IP later on
puts elastic_ip.public_ip
Launch an instance into your VPC, since you’re specifiying to use an interface that’s already connected to your VPC you will be on its’ subnet and use its’ security groups.
ssh_server = $ec2.instances.create(
availability_zone: "us-east-1d",
instance_type: "t1.micro",
key_name: "key-lab",
image_id: 'ami-e995e380', ## Ubuntu 13.04 amd64
network_interfaces: [{device_index: 0, network_interface_id: interface.id}]
)
## Wait around 30-60 seconds for the server to come up
sleep 2 until ssh_server.status == :running
Now you should be able to SSH to the server
$ ssh -i ~/.ec2/key-test.pem ubuntu@ELASTIC_IP
We can use an SSH config file (at ~/.ssh/config
) to make that easier.
note: You may want to consider adding a DNS CNAME entry for your elastic IP. This way, if it changes later, there’s nothing that needs updating besides the DNS.
Host ssh-gateway
HostName ELASTIC_IP
User ubuntu
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
IdentityFile ~/.ec2/key-lab.pem
LogLevel quiet
Now, just ssh to that server
$ ssh ssh-gateway
Next, let’s launch an instance inside the private subnet. This will only have an internal IP, so it wouldn’t otherwise have the ability to be routable to the outside world.
internal_server = $ec2.instances.create(
subnet: private_subnet,
security_groups: private_security_group,
availability_zone: "us-east-1d",
instance_type: "t1.micro",
key_name: "key-lab",
image_id: 'ami-e995e380' ## Ubuntu 13.04 amd64
)
## Again, wait around 30-60 seconds for the instance to launch
sleep 2 until internal_server.status == :running
## Get the internal IP address of the server
puts internal_server.private_ip_address
With some ssh proxying magic, you can get to it through the ssh
server that you’ve created by adding this to ~/.ssh/config
.
Host 10.0.1.*
User ubuntu
StrictHostKeyChecking no
UserKnownHostsFile /dev/null
IdentityFile ~/.ec2/key-lab.pem
ProxyCommand ssh -W %h:%p ssh-gateway
LogLevel quiet
Now you can just ssh to that server’s internal IP address
$ ssh 10.0.1.247
$ ssh 10.0.1.247 'ip addr show eth0'
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
link/ether 1e:f1:ea:c9:58:45 brd ff:ff:ff:ff:ff:ff
inet 10.0.1.247/24 brd 10.0.1.255 scope global eth0
inet6 fe80::1cf1:eaff:fec9:5845/64 scope link
valid_lft forever preferred_lft forever
Clean-up
Amazon charges you per hour, so if you’re just testing and want to shut everything down, here are some clean-up steps. There’s a mess of dependencies, so you need to do it in a specific order.
## Shut down the instances
internal_server.terminate
ssh_server.terminate
## Wait for the ssh server to go down
sleep 2 until ssh_server.status == :terminated
## Disassociate the Elastic IP and delete the network interface
elastic_ip.disassociate
interface.delete
## Delete the security groups
private_security_group.delete
public_security_group.delete
## Delete the subnets
private_subnet.delete
public_subnet.delete
## Get rid of the subnets
public_route.routes.last.delete
## Detach and delete the Internet Gateway
gateway.detach($vpc)
gateway.delete
## Finally, delete the VPC
$vpc.delete
More to Come
There is, obviously, a lot more to VPC, and I hope to cover some more topics in future blog posts.