Create a VPN-secured VPC with Packer and Terraform
Securing a web application in terms of access management can be tricky, as there are multiple ways to do it in an acceptable way.
We can use Security Groups to limit the available ports for a given instance and leaving a specific company IP to have unrestricted access, but in this way we are giving reachability to anyone connected to that network, which in most occasions is not ideal. A problem of limiting the access to a given IP will be the fact of blocking-out an engineer trying to fix a problem from the outside during after-hours. Another way to approach access management would be to set up a key-server with a very limited set of authorized users registered. With a key-server we have the problem of the need to set it up in every instance we want secured, which in some situations is not very feasible due to network size or other multiple factors.
Today we are going to approach this using a VPN, giving an authorized user a tunnel to a VPC and making it fell like if its devices were directly connected to the network.
What Is a VPN?
VPN stands for Virtual Private Network. Usually VPNs are used in corporate environments to protect the data transmission between branches located remotely in different cities or even countries. A VPN makes every computer connected to it operate like they were all in the same local network, making routing and maintenance very easy for the IT teams, as they can build an entire Intranet with many critical machines completely isolated from the Internet.
Do I Need a VPN?
The short answer is yes. Focusing on the use-case of this tutorial, we will benefit from a VPN connection because our computer, as IT engineers, are going to have access to the resources in machines that don’t even have public IPs. But what’s more, VPN provides more security as data that travels through the VPN is encrypted and private.
Preparing the Deployment
Now that we have a clear idea what a VPN is and why we need one, let’s plan the deployment. The system will be a Debian 9 machine running OpenVPN. To build the image we will use Packer and to finally deploy the infrastructure, Terraform. You’ll need to have an Alibaba Cloud Elastic Compute Service (ECS) server ready, so check out this tutorial if you are not sure how to set up one. Now just create a folder for the config files… and go!
Generate and Upload Your Key
If you haven’t uploaded your key yet, you will need to complete this part. Otherwise, you can just jump to the next step. This part will be completed using the Alibaba Cloud console. Once you log in, go to the Elastic Compute Service Management Console and then, from the menu in the left side, click in the Key Pairs link.
To generate your key, in Mac and Linux, open the terminal and type ssh-keygen -t rsa
and follow the process it launches. When your key is created, run cat ~/.ssh/id_rsa.pub
and copy the output it throws.
Let’s go back to the online console. Once in Key Pairs, click Create Key Pair. If is your personal computer, you can name it personal, but I guess here you can just put your preferred name, as long as you can identify it later if you upload more keys. In Creation Type, select Import an Existing Key Pair and a new text area will show up. Paste here the output we copied before and then OK to save it.
Using Packer to Generate the ECS Image
As said, we are going to build OpenVPN on top of Debian 9 (stretch). Open the project folder and create a file named openvpn.json
. I prepared for you a template so you can just copy and paste the following JSON into it, but try to understand it first using Packer Documentation.
{
"variables": {
"access_key": "{{env `ALICLOUD_ACCESS_KEY`}}",
"region": "{{env `ALICLOUD_REGION`}}",
"secret_key": "{{env `ALICLOUD_SECRET_KEY`}}"
},
"builders": [
{
"type": "alicloud-ecs",
"access_key": "{{user `access_key`}}",
"secret_key": "{{user `secret_key`}}",
"region": "{{user `region`}}",
"image_name": "openvpn-stretch",
"source_image": "debian_9_02_64_20G_alibase_20171023.vhd",
"ssh_username": "root",
"instance_type": "ecs.t5-lc1m1.small",
"internet_charge_type": "PayByTraffic",
"io_optimized": "true"
}
],
"provisioners": [
{
"type": "shell",
"script": "base-setup.sh"
}
]
}
Next to it, make a file named base-setup.sh
with the following contents:
#!/bin/bash
export DEBIAN_FRONTEND=noninteractive
echo "nameserver 1.1.1.1" >> /etc/resolv.conf
apt-get update && apt-get upgrade -y && apt-get install -y net-tools
wget -nv -O /opt/openvpn.deb http://swupdate.openvpn.org/as/openvpn-as-2.5.2-Debian9.amd_64.deb
cat <<- 'EOF' > /opt/start.sh
#!/bin/bash
dpkg -i /opt/openvpn.deb
EOF
Creating the Terraform Infrastructure Files
For this example, we will create 2 ECS instances, 1 new VPC and 1 Security Group. One ECS will be the VPN and will have public IP, the other will be the machine with sensitive data and will be only accessible using the VPN, with no public IP. Create a file named main.tf
with the following contents:
provider "alicloud" {}
variable "vpn_ecs_password" {
default = "Test1234!"
}
data "alicloud_zones" "default" {}
resource "alicloud_vpc" "vpc" {
name = "vpn_secured"
cidr_block = "172.16.0.0/12"
}
resource "alicloud_vswitch" "vswitch" {
name = "vsw_vpn"
availability_zone = "${data.alicloud_zones.default.zones.0.id}"
cidr_block = "172.16.0.0/16"
vpc_id = "${alicloud_vpc.vpc.id}"
depends_on = [
"alicloud_vpc.vpc"
]
}
resource "alicloud_security_group" "vpn_sg" {
name = "sg_vpn"
vpc_id = "${alicloud_vpc.vpc.id}"
}
resource "alicloud_security_group_rule" "vpn_ssh" {
type = "ingress"
ip_protocol = "tcp"
nic_type = "intranet"
policy = "accept"
port_range = "22/22"
priority = 1
security_group_id = "${alicloud_security_group.vpn_sg.id}"
cidr_ip = "0.0.0.0/0"
}
resource "alicloud_security_group_rule" "vpn_web" {
type = "ingress"
ip_protocol = "tcp"
nic_type = "intranet"
policy = "accept"
port_range = "943/943"
priority = 1
security_group_id = "${alicloud_security_group.vpn_sg.id}"
cidr_ip = "0.0.0.0/0"
}
resource "alicloud_security_group_rule" "vpn_client" {
type = "ingress"
ip_protocol = "udp"
nic_type = "intranet"
policy = "accept"
port_range = "1194/1194"
priority = 1
security_group_id = "${alicloud_security_group.vpn_sg.id}"
cidr_ip = "0.0.0.0/0"
}
data "alicloud_images" "vpn_packer_image" {
name_regex = "openvpn-stretch"
}
resource "alicloud_instance" "vpn_server" {
instance_name = "vpn-server"
image_id = "${data.alicloud_images.vpn_packer_image.images.0.id}"
instance_type = "ecs.t5-lc1m1.small"
vswitch_id = "${alicloud_vswitch.vswitch.id}"
internet_max_bandwidth_out = 100
security_groups = [
"${alicloud_security_group.vpn_sg.id}"
]
key_name = "personal"
provisioner "remote-exec" {
inline = [
"sh /opt/start.sh"
]
connection {
host = "${alicloud_instance.vpn_server.public_ip}"
private_key = "${file("~/.ssh/id_rsa")}"
}
}
}
resource "alicloud_security_group" "secret_machine_sg" {
name = "sg_vpn"
vpc_id = "${alicloud_vpc.vpc.id}"
}
resource "alicloud_security_group_rule" "ssh_from_vpn" {
type = "ingress"
ip_protocol = "tcp"
nic_type = "intranet"
policy = "accept"
port_range = "22/22"
priority = 1
security_group_id = "${alicloud_security_group.secret_machine_sg.id}"
cidr_ip = "${alicloud_vswitch.vswitch.cidr_block}"
}
data "alicloud_images" "debian_9" {
name_regex = "^debian_9*"
}
resource "alicloud_instance" "secret_machine" {
instance_name = "secret-machine"
image_id = "${data.alicloud_images.debian_9.images.0.id}"
instance_type = "ecs.t5-lc1m1.small"
vswitch_id = "${alicloud_vswitch.vswitch.id}"
security_groups = [
"${alicloud_security_group.secret_machine_sg.id}"
]
key_name = "personal"
}
output "do_next" {
value = "Go to https://${alicloud_instance.vpn_server.public_ip}:943/admin"
}
output "secret_machine_ip" {
value = "${alicloud_instance.secret_machine.private_ip}"
}
Build the Image and Deploy the Infrastructure
For this to work, you need to have 3 environment variables in your machine with the relevant values for you, ALICLOUD_REGION
, ALICLOUD_ACCESS_KEY
and ALICLOUD_SECRET_KEY
. After creating the Packer json
and the Terraform tf
files, we can make this work. Navigate into the project folder with cd
and run packer build openvpn.json
. After the image is created, run terraform init
and then terraform apply
. This will take a while, so patiently look to the command outputs to see if everything goes well. At the end, the Terraform process will output the public IP of the instance, let’s assume 123.123.123.123
in this case.
Create a VPN Profile
First of all, we need to set up the openvpn
user password in the machine. For this, go ssh [email protected]
and run passwd openvpn
, this will prompt for a password.
Now, using your browser, navigate to https://123.123.123.123:943/admin
and log in using the user openvpn
and the password you just set. We still need to tweak one more thing, as the server is not aware of its public IP, so click in Network Settings and update the Hostname or IP Address field with the public IP.
Its time to download the profile, go to https://123.123.123.123:943
and click in Yourself (user-locked profile).
Connect to the VPN
There are multiple options to connect our devices to a VPN, but here I’ll describe only one, as you can really choose your favorite, the final result won’t change at this point. In general, I personally like GUI clients when they give good functionality and SO integration. For VPN clients, I like Tunnelblick, as it lets you manage multiple profiles easily and the ability to switch quickly between them.
After downloading the *.ovpn
file, Tunnelblick recognizes the format and with just double-clicking in the file it will assist you setting it up. This app, when running, shows an icon in the menu bar to access to its options. The icon will change the look depending in the connection status as well.
SSH Into the Web Server Using Its Private IP
From now, our computer will route all the connections to IPs belonging to the VPC through the VPN. This is a big thing, as we are logging in a machine that belongs to another network using its private IP. Do ssh root@PRIVATE_IP
and enjoy your setup!
Original article: Create a VPN-secured VPC with Packer and Terraform.