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_demo
cd 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.

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-keygen
Generating 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_rsa
Your public key has been saved in /Users/keithhubner/.ssh/aws_id_rsa.pub
The key fingerprint is:
[Your unique fingerprint]
The key's randomart image is:
[Your unique image]

Setting Up the Terraform Providers

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"
}
}
}

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 provider
selections it made above. Include this file in your version control repository
so that Terraform can guarantee to make the same selections by default when
you run "terraform init" in the future.
Terraform has been successfully initialized!

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"

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 Provider
provider "aws" {
region = "eu-west-1"
access_key = var.AWS_ACCESS_KEY_ID
secret_key = var.AWS_SECRET_ACCESS_KEY
}
# Configure Twingate Provider
provider "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

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 VPC
resource "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 ami
data "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 AMI
data "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 Provider
provider "aws" {
region = "eu-west-1"
access_key = var.AWS_ACCESS_KEY_ID
secret_key = var.AWS_SECRET_ACCESS_KEY
}
# Configure Twingate Provider
provider "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 VPC
resource "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 ami
data "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 AMI
data "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.

...
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.164
The 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])? yes
Warning: 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 the
individual files in /usr/share/doc/*/copyright.
Ubuntu comes with ABSOLUTELY NO WARRANTY, to the extent permitted by
applicable 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 2 months ago