TunnelFleet
Infrastructure as Code for VPNs
DevOps 10 min read 1 views

Infrastructure as Code for VPNs

Table of Contents

Manual VPN servers age badly. The first node is a careful ssh session and a clean wg0.conf. The tenth is a half-remembered firewall rule. The fiftieth is a spreadsheet of public keys nobody trusts. Infrastructure as code (IaC) is how you stop that drift before it becomes an outage.

For self-hosted VPN infrastructure—WireGuard or OpenVPN on your own VPS—IaC means the droplet, the first-boot OS config, the tunnel inventory, and the recovery path live in versioned definitions. You rebuild from those definitions. You do not “fix the production box” and hope someone updates the wiki.

This guide focuses on practical IaC for VPN fleets on DigitalOcean (TunnelFleet’s currently supported cloud provider), with patterns that transfer to any API-driven VPS.

What You'll Learn

  • What “infrastructure as code” means for VPN servers specifically
  • Which layers you should declare (cloud, OS, VPN, peers)
  • How cloud-init, Terraform-style provisioning, and a control plane divide responsibilities
  • Secrets handling that does not dump private keys into git
  • A repeatable rebuild and disaster-recovery mindset
  • Best practices, common mistakes, and FAQ for teams adopting IaC

Why VPN Fleets Need IaC

VPN nodes look simple: one public IP, one UDP port, a handful of peers. That simplicity encourages click-ops. The failure mode is not “Terraform is hard.” The failure mode is:

  • A peer left after an employee departed
  • Two servers with overlapping tunnel CIDRs
  • A firewall change applied on one region and forgotten on another
  • No way to recreate a dead droplet with the same identity and routing policy

IaC does not make cryptography better. It makes change auditable, rebuilds boring, and peer inventory enforceable.

Treat every VPN server as cattle with a stable role: vpn-eu-fra-01, not asif-test-do-final-v2. Names, regions, CIDRs, and listen ports belong in the same place as the provisioning template.

The Four Layers of VPN Infrastructure

Separate concerns so tools stay honest.

1. Cloud resources

Droplet size, region, VPC/private networking, reserved IP, firewall/security group, DNS A/AAAA records. On DigitalOcean this is the Droplet API (or Terraform provider), plus cloud firewall rules for SSH and WireGuard/OpenVPN ports.

This layer answers: Where does the machine live, and what is allowed to reach it?

2. Guest OS bootstrap

Users, SSH keys, packages, sysctl (ip_forward), base firewall (UFW), unattended upgrades, time sync. This is classic cloud-init territory on first boot.

This layer answers: Is the OS safe and ready before any VPN daemon starts?

3. VPN service configuration

Interface addresses, listen ports, server private keys, forwarding/NAT policy, protocol choice (WireGuard vs OpenVPN TCP/UDP). Prefer rendering from templates and injecting secrets at provision time—not hand-editing /etc/wireguard/wg0.conf on live boxes as the source of truth.

This layer answers: What does this node do as a tunnel endpoint?

4. Peer / user inventory

Public keys, client tunnel IPs, AllowedIPs, certificates/CRL for OpenVPN, owner, expiry, revoke status. This layer changes the most often. Spreadsheets fail here first. A control plane or config-management inventory should own it.

This layer answers: Who may use the tunnel, and with what routing policy?

If you squash all four into one giant shell script pasted into a droplet’s user data, you will recreate the snowflake—just with YAML.

Declarative Intent vs Imperative Scripts

Imperative provisioning (“create droplet, then SSH, then apt install, then echo config”) works until concurrency, partial failure, or teammate B runs step 3 twice. Declarative intent says: desired state is X; reconcile until true.

In practice for VPN fleets:

  • Cloud layer: declare droplets and firewalls; apply until the API state matches.
  • OS layer: declare cloud-init once per image/role; prefer golden images when churn is high.
  • VPN + peers: reconcile continuously—add/remove peers without rebuilding the droplet every time.

Rebuilding the VPS for every peer change is the wrong granularity. Rebuilding when the role or region changes is correct.

A Minimal DigitalOcean-Oriented Pattern

A sane baseline pipeline:

  1. Template a droplet with Ubuntu 22.04 or 24.04 LTS, tagged role:vpn, region code in the name.
  2. Attach cloud-init that installs WireGuard (or OpenVPN), enables forwarding, configures UFW for SSH + VPN UDP/TCP, and installs a management agent or pull-based config fetcher.
  3. Inject secrets from a vault or one-time provision API—never commit server private keys.
  4. Register the node in inventory (hostname, public IP, tunnel CIDR, version).
  5. Push or pull peer config from the control plane.
  6. Health-check handshake freshness, interface up, and DNS for the node label.
  7. Tear down by destroying the droplet and removing inventory—orphaned DNS and stale peers are part of the blast radius.

TunnelFleet’s DigitalOcean path follows the same idea: create the droplet, install the agent via cloud-init, then manage protocols and VPN users from the control plane instead of SSHing into every peer block.

Version Control What Matters

Commit:

  • Terraform/API definitions for droplets and firewalls
  • cloud-init templates (with secret placeholders, not secrets)
  • CIDR plans and naming conventions
  • Runbooks for rotate/revoke/rebuild
  • Alert definitions for handshake staleness and disk/CPU

Do not commit:

  • WireGuard private keys or OpenVPN CA keys
  • Client configs containing private material
  • API tokens

Use git for structure; use a secrets manager for material. Pull requests become the change-control mechanism for “we opened UDP 51820 in FRA but not in NYC.”

Peer Inventory Is Part of IaC

Teams often IaC the VPS and leave peers manual. That inverts risk: the VPS is cheap to replace; an unrevoked peer is a standing access grant.

Model each peer as data:

peer_id: alice-laptop
server: vpn-eu-fra-01
public_key: ...
tunnel_ip: 10.66.10.14/32
allowed_ips: 10.66.10.14/32
owner: alice@example.com
expires_at: 2026-12-31
status: active

Render server config from that inventory. Revocation becomes status: revoked + reconcile—not “remember to edit wg0 on three hosts.”

Testing and Staging

You do not need a full second production fleet. You need:

  • A staging droplet role that uses a non-overlapping CIDR
  • A canary apply for cloud-init/template changes
  • A documented rebuild drill: destroy staging VPN node, recreate from IaC, confirm clients reconnect

If you cannot rebuild a node in under an hour with confidence, you do not have IaC yet—you have aspirational YAML.

Observability Belongs in the Definition

Declare what “healthy” means:

  • WireGuard: latest handshake within N minutes for expected active peers
  • Interface present and addresses correct
  • Cloud firewall and UFW still allow the listen port
  • Agent or config reconciler last success timestamp

Alert on drift between desired peer count and wg show peer count. Silent missing peers are worse than noisy alerts.

CI/CD for VPN Templates

Wire VPN role changes through the same gates as application code. A pull request that alters cloud-init, firewall ports, or CIDR defaults should run:

  • YAML/HCL validation and terraform plan (or API dry-run) against a non-prod DigitalOcean project
  • A policy check that rejects committed private keys (secret scanning)
  • A canary job that creates a throwaway droplet, waits for cloud-init status --wait, asserts wg show or agent heartbeat, then destroys the droplet

Promote templates only after the canary is green. Hotfixes that skip the pipeline recreate the snowflake culture IaC was meant to end.

Pin provider and module versions. Floating latest images or unpinned Terraform providers make last month’s known-good apply unreproducible during an incident. Record the Ubuntu image ID and agent version that passed canary in the change ticket.

Drift Detection

Even with IaC, humans will SSH “just once.” Detect drift instead of pretending it never happens:

  • Compare live cloud firewall rules to declared rules on a schedule
  • Compare desired peer inventory to wg show public keys
  • Alert when a droplet tag/role:vpn exists without an inventory record (shadow node)
  • Alert when inventory references a destroyed droplet ID

Drift alerts should name an owner. An unread dashboard is not detection.

Team Interfaces

Define who may approve CIDR changes, who may apply prod Terraform, and who may push agent versions. IaC without change authority becomes either bottlenecked on one hero or wide open to every engineer. A simple RACI beats an unspoken assumption.

Encode break-glass: when the control plane is down, which SSH path exists, who unlocks it, and how inventory is reconciled afterward so emergency edits do not become permanent shadow config.

Best Practices

  1. One role, many instances. vpn role templates scale; unique snowflake nodes do not.
  2. Plan CIDRs centrally. Publish a registry of tunnel ranges per region/environment.
  3. Separate rebuild from revoke. Rebuilding a droplet should not be your only way to remove a user.
  4. Immutable preference. Prefer replace-over-tinker for OS and package drift; prefer reconcile-in-place for peer lists.
  5. Tag everything. Cloud tags for role, env, and owner make blast-radius queries possible.
  6. Document break-glass SSH. IaC does not eliminate emergency access—it defines who has it and how it is audited.
  7. Automate DNS with the droplet. A VPN endpoint hostname should appear and disappear with the node.
  8. Keep DigitalOcean tokens scoped. Provisioning credentials are production credentials.

Common Mistakes

User-data scripts that grow forever. Unmaintainable bash in cloud-init becomes the new snowflake. Split packages/users from VPN reconcile.

Private keys in the repo “just for staging.” Staging keys leak into forks and CI logs. Use separate secret paths.

Overlapping tunnel networks across regions. Two “temporary” 10.0.0.0/24 overlays will collide when you build site-to-site later.

Applying firewall changes only in the cloud UI. The next Terraform apply may revert—or worse, never recreate what you clicked.

Treating the agent host as pet. If only one person can recreate the management path, the fleet is still manual.

No destroy checklist. Deleting a droplet without removing peers, DNS, and monitoring leaves ghosts that confuse the next incident.

Full-tunnel defaults in templates. Shipping AllowedIPs = 0.0.0.0/0 as the role default surprises users and breaks split-horizon DNS assumptions.

FAQ

Is Terraform required for VPN IaC?

No. Any declarative or reproducible API workflow counts. Terraform is common for DigitalOcean resources; cloud-init covers guest bootstrap; a control plane covers peers. Use the smallest set your team will actually maintain.

Can I IaC WireGuard without rebuilding droplets?

Yes—and you should. Rebuild for OS/role changes. Reconcile peers continuously.

Where should server private keys live?

In a secrets backend or generated on the node at first boot and registered to the control plane. Not in git, not in shared Slack, not in unencrypted disk images you distribute casually.

How does this relate to Ansible or other config management?

Config management can own OS drift and package state after boot. For VPN fleets, many teams still use cloud-init for bootstrap and a purpose-built reconciler for peers. Overlap is fine; dual sources of truth are not.

What about OpenVPN?

Same layering. The CA and certificate lifecycle become part of inventory IaC. Revocation lists must be published automatically or you will forget a departed contractor.

Does DigitalOcean VPC replace a VPN?

No. VPC private networking connects your droplets inside the provider network. A VPN connects users and external sites to those networks (or to each other) over the public internet with your encryption and access policy.

How often should I rebuild VPN nodes?

On a cadence that matches patch policy—or immediately for compromise. Scheduled rebuilds catch config drift that humans normalize.

What is the first IaC win for a small team?

Stop editing live wg0.conf as the source of truth. Put peer inventory in a file or database, render configs, and version the renderer. Then automate droplet create/destroy.

Summary

Infrastructure as code for VPNs is not a single tool. It is a discipline: cloud resources, OS bootstrap, tunnel service config, and peer inventory each have a declarative home. Rebuilds become routine. Revocations become data changes. Regions stop diverging by accident.

Start by naming roles, locking CIDR plans, removing secrets from git, and automating DigitalOcean droplet + cloud-init bootstrap. Then put peer lifecycle on a reconciler. That path scales from a handful of servers to a real fleet.

If you want to automate VPN server deployment instead of configuring everything manually, TunnelFleet helps you provision and manage VPN infrastructure on your own cloud provider with minimal manual setup.

Tags: WireGuard VPN Self Hosting Cloud-Init Automation DigitalOcean DevOps Infrastructure as Code

Share this article

T

Practical guides on VPN infrastructure, server automation, and self-hosted networking from the TunnelFleet team.

View all articles by TunnelFleet Editorial →

Related Articles