WireGuard VPN on DigitalOcean with Terraform
- Authors
- Name
- upvpn LLC
- @upvpnapp
Introduction
This guide will walk you through setting up a WireGuard VPN server on DigitalOcean 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 DigitalOcean Droplet 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 {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.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 "digitalocean" {
token = var.do_token
}
provider "wireguard" {}
2. Variables Definition
Define the necessary variables:
# variables.tf
variable "do_token" {
description = "DigitalOcean API token"
type = string
sensitive = true
}
variable "region" {
description = "DigitalOcean region"
type = string
default = "sfo3"
}
variable "droplet_size" {
description = "Droplet size"
type = string
default = "s-1vcpu-1gb"
}
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
}
3. SSH Key Generation
Generate an SSH key pair for the Droplet:
# main.tf
resource "tls_private_key" "ssh" {
algorithm = "ED25519"
}
resource "digitalocean_ssh_key" "vpn" {
name = "wireguard-vpn-key"
public_key = tls_private_key.ssh.public_key_openssh
}
4. WireGuard Key Generation
Generate server and client keys:
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
write_files:
- path: /etc/wireguard/wg0.conf
content: |
[Interface]
Address = ${server_ipv4_address},${server_ipv6_address}
PrivateKey = ${server_private_key}
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostUp = ip6tables -A FORWARD -i wg0 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -D FORWARD -i wg0 -j ACCEPT
PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
PreDown = ip6tables -D FORWARD -i wg0 -j ACCEPT
PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
${peer_configs}
owner: root:root
permissions: '0600'
runcmd:
- 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 Droplet and firewall:
# 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
]))
}
resource "digitalocean_firewall" "wireguard" {
name = "wireguard-vpn"
droplet_ids = [digitalocean_droplet.wireguard.id]
inbound_rule {
protocol = "udp"
port_range = "51820"
source_addresses = ["0.0.0.0/0", "::/0"]
}
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "tcp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "udp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
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
}
}
resource "digitalocean_droplet" "wireguard" {
name = "wireguard-vpn"
size = var.droplet_size
image = "ubuntu-24-04-x64"
region = var.region
ipv6 = true
ssh_keys = [digitalocean_ssh_key.vpn.fingerprint]
user_data = data.template_file.cloud_init.rendered
tags = ["vpn", "wireguard"]
}
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 ?
"[${digitalocean_droplet.wireguard.ipv6_address}]" :
digitalocean_droplet.wireguard.ipv4_address)
})
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 = digitalocean_droplet.wireguard.ipv4_address
}
output "server_public_ipv6" {
value = digitalocean_droplet.wireguard.ipv6_address
}
Usage Instructions
- Create a
terraform.tfvars
file with your DigitalOcean API token:
Token must have following custom scopes:
- droplet (4): delete, update, read, create
- firewall (4): delete, update, read, create
- ssh_key (4): delete, update, read, create
do_token = "your-token-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 {
digitalocean = {
source = "digitalocean/digitalocean"
version = "~> 2.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 "digitalocean" {
token = var.do_token
}
provider "wireguard" {}
# variables.tf
variable "do_token" {
description = "DigitalOcean API token"
type = string
sensitive = true
}
variable "region" {
description = "DigitalOcean region"
type = string
default = "sfo3"
}
variable "droplet_size" {
description = "Droplet size"
type = string
default = "s-1vcpu-1gb"
}
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
}
# main.tf
# SSH Key Generation
resource "tls_private_key" "ssh" {
algorithm = "ED25519"
}
resource "digitalocean_ssh_key" "vpn" {
name = "wireguard-vpn-key"
public_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
]))
}
resource "digitalocean_firewall" "wireguard" {
name = "wireguard-vpn"
droplet_ids = [digitalocean_droplet.wireguard.id]
inbound_rule {
protocol = "udp"
port_range = "51820"
source_addresses = ["0.0.0.0/0", "::/0"]
}
inbound_rule {
protocol = "tcp"
port_range = "22"
source_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "tcp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
outbound_rule {
protocol = "udp"
port_range = "1-65535"
destination_addresses = ["0.0.0.0/0", "::/0"]
}
}
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
}
}
resource "digitalocean_droplet" "wireguard" {
name = "wireguard-vpn"
size = var.droplet_size
image = "ubuntu-24-04-x64"
region = var.region
ipv6 = true
ssh_keys = [digitalocean_ssh_key.vpn.fingerprint]
user_data = data.template_file.cloud_init.rendered
tags = ["vpn", "wireguard"]
}
# 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 ?
"[${digitalocean_droplet.wireguard.ipv6_address}]" :
digitalocean_droplet.wireguard.ipv4_address)
})
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 = digitalocean_droplet.wireguard.ipv4_address
}
output "server_public_ipv6" {
value = digitalocean_droplet.wireguard.ipv6_address
}
# cloud-init.yaml.tpl
#cloud-config
package_update: true
package_upgrade: false
packages:
- wireguard
write_files:
- path: /etc/wireguard/wg0.conf
content: |
[Interface]
Address = ${server_ipv4_address},${server_ipv6_address}
PrivateKey = ${server_private_key}
ListenPort = 51820
PostUp = iptables -A FORWARD -i wg0 -j ACCEPT
PostUp = iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PostUp = ip6tables -A FORWARD -i wg0 -j ACCEPT
PostUp = ip6tables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
PreDown = iptables -D FORWARD -i wg0 -j ACCEPT
PreDown = iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
PreDown = ip6tables -D FORWARD -i wg0 -j ACCEPT
PreDown = ip6tables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
${peer_configs}
owner: root:root
permissions: '0600'
runcmd:
- 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