WireGuard VPN on Vultr with Terraform
- Authors
- Name
- upvpn LLC
- @upvpnapp
Introduction
This guide will walk you through setting up a WireGuard VPN server on Vultr using Terraform. We'll create a complete infrastructure with IPv4 and IPv6 support, automated client configuration generation, and proper security settings.
You can also obtain Complete Terraform Configuration
Overview
We'll create:
- A Vultr instance with public IPv4 and IPv6 running WireGuard
- SSH key pair for secure access
- 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 {
vultr = {
source = "vultr/vultr"
version = "~> 2.22"
}
// 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 "vultr" {
api_key = var.vultr_api_key
}
provider "wireguard" {}
2. Variables Definition
Define the necessary variables:
# variables.tf
variable "vultr_api_key" {
description = "Vultr API Key"
type = string
sensitive = true
}
variable "region" {
description = "Vultr region"
type = string
default = "sea"
}
variable "plan" {
description = "Vultr plan ID"
type = string
default = "vc2-1c-1gb"
}
variable "os_id" {
description = "Vultr OS ID"
# 2284 Ubuntu 24.04 LTS x64 x64 ubuntu
default = "2284"
}
variable "client_count" {
description = "Number of WireGuard clients to create"
type = number
default = 1
}
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 = "enp1s0"
}
3. SSH Key Generation
Generate an SSH key pair for the instance:
# main.tf
resource "tls_private_key" "ssh" {
algorithm = "ED25519"
}
resource "vultr_ssh_key" "vpn" {
name = "wireguard-vpn-key"
ssh_key = tls_private_key.ssh.public_key_openssh
}
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. Main Infrastructure
Create the Instance and Firewall:
# 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
]))
}
# Create firewall group
resource "vultr_firewall_group" "vpn" {
description = "WireGuard VPN Firewall"
}
# Allow SSH
resource "vultr_firewall_rule" "ssh" {
firewall_group_id = vultr_firewall_group.vpn.id
protocol = "tcp"
ip_type = "v4"
subnet = "0.0.0.0"
subnet_size = 0
port = "22"
}
# Allow WireGuard
resource "vultr_firewall_rule" "wireguard" {
firewall_group_id = vultr_firewall_group.vpn.id
protocol = "udp"
ip_type = "v4"
subnet = "0.0.0.0"
subnet_size = 0
port = "51820"
}
# Allow IPv6 WireGuard
resource "vultr_firewall_rule" "wireguard_ipv6" {
firewall_group_id = vultr_firewall_group.vpn.id
protocol = "udp"
ip_type = "v6"
subnet = "::"
subnet_size = 0
port = "51820"
}
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
}
}
# Create Vultr instance
resource "vultr_instance" "wireguard" {
plan = var.plan
region = var.region
os_id = var.os_id
tags = ["wireguard", "vpn"]
enable_ipv6 = true
firewall_group_id = vultr_firewall_group.vpn.id
ssh_key_ids = [vultr_ssh_key.vpn.id]
user_data = data.template_file.cloud_init.rendered
}
8. Output Configurations
Generate output for client configurations:
# outputs.tf
# Creates ready to use local files for client configurations.
# Generate QR code on Linux:
# cat clientN.conf | qrencode -t ansiutf8
resource "local_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 ?
"[${vultr_instance.wireguard.v6_main_ip}]" :
vultr_instance.wireguard.main_ip)
})
filename = "client${count.index + 1}.conf"
}
# Get ssh private key `terraform output -raw ssh_private_key > wireguard_ssh_key`
# chmod 400 wireguard_ssh_key
# ssh -i wireguard_ssh_key root@<ip-address>
output "ssh_private_key" {
value = tls_private_key.ssh.private_key_openssh
sensitive = true
}
output "server_public_ipv4" {
value = vultr_instance.wireguard.main_ip
}
output "server_public_ipv6" {
value = vultr_instance.wireguard.v6_main_ip
}
Usage Instructions
- Create a
terraform.tfvars
file with your Vultr API Key:
vultr_api_key = "your-api-key-here"
- Initialize and apply the Terraform configuration:
terraform init
terraform apply
- 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 root@$(terraform output -raw server_public_ipv4)
Complete Terraform Configuration
Here's all the code in a single block:
# versions.tf
terraform {
required_providers {
vultr = {
source = "vultr/vultr"
version = "~> 2.22"
}
// 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 "vultr" {
api_key = var.vultr_api_key
}
provider "wireguard" {}
# variables.tf
variable "vultr_api_key" {
description = "Vultr API Key"
type = string
sensitive = true
}
variable "region" {
description = "Vultr region"
type = string
default = "sea"
}
variable "plan" {
description = "Vultr plan ID"
type = string
default = "vc2-1c-1gb"
}
variable "os_id" {
description = "Vultr OS ID"
# 2284 Ubuntu 24.04 LTS x64 x64 ubuntu
default = "2284"
}
variable "client_count" {
description = "Number of WireGuard clients to create"
type = number
default = 1
}
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 = "enp1s0"
}
# main.tf
# SSH Key Generation
resource "tls_private_key" "ssh" {
algorithm = "ED25519"
}
resource "vultr_ssh_key" "vpn" {
name = "wireguard-vpn-key"
ssh_key = tls_private_key.ssh.public_key_openssh
}
# WireGuard Key Generation
resource "wireguard_asymmetric_key" "server" {}
resource "wireguard_asymmetric_key" "clients" {
count = var.client_count
}
# 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
]))
}
# Create firewall group
resource "vultr_firewall_group" "vpn" {
description = "WireGuard VPN Firewall"
}
# Allow SSH
resource "vultr_firewall_rule" "ssh" {
firewall_group_id = vultr_firewall_group.vpn.id
protocol = "tcp"
ip_type = "v4"
subnet = "0.0.0.0"
subnet_size = 0
port = "22"
}
# Allow WireGuard
resource "vultr_firewall_rule" "wireguard" {
firewall_group_id = vultr_firewall_group.vpn.id
protocol = "udp"
ip_type = "v4"
subnet = "0.0.0.0"
subnet_size = 0
port = "51820"
}
# Allow IPv6 WireGuard
resource "vultr_firewall_rule" "wireguard_ipv6" {
firewall_group_id = vultr_firewall_group.vpn.id
protocol = "udp"
ip_type = "v6"
subnet = "::"
subnet_size = 0
port = "51820"
}
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
}
}
# Create Vultr instance
resource "vultr_instance" "wireguard" {
plan = var.plan
region = var.region
os_id = var.os_id
tags = ["wireguard", "vpn"]
enable_ipv6 = true
firewall_group_id = vultr_firewall_group.vpn.id
ssh_key_ids = [vultr_ssh_key.vpn.id]
user_data = data.template_file.cloud_init.rendered
}
# outputs.tf
# Creates ready to use local files for client configurations.
# Generate QR code on Linux:
# cat clientN.conf | qrencode -t ansiutf8
resource "local_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 ?
"[${vultr_instance.wireguard.v6_main_ip}]" :
vultr_instance.wireguard.main_ip)
})
filename = "client${count.index + 1}.conf"
}
# Get ssh private key `terraform output -raw ssh_private_key > wireguard_ssh_key`
# chmod 400 wireguard_ssh_key
# ssh -i wireguard_ssh_key root@<ip-address>
output "ssh_private_key" {
value = tls_private_key.ssh.private_key_openssh
sensitive = true
}
output "server_public_ipv4" {
value = vultr_instance.wireguard.main_ip
}
output "server_public_ipv6" {
value = vultr_instance.wireguard.v6_main_ip
}
# 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