commit d3d08bf71c05322713a5988be040160a49bbc563 Author: bdeshi Date: Mon Aug 15 15:28:56 2022 +0600 init diff --git a/.terraform-version b/.terraform-version new file mode 100644 index 0000000..c04c650 --- /dev/null +++ b/.terraform-version @@ -0,0 +1 @@ +1.2.7 diff --git a/.terraform.lock.hcl b/.terraform.lock.hcl new file mode 100644 index 0000000..23d7f4f --- /dev/null +++ b/.terraform.lock.hcl @@ -0,0 +1,64 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/davidsbond/tailscale" { + version = "0.12.2" + constraints = "~> 0.12.0" + hashes = [ + "h1:Ct6l3oqTKNi+sLcSqQFI4pnZ0MzoXVTTaK8xm34M/OI=", + "zh:5eaf378ea124dfd2628531012582e7cbd7a782c710f00794c04f3960118b2ad9", + "zh:770eb677c0230f17f8d5a48ea6b8d06424c860d541256a6f58cebba8011762ef", + "zh:7a784e8a05668e83e85f8791465adee0fb3ce9fe4885ba2a2b7f732df361a4f6", + "zh:80c6ba786a454cbc151e52f366cdc19c9d81f23ae00080fccb2d5083d2837c2d", + "zh:a0c6e08cc4c52536194a68ac835823b5467f4ff5268191bf1a89dc7d41bfd472", + "zh:aebf5123b045b2682a2fdbefab2273ff2a5699ff7f72b6c9a4759abe001221c9", + "zh:b2eb7e260749222f8a104ae9c883210afee1c71242eff5d3c6e1783f95ffe5cb", + "zh:b55906ea0be52c3ee674b9ac933d94af960573d462b4c93602701ea9f42dcd98", + "zh:d3878a61638d1a5fffcdf7e646ebfd6fa4d02c563e3f87c2b8333a43d45c9c69", + "zh:efa0221f96aaf75737702a1e1f3ea644ff18ba571ef1512d402b1b9e8a327d9e", + "zh:f0758b8f96065b559ab61b1f72cb7d2c27a4ba92487f8f5a41e244c16ca7478c", + "zh:f2f6659be19e4cdce78e8b56a2d91f56ddc0eb37da0328d54ae21912d4ff6961", + "zh:f5cf9652da93ac9b930aa5cf661e5ed2861b4c9aeeb242f692b3180a1f24fcf6", + "zh:fc00eec79e8719c14880b8ca97d5dfff1a206a60cf73bd465af6a8ce3d3726a2", + ] +} + +provider "registry.terraform.io/hashicorp/aws" { + version = "4.26.0" + constraints = "~> 4.26.0" + hashes = [ + "h1:jt8jLpFFhaapdbBqw4WQpDuLN8y7zF8/iLyCzypDxSQ=", + "zh:0579b105ae471894846fbd740bc9f10b2bd8a48860d8e640b4a9b53fb7d63ffe", + "zh:0ce445cfbffb6c0eee9e0e2a95850b5749d56aa8211b95a686c24dc2847a36ea", + "zh:41f0cf0810363cea4e54f3d9c452f2eb77123bcdaacc18b978c825496168cae2", + "zh:431a7e967b5c9d7ebde6c714abedd9464be6a62f7eafa1808a86a8bd92851317", + "zh:4afebd3c3a8c0646f0874493840b6f8c82f7f4302780faec5c7b0c616077eebe", + "zh:7f077662efc8d7b91ef604999daf6b45a968cb2f5d8c4512a00d2feb4db05a7a", + "zh:9a58d1ef049ccaa9615fe5722ba815065f45d172f8bc656ffdbab4ca16f6b786", + "zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425", + "zh:9d30b70a2daa0d94661590f6533e07071d2c7052b8279f05090f1bf037f56607", + "zh:b75f88be5d048849a632895d43b836ed1693031e586cd873ee915b5d3cf4fae6", + "zh:c57ac099b01fe49dd4e1e4674a06f61029fa6316e4f92a6a2a3bdc0444b371f9", + "zh:cb48a175ebb2a12fecae7dc6580bf88fbcf5408cdc53f3cf057150ebe9144034", + ] +} + +provider "registry.terraform.io/hashicorp/http" { + version = "3.0.1" + constraints = "~> 3.0.1" + hashes = [ + "h1:4N7YctkZrU+K2AvUF57c1qUvoD92bBJj6vXwf/FKMhM=", + "zh:3b161998147d8cc3986a1580ddb065009ab628747424934cbcb9d221783541f8", + "zh:62c78b565cde08d8e3b98e8138cd8e46b50fdc2ddc560ac1f62b5646ce8e9b1f", + "zh:69ba560cd6360a285e83e1c220ab140d3119371850756ff2ed0abe39d362ea49", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:95f38aebfa176a3424a329bc0f2e958bcf5a1f98d91dee21a436ca670fb2d570", + "zh:97eae729eb859948201d4393761f5c1a7ffe84046473527f65163f062d9af5d9", + "zh:b42de839114707e2fcfdf5ebf3a89129e5e17ebb5f84651c5775daecd776dc3b", + "zh:c47fa93605b8378504008534e0057e295d209a2128553c7b1bcc4fc7f6efafa2", + "zh:d9d4fe5143f80c1ccf22b055f069445ab7470942bb46027dadda8f3bc62d2780", + "zh:f051820764c50f4736d21e40d9b13a1ffde678748a9e6e1ef22a26adf27db9bf", + "zh:f67c9b73998fce13e94623be9b7afe89b30e3e6d34b504f765a344b11b8808b8", + "zh:f7d255dac5a73d30c7e629699fdf064decf705cd701d29e2120cef7bf0fb1d7f", + ] +} diff --git a/data.tf b/data.tf new file mode 100644 index 0000000..0239953 --- /dev/null +++ b/data.tf @@ -0,0 +1,28 @@ +data "aws_vpc" "selected" { + id = var.vpc_id +} + +data "aws_subnet" "selected" { + id = var.subnet_id +} + + +data "aws_ami" "selected" { + most_recent = true + owners = ["amazon"] + + filter { + name = "name" + values = ["amzn2-ami-*"] + } +} + +data "http" "relay_auth_key_response" { + url = "https://api.tailscale.com/api/v2/tailnet/${var.tailscale_domain}/keys/${tailscale_tailnet_key.relay_auth.id}" + + # Optional request headers + request_headers = { + Accept = "application/json" + Authorization = "Basic ${local.tailscale_auth_token}" + } +} diff --git a/files/acl.hujson.tftpl b/files/acl.hujson.tftpl new file mode 100644 index 0000000..f4ad7dd --- /dev/null +++ b/files/acl.hujson.tftpl @@ -0,0 +1,29 @@ +{ + "groups": { + "group:admin": [ %{~ for admin in admins ~} "${admin}@${domain}", %{~ endfor ~} ] + }, + "acls": [ + { "action": "accept", "users": ["*"], "ports": ["*:*"] } + ], + "tagOwners": { + "${tag}": ["group:admin", "${tag}"] + }, + "autoApprovers": { + "routes": { + %{~ for route in routes ~} + "${route}": ["group:admin", "${tag}"], + %{~ endfor ~} + }, + "exitNode": ["${tag}"] + }, + %{~ if enable_ssh ~} + "ssh": [ + { + "action": "check", + "src": ["autogroup:members"], + "dst": ["autogroup:self"], + "users": ["autogroup:nonroot", "root"] + } + ] + %{~ endif ~} +} diff --git a/files/relay-init.sh.tftpl b/files/relay-init.sh.tftpl new file mode 100644 index 0000000..5446127 --- /dev/null +++ b/files/relay-init.sh.tftpl @@ -0,0 +1,6 @@ +#!/bin/bash +echo 'net.ipv4.ip_forward = 1' >> /etc/sysctl.conf +echo 'net.ipv6.conf.all.forwarding = 1' >> /etc/sysctl.conf +sysctl -p /etc/sysctl.conf +curl -fsSL https://gist.githubusercontent.com/bdeshi/ba8fed1b5d357320d0314e8380c58454/raw/4978c0b60443e448607b59bc67c09f1dbbac9a56/tailscale-install.sh | sh +tailscale up %{ if length(routes) > 0 } --advertise-routes "${join(",", routes)}" %{ endif } --authkey "${auth_key}" --accept-dns=false diff --git a/locals.tf b/locals.tf new file mode 100644 index 0000000..4cb0421 --- /dev/null +++ b/locals.tf @@ -0,0 +1,17 @@ +locals { + + tailscale_auth_token = base64encode("${var.tailscale_api_key}:") + + # list of cidr routes: cidrs of selected vpc + additional cidrs if defined + tailscale_routes = var.advertise_routes ? concat( + data.aws_vpc.selected.cidr_block_associations[*].cidr_block, + length(var.additional_routes) > 0 ? var.additional_routes : [] + ) : [] + + # list of vpc dns servers: each vpc cidr base + 2 & fallback_nameservers if defined + tailscale_nameservers = var.advertise_nameservers ? concat( + [for cidr_block in data.aws_vpc.selected.cidr_block_associations : cidrhost(cidr_block.cidr_block, 2)], + length(var.fallback_nameservers) > 0 ? var.fallback_nameservers : [] + ) : [] + +} diff --git a/outputs.tf b/outputs.tf new file mode 100644 index 0000000..fd961dd --- /dev/null +++ b/outputs.tf @@ -0,0 +1,49 @@ +output "relay_auth_key" { + value = "${tailscale_tailnet_key.relay_auth.id} | expires: ${jsondecode(data.http.relay_auth_key_response.response_body).expires}" + description = "tailscale relay auth key" +} + +output "forwarded_routes" { + value = join(", ", local.tailscale_routes) + description = "forwarded routes" +} + +output "forwarded_nameservers" { + value = join(", ", local.tailscale_nameservers) + description = "forwarded nameservers" +} + +output "vpc_detail" { + value = "${var.vpc_id}%{for k, v in data.aws_vpc.selected.tags}%{if k == "Name"} | ${v}%{endif}%{endfor}" + description = "selected vpc" +} + +output "subnet_detail" { + value = "${var.subnet_id}%{for k, v in data.aws_subnet.selected.tags}%{if k == "Name"} | ${v}%{endif}%{endfor} | ${data.aws_subnet.selected.cidr_block}" + description = "selected subnet" +} + +output "security_group_detail" { + value = "${aws_security_group.tailscale.id} | ${aws_security_group.tailscale.name}" + description = "security group" +} + +output "ami_detail" { + value = "${data.aws_ami.selected.id} | ${data.aws_ami.selected.name}" + description = "selected ami" +} + +output "ec2_detail" { + value = "${aws_instance.tailscale.id} | ${var.relay_instance_type}" + description = "tailscale relay id" +} + +output "ec2_ip" { + value = "${aws_instance.tailscale.private_ip}%{if aws_instance.tailscale.public_ip != ""}, ${aws_instance.tailscale.public_ip}%{endif}" + description = "tailscale relay ip" +} + +output "ec2_ssh" { + value = var.relay_key_name == null ? "" : var.relay_key_name + description = "tailscale relay ssh" +} diff --git a/tailscale-network.tf b/tailscale-network.tf new file mode 100644 index 0000000..dd25155 --- /dev/null +++ b/tailscale-network.tf @@ -0,0 +1,24 @@ +# configures tailscale network to use the relay server. + +resource "tailscale_acl" "default" { + acl = templatefile("${path.module}/files/acl.hujson.tftpl", { + admins = var.tailscale_admin_users + domain = var.tailscale_domain + tag = var.relay_tag + routes = local.tailscale_routes + enable_ssh = var.enable_tailscale_ssh + }) +} + +resource "tailscale_tailnet_key" "relay_auth" { + preauthorized = true + reusable = true + ephemeral = false + tags = [var.relay_tag] + depends_on = [tailscale_acl.default] +} + +resource "tailscale_dns_nameservers" "vpc_dns" { + count = var.advertise_nameservers ? 1 : 0 + nameservers = local.tailscale_nameservers +} diff --git a/tailscale-server.tf b/tailscale-server.tf new file mode 100644 index 0000000..0a70a1f --- /dev/null +++ b/tailscale-server.tf @@ -0,0 +1,54 @@ +# deploys a tailscale relay server EC2 instance in AWS VPC. + +# module "ec2_instance" { +# source = "terraform-aws-modules/ec2-instance/aws" +# version = "~> 3.0" +# create = true +# name = var.tailscale_relay_name +# ami = +# +# } + +resource "aws_instance" "tailscale" { + ami = data.aws_ami.selected.id + instance_type = var.relay_instance_type + associate_public_ip_address = var.relay_associate_public_ip + key_name = var.relay_key_name + subnet_id = var.subnet_id + vpc_security_group_ids = [aws_security_group.tailscale.id] + user_data = templatefile("${path.module}/files/relay-init.sh.tftpl", { + routes = local.tailscale_routes + auth_key = tailscale_tailnet_key.relay_auth.key + }) + tags = { + Name = "tailscale" + } +} + +resource "aws_security_group" "tailscale" { + name_prefix = "tailscale" + vpc_id = var.vpc_id + + dynamic "ingress" { + for_each = (var.relay_key_name == null || var.relay_key_name == "") ? [] : [1] + content { + from_port = 22 + to_port = 22 + protocol = "tcp" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + } + + egress { + from_port = 0 + to_port = 0 + protocol = "-1" + cidr_blocks = ["0.0.0.0/0"] + ipv6_cidr_blocks = ["::/0"] + } + + lifecycle { + create_before_destroy = true + } +} diff --git a/terraform.tf b/terraform.tf new file mode 100644 index 0000000..1043930 --- /dev/null +++ b/terraform.tf @@ -0,0 +1,41 @@ +terraform { + required_version = "~> 1.2.0" + + required_providers { + tailscale = { + source = "davidsbond/tailscale" + version = "~> 0.12.0" + } + aws = { + source = "hashicorp/aws" + version = "~> 4.26.0" + } + http = { + source = "hashicorp/http" + version = "~> 3.0.1" + } + # null = { + # source = "hashicorp/null" + # version = ">= 3.1.1" + # } + # time = { + # source = "hashicorp/time" + # version = "~> 0.8.0" + # } + } +} + +provider "aws" { + region = var.aws_region + default_tags { + tags = { + ManagedBy = "terraform" + Component = "tailscale" + } + } +} + +provider "tailscale" { + api_key = var.tailscale_api_key + tailnet = var.tailscale_domain +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..2ad6a88 --- /dev/null +++ b/variables.tf @@ -0,0 +1,106 @@ +variable "tailscale_domain" { + type = string + default = "example.net" + description = "The domain name of the tailscale network to manage." +} + +variable "tailscale_admin_users" { + type = list(string) + default = ["admin"] + description = "usernames of the tailscale network's admins, minus the `@domain` part." +} + +variable "tailscale_api_key" { + type = string + default = "tskey-XXXXXXXXXXXX-XXXXXXXXXXXXXXXXXXXXXX" + sensitive = true + description = "The tailscale API key to use." + validation { + condition = can(regex("^tskey-", var.tailscale_api_key)) + error_message = "The tailscale API key must start with `tskey-`" + } +} + +variable "relay_tag" { + type = string + default = "tag:tailscale" + description = "The tag to use for the tailscale network's relay nodes." + validation { + condition = can(regex("^tag:\\w+", var.relay_tag)) + error_message = "tailscale tags must start with `tag:` followed by a tag name." + } +} + +variable "relay_instance_type" { + type = string + default = "t2.micro" + description = "The EC2 instance type to use for the relay server." +} + +variable "relay_key_name" { + type = string + default = "default" + description = "The name of the pre-existing key pair to use for ssh access to the relay server." +} + +variable "aws_region" { + type = string + default = "us-east-1" + description = "The AWS region to use." +} + +variable "vpc_id" { + type = string + default = "vpc-XXXXXXXXXXXXXXXXXXXX" + description = "ID of the vpc to deploy tailscale relay to." +} + +variable "subnet_id" { + type = string + default = "subnet-XXXXXXXXXXXXXXXXXXXX" + description = "ID of the subnet to attach tailscale relay to." +} + +variable "additional_routes" { + type = list(string) + default = [] + description = "The routes in addition to selected VPC's routes, to add to the tailscale network." + validation { + condition = length(var.additional_routes) == 0 ? true : alltrue([ + for route in var.additional_routes : + regex("^\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}/\\d{1,2}$", route) + ]) + error_message = "routes must be in CIDR format." + } +} + +variable "fallback_nameservers" { + type = list(string) + # default = ["169.254.169.253", "1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4"] + default = ["1.1.1.1", "1.0.0.1", "8.8.8.8", "8.8.4.4"] + description = "additional nameservers to push to the tailscale network." +} + +variable "advertise_nameservers" { + type = bool + default = true + description = "Whether to advertise the tailscale network's nameservers to clients." +} + +variable "advertise_routes" { + type = bool + default = true + description = "Whether to advertise the tailscale server's subnet routes to clients." +} + +variable "enable_tailscale_ssh" { + type = bool + default = true + description = "Whether to enable ssh-over-tailscale for tailscale network nodes." +} + +variable "relay_associate_public_ip" { + type = bool + default = true + description = "Whether to associate a public IP address with the relay server." +}