How to Use Terraform with AWS and Twingate
This guide provides step-by-step instructions on automating Twingate deployments with Terraform on AWS.
Getting Started - What we are going to build
The goal of this guide is to use Terraform to deploy Twingate on an AWS VPC including all required components (Connector, Remote Network) and Configuration Items (Resource, Group, etc.):
First let’s setup a new folder for our Terraform code to reside in:
mkdir twingate_aws_democd twingate_aws_demo
All commands below will be run from within the folder created, we can now open this folder in our editor of choice.
Storing Terraform Code
For simplicity and readability all the code will be included in a single main.tf file. For more information on Terraform code structure, please see this guide and this guide.
Generating SSH keys
In order to connect to our test VM, we will be using an SSH key pair for authentication, below is an example of how to do this, for more information please see this link.
Using terminal (replacing the username path with your own):
ssh-keygenGenerating public/private rsa key pair.
Enter file in which to save the key (/Users/username]/.ssh/id_rsa): /Users/username/.ssh/aws_id_rsa
Enter passphrase (empty for no passphrase):Enter same passphrase again:Your identification has been saved in /Users/username/.ssh/aws_id_rsaYour public key has been saved in /Users/keithhubner/.ssh/aws_id_rsa.pubThe key fingerprint is:
[Your unique fingerprint]
The key's randomart image is:
[Your unique image]
Setting Up the Terraform Providers
On Terraform Providers
A Terraform Provider is a Terraform Plugin that leverages an external API (like the Twingate API) and makes certain functions available right from within Terraform without having to know anything about the external API itself.
First let’s setup the provider configuration: create a new file called main.tf (amending each value to match your environment/requirements):
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } twingate = { source = "twingate/twingate" } }}
Twingate Terraform Provider
The latest Terraform Provider for Twingate is always available here.
We need 2 providers in this case: One for Twingate (it will allow us to create and configure Twingate specific Configuration Items) and one for AWS (it will allow us to spin up the required infrastructure / VPC).
Once this is in place we can run the following command to download and install those providers locally on your system:
terraform init
You should see the provider plugins being initialized and installed successfully:
Initializing the backend...
Initializing provider plugins...- Finding hashicorp/aws versions matching "~> 4.0"...- Finding latest version of twingate/twingate...- Installing hashicorp/aws v4.29.0...- Installed hashicorp/aws v4.29.0 (signed by HashiCorp)- Installing twingate/twingate v0.2.0...- Installed twingate/twingate v0.2.0 (self-signed, key ID E8EBBE80BA0C9369)
Partner and community providers are signed by their developers.If you'd like to know more about provider signing, you can read about it here:https://www.terraform.io/docs/cli/plugins/signing.html
Terraform has created a lock file .terraform.lock.hcl to record the providerselections it made above. Include this file in your version control repositoryso that Terraform can guarantee to make the same selections by default whenyou run "terraform init" in the future.
Terraform has been successfully initialized!
AWS Authentication
There are a couple of ways to authenticate with AWS; for simplicity we will be specifying the connection details in variables stored in terraform.tfvars. We encourage you to review the documentation here and pick a method which best suits your environment & needs.
Creating the Twingate infrastructure
To create the Twingate infrastructure we will need a way to authenticate with Twingate. To do this we will create a new API key which we will use with Terraform.
In order to do so: Navigate to Settings → API then Generate a new Token:
You will need to set your token with Read, Write & Provision permissions, but you may want to restrict the allowed IP range to only where you will run your Terraform commands from.
Click on generate and copy the token.
Terraform vars file
Like all programming languages, terraform can use variables to define values. Let’s create a new file called terraform.tfvars, we will use it to define useful variables.
Add the following lines to this file, adding in the value of the API Token into tg_api_key and the name of your Twingate Tenant. (The Tenant is the mycorp
part of the URL to your Twingate Console if the full URL was https://mycorp.twingate.com
)
Also, you will need the AWS “Access Key ID and “Secrect Access Key” if you are using this authentication method. More details on these can be found here.
AWS_ACCESS_KEY_ID=""AWS_SECRET_ACCESS_KEY=""
tg_api_key="COPIED API KEY"tg_network="mycorp"
Using Source Control
If you are using or thinking of using source control, please ensure you exclude this file for security reasons. (You don’t want your API tokens visible in clear text on a public repository!)
We can then add these variables to the main.tf file:
variable "AWS_ACCESS_KEY_ID" {}variable "AWS_SECRET_ACCESS_KEY" {}variable "tg_api_key" {}variable "tg_network" {}
And reference these in the provider config (no change needed):
# Configure the AWS Providerprovider "aws" { region = "eu-west-1" access_key = var.AWS_ACCESS_KEY_ID secret_key = var.AWS_SECRET_ACCESS_KEY}
# Configure Twingate Providerprovider "twingate" { api_token = var.tg_api_key network = var.tg_network}
We also need to set the key pair we generated so this can be used by the VMs for authentication.
resource "aws_key_pair" "ssh_access_key" { key_name = "~/.ssh/aws_id_rsa" public_key = file("~/.ssh/aws_id_rsa.pub")}
Now we can start creating resources in Twingate, let’s first start with the highest level concept: the Twingate Remote Network:
resource "twingate_remote_network" "aws_demo_network" { name = "aws demo remote network"}
Then we need to create the connector.
resource "twingate_connector" "aws_demo_connector" { remote_network_id = twingate_remote_network.aws_demo_network.id}
Some clarification here as well:
- twingate_connector refers to a Connector as per the Terraform provider for Twingate
- remote_network_id is the only parameter required to create a Connector: this is consistent with creating a connector from the Admin Console: you need to attach it to a remote network.
- twingate_remote_network.aws_demo_network.id is read as
<Terraform resource type>
.<Terraform resource name>
.<internal ID of that object>
And finally, we need to create the tokens which the remote connector will use to communicate with Twingate.
resource "twingate_connector_tokens" "aws_connector_tokens" { connector_id = twingate_connector.aws_demo_connector.id}
It’s a good idea at this point to do a quick check on our Terrform script by running:
terraform plan
terraform plan
is a non-destructive command: it runs a simulation of what Terraform needs to do. It is therefore safe to run and is useful towards troubleshooting your code.
You should see a response similar to this:
...
# twingate_connector.aws_demo_connector will be created + resource "twingate_connector" "aws_demo_connector" { + id = (known after apply) + name = (known after apply) + remote_network_id = (known after apply) }
# twingate_connector_tokens.aws_connector_tokens will be created + resource "twingate_connector_tokens" "aws_connector_tokens" { + access_token = (sensitive value) + connector_id = (known after apply) + id = (known after apply) + refresh_token = (sensitive value) }
# twingate_remote_network.aws_demo_network will be created + resource "twingate_remote_network" "aws_demo_network" { + id = (known after apply) + name = "aws demo remote network" }
Plan: 4 to add, 0 to change, 0 to destroy.
If this is consistent with what you are seeing, we can then move onto doing the same for the AWS infrastructure needed.
Creating the AWS Infrastructure
First we will create a new resource group to “house” this demo.
(Note: this is a very simple demonstration on how we can provision resources in Terraform. You will need to review and change items to suit your environment, for example regions and IP address ranges.)
# Create a VPCresource "aws_vpc" "main" { cidr_block = "10.0.0.0/16"}
Then we need a new AWS subnet.
resource "aws_subnet" "main" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24"
tags = { Name = "Main Subnet" }}
Our Twingate connector only requires outbound internet access. To allow this we need to create an internet gateway and routing.
resource "aws_internet_gateway" "gw" { vpc_id = aws_vpc.main.id
tags = { Name = "my-gateway" }}
resource "aws_route_table" "example" { vpc_id = aws_vpc.main.id
route { cidr_block = "0.0.0.0/0" #allow all gateway_id = aws_internet_gateway.gw.id }
route { ipv6_cidr_block = "::/0" gateway_id = aws_internet_gateway.gw.id }
tags = { Name = "my-route-table" }}
resource "aws_route_table_association" "a" { subnet_id = aws_subnet.main.id route_table_id = aws_route_table.example.id}
Virtual Machines
As shown in the diagram, we are building 2 VMs. One will be our test VM which is private and has no public internet access. The second will be our Twingate connector which does have internet access but is not accessible from the outside world via its public interface.
In this example we are querying AWS for the latest Ubunutu AMI, which we will then use to build the VM:
# Data Block top get latest amidata "aws_ami" "ubuntu" { most_recent = true
filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] }
filter { name = "virtualization-type" values = ["hvm"] }
owners = ["099720109477"] # Canonical}
And a similar technique to find the most recent Twingate AMI:
#Twingate AMIdata "aws_ami" "twingate" { most_recent = true
filter { name = "name" values = ["twingate/images/hvm-ssd/twingate-amd64-*"] }
owners = ["617935088040"] # Twingate}
We can then use the images to build the VMs.
Firstly the VM we will be using to test connectivity over Twingate:
(Note: we are specifying the key we generated earlier)
resource "aws_instance" "test" { ami = data.aws_ami.ubuntu.id instance_type = "t3.micro"
key_name = aws_key_pair.ssh_access_key.key_name
subnet_id = aws_subnet.main.id
tags = { "Name" = "Demo VM" }}
Then the Twingate connector.
As you can see we are pulling in variables here to build the configuration for the connector:
resource "aws_instance" "twingate_connector" { ami = data.aws_ami.twingate.id instance_type = "t3.micro" associate_public_ip_address = true key_name = aws_key_pair.ssh_access_key.key_name
user_data = <<-EOT #!/bin/bash set -e mkdir -p /etc/twingate/ { echo TWINGATE_URL="https://${var.tg_network}.twingate.com" echo TWINGATE_ACCESS_TOKEN="${twingate_connector_tokens.aws_connector_tokens.access_token}" echo TWINGATE_REFRESH_TOKEN="${twingate_connector_tokens.aws_connector_tokens.refresh_token}" } > /etc/twingate/connector.conf sudo systemctl enable --now twingate-connector EOT
subnet_id = aws_subnet.main.id
tags = { "Name" = "Twingate Connector" }}
Finally we can create the last part of the Twingate configuration, a new group and a resource.
Group:
resource "twingate_group" "aws_demo" { name = "aws demo group"}
Resource:
resource "twingate_resource" "aws_demo_resource" { name = "aws demo web sever" address = aws_instance.test.private_ip remote_network_id = twingate_remote_network.aws_demo_network.id group_ids = [twingate_group.aws_demo.id] protocols { allow_icmp = true tcp { policy = "RESTRICTED" ports = ["22"] } udp { policy = "ALLOW_ALL" } }
}
As you can see in this example we are only allowing port 22 to test basic SSH access to the resource. You may want to change this depending on your circumstances.
Finished Scripts
terraform { required_providers { aws = { source = "hashicorp/aws" version = "~> 4.0" } twingate = { source = "twingate/twingate" } }}
variable "AWS_ACCESS_KEY_ID" {}variable "AWS_SECRET_ACCESS_KEY" {}variable "tg_api_key" {}variable "tg_network" {}
# Configure the AWS Providerprovider "aws" { region = "eu-west-1" access_key = var.AWS_ACCESS_KEY_ID secret_key = var.AWS_SECRET_ACCESS_KEY}
# Configure Twingate Providerprovider "twingate" { api_token = var.tg_api_key network = var.tg_network}
resource "aws_key_pair" "ssh_access_key" { key_name = "~/.ssh/aws_id_rsa" public_key = file("~/.ssh/aws_id_rsa.pub")}
resource "twingate_remote_network" "aws_demo_network" { name = "aws demo remote network"}
resource "twingate_connector" "aws_demo_connector" { remote_network_id = twingate_remote_network.aws_demo_network.id}
resource "twingate_connector_tokens" "aws_connector_tokens" { connector_id = twingate_connector.aws_demo_connector.id}
# Create a VPCresource "aws_vpc" "main" { cidr_block = "10.0.0.0/16"}
resource "aws_subnet" "main" { vpc_id = aws_vpc.main.id cidr_block = "10.0.1.0/24"
tags = { Name = "Main Subnet" }}
resource "aws_internet_gateway" "gw" { vpc_id = aws_vpc.main.id
tags = { Name = "my-gateway" }}
resource "aws_route_table" "example" { vpc_id = aws_vpc.main.id
route { cidr_block = "0.0.0.0/0" #allow all gateway_id = aws_internet_gateway.gw.id }
route { ipv6_cidr_block = "::/0" gateway_id = aws_internet_gateway.gw.id }
tags = { Name = "my-route-table" }}
resource "aws_route_table_association" "a" { subnet_id = aws_subnet.main.id route_table_id = aws_route_table.example.id}
# Data Block top get latest amidata "aws_ami" "ubuntu" { most_recent = true
filter { name = "name" values = ["ubuntu/images/hvm-ssd/ubuntu-focal-20.04-amd64-server-*"] }
filter { name = "virtualization-type" values = ["hvm"] }
owners = ["099720109477"] # Canonical}
#Twingate AMIdata "aws_ami" "twingate" { most_recent = true
filter { name = "name" values = ["twingate/images/hvm-ssd/twingate-amd64-*"] }
owners = ["617935088040"] # Twingate}
resource "aws_instance" "test" { ami = data.aws_ami.ubuntu.id instance_type = "t3.micro"
key_name = aws_key_pair.ssh_access_key.key_name
subnet_id = aws_subnet.main.id
tags = { "Name" = "Demo VM" }}
resource "aws_instance" "twingate_connector" { ami = data.aws_ami.twingate.id instance_type = "t3.micro" associate_public_ip_address = true key_name = aws_key_pair.ssh_access_key.key_name
user_data = <<-EOT #!/bin/bash set -e mkdir -p /etc/twingate/ { echo TWINGATE_URL="https://${var.tg_network}.twingate.com" echo TWINGATE_ACCESS_TOKEN="${twingate_connector_tokens.aws_connector_tokens.access_token}" echo TWINGATE_REFRESH_TOKEN="${twingate_connector_tokens.aws_connector_tokens.refresh_token}" } > /etc/twingate/connector.conf sudo systemctl enable --now twingate-connector EOT
subnet_id = aws_subnet.main.id
tags = { "Name" = "Twingate Connector" }}
resource "twingate_group" "aws_demo" { name = "aws demo group"}
resource "twingate_resource" "aws_demo_resource" { name = "aws demo web sever" address = aws_instance.test.private_ip remote_network_id = twingate_remote_network.aws_demo_network.id group_ids = [twingate_group.aws_demo.id] protocols { allow_icmp = true tcp { policy = "RESTRICTED" ports = ["22"] } udp { policy = "ALLOW_ALL" } }
}
AWS_ACCESS_KEY_ID="YOUR AWS ACCESS KEY"AWS_SECRET_ACCESS_KEY="YOUR AWS SECRET"
tg_api_key="YOUR TWINGATE API KEY"tg_network="YOUR TWINGATE NETWORK"
Deployment: Running our script 🏃♂️
The first thing we want to do is to check the script using the terraform plan
command:
terraform plan
All being well this will run and show you all the resources which will be added to both Twingate and Terraform.
On Using Terraform Apply
As this is a brand new infrastructure you should not see anything being destroyed. Please make sure you double check what is being added and where!
...Plan: 13 to add, 0 to change, 0 to destroy.
Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve.
Enter a value:
Once you are happy with what is being created, you can now apply this.
terraform apply
You will need to confirm you are happy for the changes to proceed, again double check what is happening is what you expect to happen.
You should then see all the resources being created.
Checking What Has Been Created
Next we want to check our infrastructure has been built and we can connect to the AWS test VM.
If you open the Twingate admin panel, you will see a new network:
And a new Connector:
and a new Group:
and a new Resource:
If you then look at the AWS portal, you will see the new instances up and running:
Testing our connection
At this point we need to add a Twingate user to the new group that has been created:
After a few moments, you should then see the new resource in your Twingate client:
We can now try connecting to the VM via ssh (using the key we generated):
ssh -i ~/.ssh/aws_id_rsa ubuntu@10.0.1.164The authenticity of host '10.0.1.264 (10.0.1.164)' can't be established.ECDSA key fingerprint is SHA256:VFOTKrZvzwK5hgCwCZApGCDrrzFp3ISTaZoKaBG9218.Are you sure you want to continue connecting (yes/no/[fingerprint])? yesWarning: Permanently added '10.0.1.214' (ECDSA) to the list of known hosts.
You should then see the VM logged in:
Welcome to Ubuntu 20.04.5 LTS (GNU/Linux 5.15.0-1019-aws x86_64)
* Documentation: https://help.ubuntu.com * Management: https://landscape.canonical.com * Support: https://ubuntu.com/advantage
System information as of Tue Sep 6 06:39:14 UTC 2022
System load: 0.0 Processes: 102 Usage of /: 19.6% of 7.57GB Users logged in: 0 Memory usage: 21% IPv4 address for ens5: 10.0.1.214 Swap usage: 0%
0 updates can be applied immediately.
The programs included with the Ubuntu system are free software;the exact distribution terms for each program are described in theindividual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted byapplicable law.
To run a command as administrator (user "root"), use "sudo <command>".See "man sudo_root" for details.
ubuntu@ip-10-0-1-164:~$
We have now successfully created our infrastructure and we can now take it all down just as quickly.
Tearing it all down
To remove the infrastructure you just created, just run:
terraform destroy
This will show you what will be destroyed, please check that this is correct before agreeing to the prompt.
You can now create and destroy this Twingate secured infrastructure with just a few keystrokes!
Last updated 3 months ago