The VPC side has been set up. We can verify the public subnet works as intended by deploying a bastion host in the public network.
Funny name but unless we connect to the private part of the VPC via site to site VPN or Direct Connect we will a way to connect to EC2s deployed in the private subnets somehow. That “somehow” is an EC2 instance in the public subnet that only allows SSH connections from specific IPs. We do not want the internet to try and brute force them selves into that host.
OK, so, we’ll need and EC2 and a security group. Let’s add them to our VPC yaml file.
Things to note here:
- the reference to myVPC (nothing new there)
- the RemoteAccessCIDR reference
- the ingress rule
The RemoteAccessCIDR is new and we reference it. We want this to be a parameter for easy setup and re-use.
I skip the metadata part for now, here is the parameter part:
No default value this time! That means we will be forced to give this parameter a value.
The ingress rule is only allowing SSH traffic on port 22 if it comes from the SSHRemoteAccessCIDR. Any other traffic is rejected.
Off to the Bastion host where we will start by hardcoding a few things.
The AMI ID is for Centos 7. There is plenty of choices, use what floats your boat.
The instance size is a t2.micro. We really do not need more for this type of host that is going to be idle most of the time.
The security group and subnet are already known. But we need a key pair. So go to the AWS console into the EC2 section and create a new key pair: I called mine “cfntest”.
We reference a BastionKeyPairName, so again, we want this to be a parameter. This is a little different from the previous parameters as there is a construct that will give us a list of all key pairs in a drop down list for easy selection in the Cloudformation interface.
We are ready for a first run. If you had difficulties completing the metadata part of the file, you can use the file from the github repository.
Notice the 2 new fields in the Cloudformation web interface:
In the drop down menu, you will find the key pair we created earlier.
In the bastion access CIDR, I will limit the CIDR to my outgoing work address IP. The screenshot IP is of course invented for security reasons. Use your outgoing IP. If you wonder what it is, open your browser and google “what’s my ip”.
Let’s create our stack …
All should be going well and you should have a bastion host created in subnet 1. If you wonder what’s its IP, you can get it in the EC2 section. But we will use an Output to make our life easier. Add the outputs at the end of the file.
We use the GetAtt intrinsic function to get the resource attributes we want: PublicDnsName and PublicIp.
Now, maybe you have already updated your stack. If so kudos! But in case you didn’t, now is the time.
Choose “Replace current template” and re-upload the file. Our parameters have not changed, we can “next” our way to the stack creation and this time update the stack.
The bastion host public DNS and public IP are now displayed in the “Outputs” tab in Cloudformation.
Remember the key pair we created earlier. It should be in your downloads folder.
We need to change the rights on it or our ssh connection will not succeed.
Now we are ready to check if we can log into the bastion host.
➜ ssh -i cfntest.pem firstname.lastname@example.org
The authenticity of host ‘22.214.171.124 (126.96.36.199)’ can’t be established.
ECDSA key fingerprint is SHA256:IzF8sTEGjeuew8XBo96TNpu5M+QOTedZVms7ymfaCGs.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added ‘188.8.131.52’ (ECDSA) to the list of known hosts.
[centos@ip-10-0-1-219 ~]$ ping google.com
PING google.com (184.108.40.206) 56(84) bytes of data.
64 bytes from dg-in-f113.1e100.net (220.127.116.11): icmp_seq=1 ttl=40 time=1.02 ms
64 bytes from dg-in-f113.1e100.net (18.104.22.168): icmp_seq=2 ttl=40 time=1.04 ms
— google.com ping statistics —
2 packets transmitted, 2 received, 0% packet loss, time 1001ms
rtt min/avg/max/mdev = 1.025/1.032/1.040/0.033 ms
Outgoing internet works as intended. Looks good. Try to log in from another IP (tethered from your phone for example). It won’t work thanks to the security group we configured.
We could deploy an EC2 in a private subnet and connect to it as well.
The private host has only an internal IP. So, there is no login into it from the internet. This is exactly what we wanted. The private security group is different from the previous security group. We don’t use a CIDR for authorised connections. We use source security group ID from the BastionSecurityGroup. This means that if the connection originates from that security group, it is authorised.
The only way we can connect to the private host is through the bastion host. Before you continue, read this: Securely Connect to Linux Instances Running in a Private Amazon VPC. Done? Good, we can now connect to the private host via the bastion host.
The first connection is to the bastion host, the second to the internal IP of the private host. The private host has internet access via the NAT Gateway, meaning it can initiate connections to internet services, but there is no way to access the private host from the internet. To test the private host connectivity to the internet, update the OS for example.
Now, be aware, that there are no outbound restrictions in this setup, neither on the security group, nor via network ACLs, so outbound, everything is possible. What should be the right security configuration depends on what you want to do and how you want to do it. There are no stone written rules: make your own. Rule of thumb: open as much as needed and as little as possible.
We validated our setup works. In Part 4, we will add a little more flexibility to our template and deploy an nginx to the private host that we will access from the internet via a load balancer. “But you said the only way to access the private host is via the bastion host, …”. Yeah, well, the load balancer is another way 😉
Anyway, the template gets bigger. I already mentioned it but, from now on, you can use the Github repository to follow along.