From 8447502d54335562f5f5fa96a0fd2026d932ff79 Mon Sep 17 00:00:00 2001 From: Khue Doan Date: Sat, 27 Aug 2022 13:24:57 +0700 Subject: [PATCH] feat: add ZeroTier for remote access - Fully open source - Has free hosted version (my.zerotier.com) - Can be automated with Terraform - Pretty good performance with UDP hole punching --- README.md | 6 ++ .../production/external-resources.md | 36 +++++++--- external/main.tf | 8 +++ external/modules/zerotier/main.tf | 53 +++++++++++++++ external/modules/zerotier/variables.tf | 34 ++++++++++ external/modules/zerotier/versions.tf | 13 ++++ external/namespaces.yml | 1 + external/terraform.tfvars.j2 | 1 + external/tfvars.yml | 2 + external/variables.tf | 4 ++ platform/zerotier/deployment.yaml | 65 +++++++++++++++++++ platform/zerotier/kustomization.yaml | 5 ++ 12 files changed, 220 insertions(+), 8 deletions(-) create mode 100644 external/modules/zerotier/main.tf create mode 100644 external/modules/zerotier/variables.tf create mode 100644 external/modules/zerotier/versions.tf create mode 100644 platform/zerotier/deployment.yaml create mode 100644 platform/zerotier/kustomization.yaml diff --git a/README.md b/README.md index 5f8a0d80..d0d69e3f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ More information can be found in [the roadmap](#roadmap) below. - [x] Modular architecture, easy to add or remove features/components - [x] Automated certificate management - [x] Automatically update DNS records for exposed services +- [x] VPN - [x] Expose services to the internet securely with [Cloudflare Tunnel](https://www.cloudflare.com/products/tunnel/) - [x] CI/CD platform - [x] Private container registry @@ -195,6 +196,11 @@ They can't capture all the project's features, but they are sufficient to get a Vault Secrets and encryption management system + + + ZeroTier + VPN without port-forwarding + ## Get Started diff --git a/docs/installation/production/external-resources.md b/docs/installation/production/external-resources.md index 41ad1432..ea870e03 100644 --- a/docs/installation/production/external-resources.md +++ b/docs/installation/production/external-resources.md @@ -7,11 +7,12 @@ Although I try to keep the amount of external resources to the minimum, there's still need for a few of them. Below is a list of external resources and why we need them (also see some [alternatives](#alternatives) below). -| Provider | Resource | Purpose | -| -------- | -------- | ------- | -| Terraform Cloud | Workspace | Terraform state backend | -| Cloudflare | DNS | DNS and [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) for certificates | -| Cloudflare | Tunnel | Public services to the internet without port-forwarding | +| Provider | Resource | Purpose | +| -------- | -------- | ------- | +| Terraform Cloud | Workspace | Terraform state backend | +| Cloudflare | DNS | DNS and [DNS-01 challenge](https://letsencrypt.org/docs/challenge-types/#dns-01-challenge) for certificates | +| Cloudflare | Tunnel | Public services to the internet without port-forwarding | +| ZeroTier | Virtual network | Use as VPN to access home network from anywhere (with [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching)) | @@ -49,6 +50,11 @@ If you decide to use a [different Terraform backend](https://www.terraform.io/la +### ZeroTier + +- Create a ZeroTier account +- Generate a new API Token at + @@ -59,7 +65,21 @@ If you decide to use a [different Terraform backend](https://www.terraform.io/la ## Alternatives -- Terraform Cloud: any other [Terraform backends](https://www.terraform.io/language/settings/backends) -- Cloudflare DNS: see [manual DNS setup](../../tutorials/manual-dns-setup.md) -- Cloudflare Tunnel: you can create a small VPS in the cloud and utilize Wireguard and HAProxy to route traffic via it, or just use simple port-forwarding if it's available (see also [awesome tunneling](https://github.com/anderspitman/awesome-tunneling)) +To avoid vendor lock-in, each external provider must have an equivalent alternative that is easy to replace: + +- Terraform Cloud: + - Any other [Terraform backends](https://www.terraform.io/language/settings/backends) +- Cloudflare DNS: + - Update cert-manager and external-dns to use a different provider + - [Manual DNS setup](../../tutorials/manual-dns-setup.md) +- Cloudflare Tunnel: + - Use port-forwarding if it's available + - Create a small VPS in the cloud and utilize Wireguard and HAProxy to route traffic via it + - Access everything via VPN + - See also [awesome tunneling](https://github.com/anderspitman/awesome-tunneling) +- ZeroTier virtual network: + - [Host your own ZeroTier](https://docs.zerotier.com/self-hosting/introduction) + - [Tailscale](https://tailscale.com) (closed source, but you can use [Headscale](https://github.com/juanfont/headscale) to host your own Tailscale control server) + - [Netmaker](https://www.netmaker.org) (there's no hosted version, you'll need to host your own server) + - Wireguard server (requires port-forwarding) diff --git a/external/main.tf b/external/main.tf index af4baff0..cfe169fa 100644 --- a/external/main.tf +++ b/external/main.tf @@ -4,3 +4,11 @@ module "cloudflare" { cloudflare_email = var.cloudflare_email cloudflare_api_key = var.cloudflare_api_key } + +module "zerotier" { + source = "./modules/zerotier" + zerotier_central_token = var.zerotier_central_token + bridged_routes = [ + "192.168.1.0/24" # TODO add this to configure script + ] +} diff --git a/external/modules/zerotier/main.tf b/external/modules/zerotier/main.tf new file mode 100644 index 00000000..5f1d83fb --- /dev/null +++ b/external/modules/zerotier/main.tf @@ -0,0 +1,53 @@ +locals { + router_ip = cidrhost(var.managed_route, 1) # Use the second IP in the VPN subnet as the router +} + +resource "zerotier_network" "network" { + name = var.name + description = var.description + private = true + + route { + target = var.managed_route + } + + dynamic "route" { + for_each = var.bridged_routes + + content { + target = route.value + via = local.router_ip + } + } + + assignment_pool { + start = cidrhost(var.managed_route, 0) + end = cidrhost(var.managed_route, -1) + } +} + +resource "zerotier_identity" "router" {} + +resource "zerotier_member" "router" { + network_id = zerotier_network.network.id + name = "router" + member_id = zerotier_identity.router.id + allow_ethernet_bridging = true + no_auto_assign_ips = true + ip_assignments = [ + local.router_ip + ] +} + +resource "kubernetes_secret" "router" { + metadata { + name = "zerotier-router" + namespace = "zerotier" + } + + data = { + ZEROTIER_NETWORK_ID = zerotier_network.network.id + ZEROTIER_IDENTITY_PUBLIC = zerotier_identity.router.public_key + ZEROTIER_IDENTITY_SECRET = zerotier_identity.router.private_key + } +} diff --git a/external/modules/zerotier/variables.tf b/external/modules/zerotier/variables.tf new file mode 100644 index 00000000..e7a2bc27 --- /dev/null +++ b/external/modules/zerotier/variables.tf @@ -0,0 +1,34 @@ +variable "zerotier_central_url" { + description = "ZeroTier Central API endpoint" + type = string + default = "https://my.zerotier.com/api" # https://github.com/zerotier/go-ztcentral/blob/4d397d1e82c043a5376789177ad55536044d69ce/client.go#L44 +} + +variable "zerotier_central_token" { + description = "ZeroTier Central API Token" + type = string + sensitive = true +} + +variable "name" { + description = "Network name" + type = string + default = "homelab" +} + +variable "description" { + description = "Network description" + type = string + default = "Homelab network" +} + +variable "managed_route" { + description = "ZeroTier managed route" + type = string + default = "10.147.17.0/24" +} + +variable "bridged_routes" { + description = "List of bridged routes" # TODO + type = list(string) +} diff --git a/external/modules/zerotier/versions.tf b/external/modules/zerotier/versions.tf new file mode 100644 index 00000000..3c0e3784 --- /dev/null +++ b/external/modules/zerotier/versions.tf @@ -0,0 +1,13 @@ +terraform { + required_providers { + zerotier = { + source = "zerotier/zerotier" + version = "~> 1.2.0" + } + } +} + +provider "zerotier" { + zerotier_central_url = var.zerotier_central_url + zerotier_central_token = var.zerotier_central_token +} diff --git a/external/namespaces.yml b/external/namespaces.yml index 1d398ccd..2932ee90 100644 --- a/external/namespaces.yml +++ b/external/namespaces.yml @@ -12,3 +12,4 @@ - external-dns - k8up-operator - tekton-pipelines + - zerotier diff --git a/external/terraform.tfvars.j2 b/external/terraform.tfvars.j2 index 80a070ee..0e761dae 100644 --- a/external/terraform.tfvars.j2 +++ b/external/terraform.tfvars.j2 @@ -1,3 +1,4 @@ cloudflare_email = "{{ cloudflare_email }}" cloudflare_api_key = "{{ cloudflare_api_key }}" cloudflare_account_id = "{{ cloudflare_account_id }}" +zerotier_central_token = "{{ zerotier_central_token }}" diff --git a/external/tfvars.yml b/external/tfvars.yml index 6f763c72..1c42d732 100644 --- a/external/tfvars.yml +++ b/external/tfvars.yml @@ -9,6 +9,8 @@ - name: cloudflare_account_id prompt: Enter Cloudflare account ID private: false + - name: zerotier_central_token + prompt: Enter ZeroTier Central API Token tasks: - name: Render environment file template: diff --git a/external/variables.tf b/external/variables.tf index c5a4b342..2ad44210 100644 --- a/external/variables.tf +++ b/external/variables.tf @@ -10,3 +10,7 @@ variable "cloudflare_api_key" { variable "cloudflare_account_id" { type = string } + +variable "zerotier_central_token" { + type = string +} diff --git a/platform/zerotier/deployment.yaml b/platform/zerotier/deployment.yaml new file mode 100644 index 00000000..a3c89d5d --- /dev/null +++ b/platform/zerotier/deployment.yaml @@ -0,0 +1,65 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: zerotier-router + namespace: zerotier + labels: + app: zerotier-router +spec: + selector: + matchLabels: + app: zerotier-router + replicas: 1 + template: + metadata: + labels: + app: zerotier-router + spec: + containers: + - name: zerotier-router + image: zerotier/zerotier:latest # TODO use tag + command: + - "sh" + - "-c" + args: # TODO optimize this + - | + # TODO install this on upstream image? + apt-get install -y iptables + + # TODO is there a better way to get the interface name? + export PHY_IFACE="$(ip route | grep ${POD_IP} | cut -d ' ' -f 3)" + export ZT_IFACE=zt0 + + # Override the default random interface name + mkdir -p /var/lib/zerotier-one + echo "${ZEROTIER_NETWORK_ID}=${ZT_IFACE}" >> /var/lib/zerotier-one/devicemap + + iptables -t nat -A POSTROUTING -o $PHY_IFACE -j MASQUERADE + iptables -A FORWARD -i $PHY_IFACE -o $ZT_IFACE -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A FORWARD -i $ZT_IFACE -o $PHY_IFACE -j ACCEPT + + /entrypoint.sh "${ZEROTIER_NETWORK_ID}" + resources: + requests: + cpu: 100m + memory: 128Mi + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + envFrom: + - secretRef: + name: zerotier-router + securityContext: + capabilities: + add: + - NET_ADMIN + volumeMounts: + - name: dev-net-tun + mountPath: /dev/net/tun + volumes: + - name: dev-net-tun + hostPath: + path: /dev/net/tun + type: CharDevice diff --git a/platform/zerotier/kustomization.yaml b/platform/zerotier/kustomization.yaml new file mode 100644 index 00000000..88a04b54 --- /dev/null +++ b/platform/zerotier/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - deployment.yaml