Private Network + VPN
Private network + VPN
This pattern composes Network and Compute.
What this template does#
Provisions a private network topology with site-to-site VPN connectivity:
- Private network and subnet for internal workloads
- VPN gateway instance with WireGuard or IPsec (cloud-init installs WireGuard; see cloud-init and first-boot configuration)
- Security groups restricting VPN traffic to authorized endpoints
- Router configuration for VPN tunnel routing
- Split DNS or route-based forwarding to on-premises networks
Parameters#
| Parameter | Description | Default |
|---|---|---|
key_name | SSH keypair name (must already exist in your project) | required |
vpn_protocol | VPN protocol (wireguard or ipsec) | wireguard |
private_cidr | Private network CIDR | 10.0.0.0/24 |
remote_cidr | Remote network CIDR | No default |
remote_endpoint | Remote VPN endpoint IP | No default |
remote_wireguard_public_key | Public key of the WireGuard peer | No default |
gateway_flavor | VPN gateway instance size | s1a.small |
image_name | Operating system image | Ubuntu-24.04 |
external_network | External network for router gateway and floating IP | PublicStatic |
vpn_port | UDP port WireGuard listens on | 51820 |
When to use this pattern#
Connect a private network to remote clients through a WireGuard VPN gateway instance. Choose Development Environment for a bastion-based lab without VPN, or Simple VM with Floating IP when a single public VM is enough.
Estimated cost#
Monthly cost estimate
Pricing calculator ↗Sized as a custom package on shared vCPU.
Monthly total for the required template above. Use the configurator below to add optional pieces and see the total update.
What each resource is for
Gateway
s1a.small · 2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute shown per role at custom-package rates ($29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM). The headline above is the billed total: the cheaper of a named plan and the custom package, plus add-ons.
Included in baseline
s1a.small
2 shared vCPU, 2 GiB RAM, 0.5 Gbps
Compute + RAM rate basis
2 vCPU + 2 GiB RAM at $29/dedicated vCPU, $7.25/shared vCPU, $1/GiB RAM (regular). Totals apply the flat −$5/mo package promotion.
Block storage (20 GiB)
20 GiB at $0.08/GiB/mo
Public IP (included)
1 included with the custom package
Package promotional discount
Flat −$5.00/mo on the custom package (same promotion as named plans).
Included at no charge
These line items are zero on Quake AI. Many other providers meter them separately.
Data transfer (inbound and outbound)
Unlimited data transfer on every plan; Quake AI does not meter per-GB egress.
AWS, GCP, and Azure meter outbound transfer per GB. DigitalOcean and Hetzner include an allowance on compute plans, then charge overage.
Learn morePrivate networking
Private networks, subnets, Neutron routers, and security groups are included with the plan.
VPC objects are usually free to create elsewhere, but NAT gateways bill hourly plus per-GB processed. Quake AI uses router SNAT with no separate NAT line item.
Control-plane API requests
OpenStack API calls for provisioning and management are included.
Some managed services on other clouds meter API calls or charge for premium control-plane features.
Pricing data last validated: . For current rates, check quake.ai/pricing.
Template source#
Show source (7 files)Hide source
data "openstack_images_image_v2" "os" {
name = var.image_name
most_recent = true
}
data "openstack_networking_network_v2" "external" {
name = var.external_network
}
resource "openstack_networking_network_v2" "private" {
name = "private-network-vpn-net"
admin_state_up = true
}
resource "openstack_networking_subnet_v2" "private" {
name = "private-network-vpn-subnet"
network_id = openstack_networking_network_v2.private.id
cidr = var.private_cidr
ip_version = 4
dns_nameservers = ["1.1.1.1", "8.8.8.8"]
}
resource "openstack_networking_router_v2" "main" {
name = "private-network-vpn-router"
external_network_id = data.openstack_networking_network_v2.external.id
}
resource "openstack_networking_router_interface_v2" "private" {
router_id = openstack_networking_router_v2.main.id
subnet_id = openstack_networking_subnet_v2.private.id
}
resource "openstack_networking_secgroup_v2" "gateway" {
name = "private-network-vpn-sg"
}
resource "openstack_networking_secgroup_rule_v2" "wireguard" {
direction = "ingress"
ethertype = "IPv4"
protocol = "udp"
port_range_min = var.vpn_port
port_range_max = var.vpn_port
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.gateway.id
}
resource "openstack_networking_secgroup_rule_v2" "ssh" {
direction = "ingress"
ethertype = "IPv4"
protocol = "tcp"
port_range_min = 22
port_range_max = 22
remote_ip_prefix = "0.0.0.0/0"
security_group_id = openstack_networking_secgroup_v2.gateway.id
}
resource "openstack_networking_secgroup_rule_v2" "private_all" {
direction = "ingress"
ethertype = "IPv4"
remote_ip_prefix = var.private_cidr
security_group_id = openstack_networking_secgroup_v2.gateway.id
}
resource "openstack_compute_instance_v2" "gateway" {
name = "private-network-vpn-gateway"
flavor_name = var.gateway_flavor
key_pair = var.key_name
metadata = {
vpn_protocol = var.vpn_protocol
}
user_data = templatefile("${path.module}/cloud-init/wireguard.yaml", {
private_cidr = var.private_cidr
vpn_port = var.vpn_port
remote_cidr = var.remote_cidr
remote_endpoint = var.remote_endpoint
remote_wireguard_public_key = var.remote_wireguard_public_key
})
block_device {
uuid = data.openstack_images_image_v2.os.id
source_type = "image"
destination_type = "volume"
volume_size = 20
boot_index = 0
delete_on_termination = true
}
network {
port = openstack_networking_port_v2.gateway.id
}
lifecycle {
precondition {
condition = var.vpn_protocol == "wireguard"
error_message = "vpn_protocol must be wireguard for this template."
}
}
}
resource "openstack_networking_port_v2" "gateway" {
name = "private-network-vpn-gateway-port"
network_id = openstack_networking_network_v2.private.id
security_group_ids = [openstack_networking_secgroup_v2.gateway.id]
fixed_ip {
subnet_id = openstack_networking_subnet_v2.private.id
}
depends_on = [openstack_networking_router_interface_v2.private]
}
resource "openstack_networking_floatingip_v2" "gateway" {
pool = var.external_network
}
resource "openstack_networking_floatingip_associate_v2" "gateway" {
floating_ip = openstack_networking_floatingip_v2.gateway.address
port_id = openstack_networking_port_v2.gateway.id
}
variable "vpn_protocol" {
type = string
default = "wireguard"
}
variable "private_cidr" {
type = string
default = "10.0.0.0/24"
}
variable "remote_cidr" {
type = string
}
variable "remote_endpoint" {
type = string
}
variable "remote_wireguard_public_key" {
type = string
sensitive = true
}
variable "gateway_flavor" {
type = string
default = "s1a.small"
}
variable "key_name" {
description = "Existing SSH keypair name in the project for compute instances"
type = string
}
variable "image_name" {
type = string
default = "Ubuntu-24.04"
}
variable "external_network" {
description = "Shared external network for router gateway and floating IPs; defaults to PublicStatic (persisted FIP / production pattern). Override with PublicEphemeral for ephemeral demos."
type = string
default = "PublicStatic"
}
variable "vpn_port" {
type = number
default = 51820
}
output "vpn_endpoint" {
value = "${openstack_networking_floatingip_v2.gateway.address}:${var.vpn_port}"
}
output "gateway_private_ip" {
value = openstack_compute_instance_v2.gateway.network[0].fixed_ip_v4
}
terraform {
required_version = ">= 1.6.0"
required_providers {
openstack = {
source = "terraform-provider-openstack/openstack"
version = "~> 2.0"
}
}
}
provider "openstack" {}
# Required
remote_cidr = "YOUR_REMOTE_CIDR"
remote_endpoint = "YOUR_REMOTE_ENDPOINT_HOST_OR_IP"
remote_wireguard_public_key = "YOUR_REMOTE_WIREGUARD_PUBLIC_KEY"
key_name = "YOUR_KEY_NAME"
# vpn_protocol = "wireguard"
# private_cidr = "10.0.0.0/24"
# gateway_flavor = "s1a.small"
# image_name = "Ubuntu-24.04"
# external_network = "PublicStatic"
# vpn_port = 51820
#cloud-config
packages:
- wireguard-tools
write_files:
- path: /etc/wireguard/remote.peer.pub
owner: root:root
permissions: "0600"
content: |
${remote_wireguard_public_key}
runcmd:
- |
set -eux
umask 077
mkdir -p /etc/wireguard
wg genkey | tee /etc/wireguard/private.key | wg pubkey > /etc/wireguard/public.key
DEF_IF=$(ip -4 route show default | awk '{print $5; exit}')
ADDR=$(ip -4 -o addr show "$DEF_IF" | awk '{print $4}' | head -n1)
PRIV=$(tr -d '\n' < /etc/wireguard/private.key)
REMOTE_PUB=$(tr -d '[:space:]' < /etc/wireguard/remote.peer.pub)
cat > /etc/wireguard/wg0.conf <<WGEOF
[Interface]
# private_cidr ${private_cidr}
PrivateKey = $PRIV
Address = $ADDR
ListenPort = ${vpn_port}
PostUp = sysctl -w net.ipv4.ip_forward=1
PostDown = sysctl -w net.ipv4.ip_forward=0
[Peer]
PublicKey = $REMOTE_PUB
Endpoint = ${remote_endpoint}:${vpn_port}
AllowedIPs = ${remote_cidr}
PersistentKeepalive = 25
WGEOF
sed -i 's/^[[:space:]]*//' /etc/wireguard/wg0.conf
chmod 600 /etc/wireguard/wg0.conf /etc/wireguard/private.key
systemctl enable wg-quick@wg0
systemctl start wg-quick@wg0
# Private Network VPN
Site-to-site WireGuard VPN with a gateway instance on your external network and private addressing for resources behind the gateway.
**Network class:** production — `external_network` defaults to `PublicStatic` for persisted floating IPs and multi-tier stacks; override with `PublicEphemeral` for ephemeral demos.
## Prerequisites
- OpenTofu >= 1.6.0 or Terraform >= 1.6.0
- Quake AI account with OpenStack credentials
- Remote site details: peer CIDR, public endpoint, and WireGuard public key for the peer
- UDP access to the chosen VPN port (default WireGuard) from the remote site
## Usage
1. Clone or copy this template directory
2. Copy `terraform.tfvars.example` to `terraform.tfvars` and fill in your values (treat `remote_wireguard_public_key` as sensitive)
3. Source your OpenStack credentials: `source openrc.sh`
4. Initialize: `tofu init`
5. Preview: `tofu plan`
6. Apply: `tofu apply`
## Variables
| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `remote_cidr` | string | yes | n/a | CIDR of the remote network to route over the tunnel |
| `remote_endpoint` | string | yes | n/a | Remote peer public hostname or IP |
| `remote_wireguard_public_key` | string | yes | n/a | Remote peer WireGuard public key (sensitive) |
| `key_name` | string | yes | n/a | Existing SSH keypair name in the project for compute instances |
| `vpn_protocol` | string | no | `wireguard` | VPN protocol identifier |
| `private_cidr` | string | no | `10.0.0.0/24` | Local private network CIDR behind the gateway |
| `gateway_flavor` | string | no | `s1a.small` | Flavor for the VPN gateway |
| `image_name` | string | no | `Ubuntu-24.04` | Boot image name |
| `external_network` | string | no | `PublicStatic` | Persisted FIP / production default; override with `PublicEphemeral` for demos |
| `vpn_port` | number | no | `51820` | UDP port for WireGuard |
## Documentation
Full documentation: [Private network VPN template](/docs/automation/templates/private-network-vpn)
Resources, parameters, and variables
vpn_protocol="wireguard"private_cidr="10.0.0.0/24"remote_cidrrequiredremote_endpointrequiredremote_wireguard_public_keyrequiredgateway_flavor="s1a.small"key_namerequiredimage_name="Ubuntu-24.04"external_network="PublicStatic"vpn_port=51820
Customize this pattern#
- Customize a template's image and flavor
- Add a block volume to a template
- Parameterize a template with a tfvars file
See also#
- Deploy the private network + VPN template with OpenTofu: end-to-end tutorial that applies this template, verifies the WireGuard tunnel, and tears the stack down
- Networking concepts
- Kubernetes Cluster template
Usage Guidelines
The sample code, software libraries, command line tools, proofs of concept, templates, and other related technology on this page (including any of the foregoing that is provided by Quake AI personnel) is provided to you as Quake AI Content under the Quake AI Customer Agreement, or the relevant written agreement between you and Quake AI (whichever applies). Do not use this Quake AI Content in your production accounts, or on production or other critical data. You are responsible for testing, securing, and optimizing the Quake AI Content (such as sample code) as appropriate for production grade use based on your specific quality control practices and standards. Deploying Quake AI Content may incur Quake AI charges for creating or using Quake AI chargeable resources, such as running Compute instances or storing data in Object Storage. Your use is also subject to the Acceptable Use Policy.
For the full policy, see Usage Guidelines.
Last validated: 07.07.2026
See Also
How to set up a site-to-site or remote-access VPN to Quake AI
Shares: VPN, Routers
Deploy the private network + VPN template with OpenTofu
Shares: VPN, Routers
Networks
Prerequisite
Terraform and OpenTofu on Quake AI
Prerequisite
Deploy the full-stack application template with OpenTofu
Shares: Routers, Security Groups