WireGuard VPN on GCP with Terraform
- Authors
- Name
- upvpn LLC
- @upvpnapp
Table of Contents
Introduction
This guide will walk you through setting up a WireGuard VPN server on Google Cloud Platform using Terraform. We'll create a complete infrastructure with IPv4 and IPv6 support, automated client configuration generation, and proper security settings.
Overview
We'll create:
- GCP instance with public IPv4 and IPv6 running WireGuard
- SSH key pair for secure access
- Network, dual-stack Subnetwork, and Firewall rules for VPN and SSH traffic
- WireGuard server and client configurations
- Configurable number of clients.
- Cloud-init template for automated server setup
Project Structure
.
├── main.tf
├── variables.tf
├── outputs.tf
├── versions.tf
├── cloud-init.yaml.tpl
└── client-config.tpl
Implementation Steps
1. Provider and Version Configuration
First, let's set up the required providers and versions:
# versions.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.12.0"
}
// For generating WireGuard key pairs
wireguard = {
source = "OJFord/wireguard"
version = "0.3.1"
}
// For generating SSH key
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
}
required_version = ">= 1.0.0"
}
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}
provider "wireguard" {}
2. Variables Definition
Define the necessary variables:
# variables.tf
variable "project_id" {
description = "GCP Project ID"
type = string
}
variable "region" {
description = "GCP region"
type = string
default = "us-central1"
}
variable "zone" {
description = "GCP zone"
type = string
default = "us-central1-a"
}
variable "machine_type" {
description = "GCP machine type"
type = string
default = "e2-micro"
}
variable "image" {
description = "OS project and family "
default = "ubuntu-os-cloud/ubuntu-minimal-2404-lts-amd64"
}
variable "client_count" {
description = "Number of WireGuard clients to create"
type = number
default = 1
}
variable "gcp_subnetwork_cidr" {
description = "IPv4 CIDR for GCP subnetwork where instance will be hosted"
default = "10.240.0.0/24"
}
variable "vpn_ipv4_cidr" {
description = "IPv4 CIDR for WireGuard VPN"
type = string
default = "10.10.0.0/24"
}
variable "vpn_ipv6_cidr" {
description = "IPv6 CIDR for WireGuard VPN"
type = string
default = "fd00:10:10::/64"
}
variable "use_ipv6_endpoint" {
description = "Use IPv6 endpoint in client config, otherwise use IPv4"
default = true
}
variable "interface" {
description = "Default network interface on the instance"
default = "ens4"
}
3. SSH Key Generation
Generate an SSH key pair for the instance:
# main.tf
resource "tls_private_key" "ssh" {
algorithm = "RSA"
rsa_bits = 4096
}
4. WireGuard Key Generation
Generate server and client keys:
# main.tf
resource "wireguard_asymmetric_key" "server" {}
resource "wireguard_asymmetric_key" "clients" {
count = var.client_count
}
5. Cloud-init Template
Create a cloud-init template for automated server setup:
# cloud-init.yaml.tpl
#cloud-config
package_update: true
package_upgrade: false
packages:
- wireguard
- ufw
write_files:
- path: /etc/wireguard/wg0.conf
content: |
[Interface]
Address = ${server_ipv4_address},${server_ipv6_address}
PrivateKey = ${server_private_key}
ListenPort = 51820
PostUp = ufw route allow in on wg0 out on ${interface}
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o ${interface} -j MASQUERADE
PostUp = ip6tables -A FORWARD -i wg0 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o ${interface} -j MASQUERADE
PostDown = ufw route delete allow in on wg0 out on ${interface}
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o ${interface} -j MASQUERADE
PostDown = ip6tables -D FORWARD -i wg0 -j ACCEPT
PostDown = ip6tables -t nat -D POSTROUTING -o ${interface} -j MASQUERADE
${peer_configs}
owner: root:root
permissions: '0600'
runcmd:
- ufw allow 51820/udp
- ufw allow OpenSSH
- echo "y" | ufw enable
- echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
- echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
- sysctl -p
- systemctl enable wg-quick@wg0
- systemctl start wg-quick@wg0
6. Client Configuration Template
Create a template for client configurations:
# client-config.tpl
[Interface]
PrivateKey = ${client_private_key}
Address = ${client_ipv4_address},${client_ipv6_address}
DNS = 1.1.1.1, 2606:4700:4700::1111
[Peer]
PublicKey = ${server_public_key}
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = ${server_public_ip}:51820
PersistentKeepalive = 25
7. Create Network Infrastructure
Create Network and Subnetwork with both IPv4 and IPv6 capabilities:
# main.tf
# VPC Network
resource "google_compute_network" "vpn_network" {
name = "wireguard-network"
auto_create_subnetworks = false
}
# Subnet
resource "google_compute_subnetwork" "vpn_subnet" {
name = "wireguard-subnet"
ip_cidr_range = var.gcp_subnetwork_cidr
network = google_compute_network.vpn_network.id
region = var.region
stack_type = "IPV4_IPV6"
ipv6_access_type = "EXTERNAL"
}
# Firewall rules
resource "google_compute_firewall" "wireguard_udp" {
name = "allow-wireguard"
network = google_compute_network.vpn_network.name
allow {
protocol = "udp"
ports = ["51820"]
}
source_ranges = ["0.0.0.0/0"]
}
resource "google_compute_firewall" "ssh" {
name = "allow-ssh"
network = google_compute_network.vpn_network.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
}
resource "google_compute_firewall" "wireguard_udp_ipv6" {
name = "allow-wireguard-ipv6"
network = google_compute_network.vpn_network.name
allow {
protocol = "udp"
ports = ["51820"]
}
source_ranges = ["::/0"]
}
resource "google_compute_firewall" "ssh_ipv6" {
name = "allow-ssh-ipv6"
network = google_compute_network.vpn_network.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["::/0"]
}
8. Create Instance
Create the instance:
# main.tf
# Calculate IP addresses for server and clients
locals {
server_ipv4_address = cidrhost(var.vpn_ipv4_cidr, 1)
server_ipv6_address = cidrhost(var.vpn_ipv6_cidr, 1)
client_configs = [
for idx in range(var.client_count) : {
ipv4_address = cidrhost(var.vpn_ipv4_cidr, idx + 2)
ipv6_address = cidrhost(var.vpn_ipv6_cidr, idx + 2)
private_key = wireguard_asymmetric_key.clients[idx].private_key
public_key = wireguard_asymmetric_key.clients[idx].public_key
}
]
# Indent string to keep cloud-init.yaml well formatted after interpolation
peer_configs = indent(6, join("\n", [
for client in local.client_configs : <<-EOT
[Peer]
PublicKey = ${client.public_key}
AllowedIPs = ${client.ipv4_address}/32,${client.ipv6_address}/128
EOT
]))
}
# Cloud-init template
data "template_file" "cloud_init" {
template = file("cloud-init.yaml.tpl")
vars = {
server_private_key = wireguard_asymmetric_key.server.private_key
server_ipv4_address = "${local.server_ipv4_address}/32"
server_ipv6_address = "${local.server_ipv6_address}/128"
peer_configs = local.peer_configs
interface = var.interface
}
}
# Compute instance
resource "google_compute_instance" "wireguard" {
name = "wireguard-vpn"
machine_type = var.machine_type
zone = var.zone
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
stack_type = "IPV4_IPV6"
subnetwork = google_compute_subnetwork.vpn_subnet.id
access_config {}
ipv6_access_config {
network_tier = "PREMIUM"
}
}
metadata = {
ssh-keys = "ubuntu:${tls_private_key.ssh.public_key_openssh}"
user-data = data.template_file.cloud_init.rendered
}
can_ip_forward = true
tags = ["wireguard-vpn"]
}
9. Output Configurations
Generate output for client configurations and SSH key:
# outputs.tf
locals {
ipv6_address = google_compute_instance.wireguard.network_interface[0].ipv6_access_config[0].external_ipv6
ipv4_address = google_compute_instance.wireguard.network_interface[0].access_config[0].nat_ip
}
# Creates ready to use local files for client configurations.
# Generate QR code on Linux:
# cat clientN.conf | qrencode -t ansiutf8
resource "local_sensitive_file" "client_configs" {
count = var.client_count
content = templatefile("client-config.tpl", {
client_private_key = wireguard_asymmetric_key.clients[count.index].private_key
client_ipv4_address = local.client_configs[count.index].ipv4_address
client_ipv6_address = local.client_configs[count.index].ipv6_address
server_public_key = wireguard_asymmetric_key.server.public_key
server_public_ip = (var.use_ipv6_endpoint ?
"[${local.ipv6_address}]" :
local.ipv4_address)
})
filename = "client${count.index + 1}.conf"
file_permission = "0600"
}
# Get ssh private key `terraform output -raw ssh_private_key > wireguard-ssh-key`
# chmod 400 wireguard-ssh-key
# ssh -i wireguard-ssh-key ubuntu@<ip-address>
output "ssh_private_key" {
value = tls_private_key.ssh.private_key_openssh
sensitive = true
}
output "server_public_ipv4" {
value = local.ipv4_address
}
output "server_public_ipv6" {
value = local.ipv6_address
}
Usage Instructions
- Setup
gcloud
- Install
gcloud
: https://cloud.google.com/sdk/docs/install - Login:
gcloud auth application-default login
- Create
terraform.tfvars
withproject_id
project_id = "your-gcp-project-id"
After this hashicorp/google
Terraform provider can use the credentials file created by login.
- Initialize and apply the Terraform configuration:
terraform init
terraform apply
After instance creation, wait for cloud init to complete the setup.
- Connect Client
Client configurations will be generated in the current directory as client1.conf
, client2.conf
, etc. For mobile devices, create QR code:
cat client1.conf | qrencode -t ansiutf8
- SSH into the server:
# Save the SSH private key
terraform output -raw ssh_private_key > wireguard-ssh-key
chmod 400 wireguard-ssh-key
# Connect to the server
ssh -i wireguard-ssh-key ubuntu@$(terraform output -raw server_public_ipv4)
Complete Terraform Configuration
Here's all the code in a single block:
# versions.tf
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = "~> 6.12.0"
}
// For generating WireGuard key pairs
wireguard = {
source = "OJFord/wireguard"
version = "0.3.1"
}
// For generating SSH key
tls = {
source = "hashicorp/tls"
version = "~> 4.0"
}
}
required_version = ">= 1.0.0"
}
provider "google" {
project = var.project_id
region = var.region
zone = var.zone
}
provider "wireguard" {}
# variables.tf
variable "project_id" {
description = "GCP Project ID"
type = string
}
variable "region" {
description = "GCP region"
type = string
default = "us-central1"
}
variable "zone" {
description = "GCP zone"
type = string
default = "us-central1-a"
}
variable "machine_type" {
description = "GCP machine type"
type = string
default = "e2-micro"
}
variable "image" {
description = "OS project and family "
default = "ubuntu-os-cloud/ubuntu-minimal-2404-lts-amd64"
}
variable "client_count" {
description = "Number of WireGuard clients to create"
type = number
default = 1
}
variable "gcp_subnetwork_cidr" {
description = "IPv4 CIDR for GCP subnetwork where instance will be hosted"
default = "10.240.0.0/24"
}
variable "vpn_ipv4_cidr" {
description = "IPv4 CIDR for WireGuard VPN"
type = string
default = "10.10.0.0/24"
}
variable "vpn_ipv6_cidr" {
description = "IPv6 CIDR for WireGuard VPN"
type = string
default = "fd00:10:10::/64"
}
variable "use_ipv6_endpoint" {
description = "Use IPv6 endpoint in client config, otherwise use IPv4"
default = true
}
variable "interface" {
description = "Default network interface on the instance"
default = "ens4"
}
# main.tf
# SSH Key Generation
resource "tls_private_key" "ssh" {
algorithm = "RSA"
rsa_bits = 4096
}
# WireGuard Key Generation
resource "wireguard_asymmetric_key" "server" {}
resource "wireguard_asymmetric_key" "clients" {
count = var.client_count
}
# Create Network Infrastructure
# VPC Network
resource "google_compute_network" "vpn_network" {
name = "wireguard-network"
auto_create_subnetworks = false
}
# Subnet
resource "google_compute_subnetwork" "vpn_subnet" {
name = "wireguard-subnet"
ip_cidr_range = var.gcp_subnetwork_cidr
network = google_compute_network.vpn_network.id
region = var.region
stack_type = "IPV4_IPV6"
ipv6_access_type = "EXTERNAL"
}
# Firewall rules
resource "google_compute_firewall" "wireguard_udp" {
name = "allow-wireguard"
network = google_compute_network.vpn_network.name
allow {
protocol = "udp"
ports = ["51820"]
}
source_ranges = ["0.0.0.0/0"]
}
resource "google_compute_firewall" "ssh" {
name = "allow-ssh"
network = google_compute_network.vpn_network.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["0.0.0.0/0"]
}
resource "google_compute_firewall" "wireguard_udp_ipv6" {
name = "allow-wireguard-ipv6"
network = google_compute_network.vpn_network.name
allow {
protocol = "udp"
ports = ["51820"]
}
source_ranges = ["::/0"]
}
resource "google_compute_firewall" "ssh_ipv6" {
name = "allow-ssh-ipv6"
network = google_compute_network.vpn_network.name
allow {
protocol = "tcp"
ports = ["22"]
}
source_ranges = ["::/0"]
}
# Calculate IP addresses for server and clients
locals {
server_ipv4_address = cidrhost(var.vpn_ipv4_cidr, 1)
server_ipv6_address = cidrhost(var.vpn_ipv6_cidr, 1)
client_configs = [
for idx in range(var.client_count) : {
ipv4_address = cidrhost(var.vpn_ipv4_cidr, idx + 2)
ipv6_address = cidrhost(var.vpn_ipv6_cidr, idx + 2)
private_key = wireguard_asymmetric_key.clients[idx].private_key
public_key = wireguard_asymmetric_key.clients[idx].public_key
}
]
# Indent string to keep cloud-init.yaml well formatted after interpolation
peer_configs = indent(6, join("\n", [
for client in local.client_configs : <<-EOT
[Peer]
PublicKey = ${client.public_key}
AllowedIPs = ${client.ipv4_address}/32,${client.ipv6_address}/128
EOT
]))
}
# Cloud-init template
data "template_file" "cloud_init" {
template = file("cloud-init.yaml.tpl")
vars = {
server_private_key = wireguard_asymmetric_key.server.private_key
server_ipv4_address = "${local.server_ipv4_address}/32"
server_ipv6_address = "${local.server_ipv6_address}/128"
peer_configs = local.peer_configs
interface = var.interface
}
}
# Compute instance
resource "google_compute_instance" "wireguard" {
name = "wireguard-vpn"
machine_type = var.machine_type
zone = var.zone
boot_disk {
initialize_params {
image = var.image
}
}
network_interface {
stack_type = "IPV4_IPV6"
subnetwork = google_compute_subnetwork.vpn_subnet.id
access_config {}
ipv6_access_config {
network_tier = "PREMIUM"
}
}
metadata = {
ssh-keys = "ubuntu:${tls_private_key.ssh.public_key_openssh}"
user-data = data.template_file.cloud_init.rendered
}
can_ip_forward = true
tags = ["wireguard-vpn"]
}
# outputs.tf
locals {
ipv6_address = google_compute_instance.wireguard.network_interface[0].ipv6_access_config[0].external_ipv6
ipv4_address = google_compute_instance.wireguard.network_interface[0].access_config[0].nat_ip
}
# Creates ready to use local files for client configurations.
# Generate QR code on Linux:
# cat clientN.conf | qrencode -t ansiutf8
resource "local_sensitive_file" "client_configs" {
count = var.client_count
content = templatefile("client-config.tpl", {
client_private_key = wireguard_asymmetric_key.clients[count.index].private_key
client_ipv4_address = local.client_configs[count.index].ipv4_address
client_ipv6_address = local.client_configs[count.index].ipv6_address
server_public_key = wireguard_asymmetric_key.server.public_key
server_public_ip = (var.use_ipv6_endpoint ?
"[${local.ipv6_address}]" :
local.ipv4_address)
})
filename = "client${count.index + 1}.conf"
file_permission = "0600"
}
# Get ssh private key `terraform output -raw ssh_private_key > wireguard-ssh-key`
# chmod 400 wireguard-ssh-key
# ssh -i wireguard-ssh-key ubuntu@<ip-address>
output "ssh_private_key" {
value = tls_private_key.ssh.private_key_openssh
sensitive = true
}
output "server_public_ipv4" {
value = local.ipv4_address
}
output "server_public_ipv6" {
value = local.ipv6_address
}
# cloud-init.yaml.tpl
#cloud-config
package_update: true
package_upgrade: false
packages:
- wireguard
- ufw
write_files:
- path: /etc/wireguard/wg0.conf
content: |
[Interface]
Address = ${server_ipv4_address},${server_ipv6_address}
PrivateKey = ${server_private_key}
ListenPort = 51820
PostUp = ufw route allow in on wg0 out on ${interface}
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o ${interface} -j MASQUERADE
PostUp = ip6tables -A FORWARD -i wg0 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o ${interface} -j MASQUERADE
PostDown = ufw route delete allow in on wg0 out on ${interface}
PostDown = iptables -D FORWARD -i wg0 -j ACCEPT
PostDown = iptables -t nat -D POSTROUTING -o ${interface} -j MASQUERADE
PostDown = ip6tables -D FORWARD -i wg0 -j ACCEPT
PostDown = ip6tables -t nat -D POSTROUTING -o ${interface} -j MASQUERADE
${peer_configs}
owner: root:root
permissions: '0600'
runcmd:
- ufw allow 51820/udp
- ufw allow OpenSSH
- echo "y" | ufw enable
- echo "net.ipv4.ip_forward=1" >> /etc/sysctl.conf
- echo "net.ipv6.conf.all.forwarding=1" >> /etc/sysctl.conf
- sysctl -p
- systemctl enable wg-quick@wg0
- systemctl start wg-quick@wg0
# client-config.tpl
[Interface]
PrivateKey = ${client_private_key}
Address = ${client_ipv4_address},${client_ipv6_address}
DNS = 1.1.1.1, 2606:4700:4700::1111
[Peer]
PublicKey = ${server_public_key}
AllowedIPs = 0.0.0.0/0, ::/0
Endpoint = ${server_public_ip}:51820
PersistentKeepalive = 25