Do you need to secure your internet connection occasionally while travelling, connecting to the internet over untrusted networks (cafes, airports, …)? Tired of growing number of subscriptions or searching for a trusted VPN provider? Skilled enough to build your private VPN server but don’t want to host the service on your home network?

I found myself in a similar situation. Then a couple of announcements came during the last weeks of January 2020:

  • “Linus pulled in net-next about a half hour ago. So WireGuard is now officially upstream. Yeah!” by Bruno Wolf III on the WireGuard mailing list.
  • “I am beyond excited to finally announce Secret Manager - a secure and convenient method for storing API keys, passwords, certificates, and other sensitive data on @GCPcloud. It’s available for everyone today in beta” by @sethvargo on Twitter

The news combined with my intense affair with Google Cloud Platform (GCP) sparked an idea to build a solution for this - let’s create a pool of short-lived WireGuard VPN servers in GCP cloud with minimum effort and cost. Starting a VPN instance in any supported location worldwide only when I need it. Most of the internet is running through HTTPS anyway and unless I want to escape internet censorship or access a service not being served in the current country, VPN is no more a daily necessity.

The idea

The idea is the following:

  1. Store both sensitive data (DNS API keys, WireGuard private keys,…) and WireGuard configuration inside the Google Secret Manager
  2. Create a free (US-only) or paid GCE VM instance, without a static IP address to enjoy
  3. Use cloud-init config to install & setup the WireGuard VPN and update DNS record automatically on VM boot
  4. Create multiple VMs (VPN instances) in various locations but run only 1 at a time to minimize costs
  5. Use the Cloud Console mobile app to start the instance in a region you just need
  6. Enjoy

This article can introduce you to the following topics:

  • Install a WireGuard VPN server with minimal 3rd party tooling or complicated scripts
  • Store sensitive info required at runtime in a Google Secret Manager secure vault
  • Spawn a VPN server VM in an automated way in a location of your choice worldwide

WARNING

  • The article is intended for tech-savvy readers with basic experience with DNS & Cloud providers as it’s not going to provide you a step-by-step guide for all the topics covered.
  • If you search for a solution to provide you with complete privacy, total anonymity and bullet-proof security, this provides you with none of those. Using a VPN is not a magic bullet, but definitely brings a few benefits.
  • Consult with a security professional all the pros and cons of using a VPN.

Requirements

I built this concept using services I like for their simplicity and features offered in their free plan: GCP as a cloud, Cloudflare as a DNS provider. Here is a complete list of building blocks and skills recommended to try the concept:

  • your own domain name
  • a DNS hosting for the domain allowing to update DNS records using API - I’m using Cloudflare’s Free Plan
  • Google Cloud Platform account
  • basic GCP experience - create, start, stop VM, create IAM account and configure firewall rules
  • (optional) Google Cloud SDK CLI installed - most of the implementation steps will provide a gcloud example
  • (optional) Shell scripting / Python basics - if you want to understand a few simple scripts and oneliners used in the examples
  • (optional) Cloud Console Mobile App - to control VPN instances from your smartphone

💡 Feel free to use providers and services based on your preference and experience.

Let’s play

Enough theory, let’s play. This is the agenda:

  • GCP quickstart
  • Create a limited GCP IAM Service Account
  • Create a DNS record
  • Create WireGuard configuration
  • Create firewall rules to allow the WireGuard traffic
  • Create WireGuard secrets using Google Secret Manager
  • Create a VM and your first VPN instance
  • Test, play, enjoy!

GCP quickstart

  1. Read the GCP overview if this domain is completely new for you
  2. Sign in to the GCP Cloud console using a Google account. If you don’t already have one, sign up for a new account.
  3. Create a new billing account to use the account (required only for new users). You must have a valid Cloud Billing Account to use GCP even if you are in your free trial period or you only use Google Cloud resources that are covered by the Always Free program.
  4. Select or create a Cloud project
  5. Install Cloud SDK cli if you prefer to follow the gcloud examples in this article
  6. (Optional, but highly recommended) Set budget alerts to avoid any unexpected payments.

Create a limited GCP IAM service Account

Create a new Cloud IAM service account and grant it only a role to access the Secret manager secrets for the project. The SA account will be used later to create every new VPN VM instance with a restricted identity. This is important for security reasons:

  • the default Compute Engine service account grants the instance more permissions than is required. Google recommends that each instance that needs to call a Google API should run as a service account with the minimum permissions necessary for that instance to do its job.
  • The role grants instance rights to only access (view) secrets stored in the Secret Manager vault. The VM (or a potential attacked) will not be allowed to edit or list the secrets stored in the Secret Manager, nor to access any other resources using API.

Steps:

  1. Enable the Secret Manager API for the project
  2. Create a service account using a console or use gcloud CLI:
    gcloud iam service-accounts create secret-accessor \
    --description "SA for accessing Secret Manager secrets" \
    --display-name "secret accessor"
    
  3. Grant the service account secretmanager.secretAccessor IAM role:
    gcloud projects add-iam-policy-binding project-123 \
    --member serviceAccount:secret-accessor@project-123.iam.gserviceaccount.com \
    --role roles/secretmanager.secretAccessor
    

Setup DNS

To setup the system and enable the dynamic DNS update, we need the following:

  • A DNS record
  • API token and another provider specific info (e.g. Cloudflare Zone ID, the DNS record Cloudflare ID) to enable dynamic DNS updates

Steps:

  1. Create an A DNS record that will VPN clients use to connect to the VPN server. As we do know the IP address of the VM instance yet, point the record to any IP address, e.g. any from the private ranges of the IPv4 addresses: vpn.example.com -> 172.27.27.2. Example instructions for adding a DNS record on Cloudflare
  2. Create an API token or any provider specific key that will allow you to update DNS record remotely using API.
  3. Get other details required for the API requests to work

Create a WireGuard configuration

There are tons of howto guides on the internet how to create a WireGuard configuration fitting your needs. Here are at least 2 links:

I will further use following WireGuard configuration used in every VM instance created (wg0.conf):

[Interface]
Address = 172.30.0.1/24
PostUp = iptables -A FORWARD -i wg0 -o wg0 -j ACCEPT; iptables -t nat -A POSTROUTING -s 172.30.0.0/24 -o ens4 -j MASQUERADE
PostDown = iptables -D FORWARD -i wg0 -o wg0  -j ACCEPT; iptables -t nat -D POSTROUTING -s 172.30.0.0/24 -o ens4 -j MASQUERADE
ListenPort = 51820
PrivateKey = abcdef123456serverPrivateKey==

# Client Alice
[Peer]
PublicKey = abcdef123456aliceKey=
AllowedIPs = 172.30.0.2/32

# Client Bob
[Peer]
PublicKey = abcdef123456bobKey=
AllowedIPs = 172.30.0.3/32

This configuration exaplanation:

  • WireGuard server listens on UDP port 51820
  • creates iptables NAT & forwarding firewall rules on start
  • binds to ens4 network interface that’s being created and enabled by default on the ubuntu-minimal-1910 VM image from the ubuntu-os-cloud GCE family that I use for this setup.
  • uses following keypair for the VPN server: abcdef123456serverPrivateKey==/abcdef123456serverPublicKey==
  • configures 2 peers/clients - Alice (IP: 172.30.0.2/32) and Bob (IP: 172.30.0.3/32)

Store config and secrets in the Secret Manager vault

Secret Manager is a pretty cool, new product in the GCP portfolio (currently in Beta) that complements the existing Cloud KMS solution for data encryption. It allows storing sensitive data encrypted at rest using AES-256. Customer managed encryption keys are not yet supported.

Let’s create and store following secrets for this example setup:

  1. WireGuard configuration created in the previous section: gcloud beta secrets create wg_config --data-file wg0.conf --replication-policy automatic
  2. Cloudflare API token: echo -n 'my_cloudflare_api_token==' | gcloud beta secrets create wg_cf_api_token --data-file - --replication-policy automatic
  3. Cloudflare Zone ID: echo -n 'my_cloudflare_zone_id_123456' | gcloud beta secrets create wg_cf_zone_id --data-file - --replication-policy automatic
  4. Hostname of the VM WireGuard clients will conect to: echo -n 'vpn.example.com' | gcloud beta secrets create wg_hostname --data-file - --replication-policy automatic

As an alternative, Secrets Manager cloud console can be used to create the secrets if you prefer GUI.

Create a cloud-init configuration

cloud-init is a tool that helps to install & configure a cloud (GCE) VM to the desired state. In short, the cloud-init example:

  • installs required packages - iptables & wireguard
  • enables IP forwarding
  • sets up a script executed on every boot to:
    • read and store WireGuard config from the Secret Manager
    • read and use other sensitive data we saved in the Secret Manager
    • create or update Cloudflare DNS record with the VM’s public IP address using Cloudflare API on boot
  • updates OS packages
  • reboots the VM after the install is finished
#cloud-config
package_upgrade: true
packages:
  - iptables
  - wireguard
write_files:
  - content: |
      net.ipv4.ip_forward=1
    path: /etc/sysctl.d/99-wireguard.conf
    permissions: '0644'
  - content: |
      #!/usr/bin/env bash
      set -e

      # get latest wireguard configuration from the secret manager and store it locally
      gcloud beta secrets versions access latest --secret wg_config > /etc/wireguard/wg0.conf
      chmod 0600 /etc/wireguard/wg0.conf

      # get other sensitive data
      hostname="$(gcloud beta secrets versions access latest --secret wg_hostname)"
      cf_api_token="$(gcloud beta secrets versions access latest --secret wg_cf_api_token)"
      cf_zone_id="$(gcloud beta secrets versions access latest --secret wg_cf_zone_id)"

      # create or update the Cloudflare DNS record
      if ip=$(curl -fs ifconfig.me); then
              echo "Checking if DNS record $hostname exists in Cloudflare"
              dns_check=$(curl -sX GET "https://api.cloudflare.com/client/v4/zones/${cf_zone_id}/dns_records?type=A&name=${hostname}" \
              -H "Authorization: Bearer $cf_api_token" \
              -H "Content-Type: application/json" \
              | python3 -c "import sys, json; f=json.load(sys.stdin); print('{};{}'.format(f['result'][0]['content'],f['result'][0]['id'])) if 'result' in f else print('EE')")
              IFS=';' read -ra response <<< "${dns_check}"
              if [[ "${response[0]}" == "${ip}" ]]; then
                      echo "DNS record ${hostname} OK: ${ip}. No need to update."
              else
                      echo "Updating existing A record $hostname to $ip"
                      curl -o /dev/null -fsX PUT "https://api.cloudflare.com/client/v4/zones/${cf_zone_id}/dns_records/${response[1]}" \
                      -H "Authorization: Bearer ${cf_api_token}" \
                      -H "Content-Type: application/json" \
                      -d "{\"type\": \"A\", \"name\": \"${hostname}\", \"content\": \"${ip}\", \"proxied\": false}"
              fi
      else
              echo "Problem detecting current IP address. Exiting"
      fi
    path: /usr/local/bin/wg_dns_updater.sh
    permissions: '0755'
  - content: |
      [Unit]
      Description=wg-dns-updater script
      After=network.target

      [Service]
      Type=oneshot
      RemainAfterExit=yes
      ExecStart=/usr/local/bin/wg_dns_updater.sh

      [Install]
      WantedBy=multi-user.target
    path: /etc/systemd/system/wg-dns-updater.service
    permissions: '0644'
runcmd:
  - [ sysctl, -p, /etc/sysctl.d/99-wireguard.conf ]
  - [ systemctl, daemon-reload ]
  - [ systemctl, enable, --now, --no-block, wg-quick@wg0.service ]
  - [ systemctl, enable, --now, wg-dns-updater.service ]
power_state:
  mode: reboot
  delay: 1
  message: Rebooting after installation

I recommend everyone to review the config and change it to fit personal needs.

Create a GCE VM

Now the best part - glueing everything together. I will create multiple VMs using the cloud-init config in regions I need. I use the following bash script to create the instances:

#!/usr/bin/env bash
NAME=${1:-vpn-ue1b}
ZONE=${2:-us-east1-b}
gcloud compute instances create "$NAME" \
  --machine-type f1-micro \ # f1-micro is powerful enough to run WireGuard server
  --image-family ubuntu-minimal-1910 \ # Ubuntu 19.10 provides WireGuard in the universe repo
  --image-project ubuntu-os-cloud \
  --metadata-from-file user-data=cloud-init-config.yaml \ # use the prepared cloud-init config here
  --scopes cloud-platform \
  --service-account secret-accessor@project-123.iam.gserviceaccount.com \ # use the prepared secret-accessor service account
  --tags wg \ # use network tags for the firewall rules
  --zone "$ZONE" # zone where to create the instance

And then execute the script:

./vm-create.sh vpn-ue1b us-east1-b
./vm-create.sh vpn-uc1b us-central1-b
./vm-create.sh vpn-uw1b us-west1-b

It’s important to remind the DNS record is updated on every VM boot. Therefore shut down all VMs after creation and start only a single VM in a region you just need.

Pro tips:

  • you can have up to 3 VMs created in the US zones and still qualify for the free tier if you have only single one running at a time.
  • use --preemptible argument when creating the VM to have it automatically shut down within 24 hours. The only con of this is that a preemptible instance is excluded from the free tier so you will be charged for running it.
  • WARNING: only 1 GB of network egress from North America to all region destinations per month (excluding China and Australia) is included in the free tier.

Configure firewall

Create a firewall rule to enable incoming traffic to udp/51820 to the WireGuard VM(s): gcloud compute firewall-rules create wg --direction IN --target-tags wg --allow udp:51820 --source-ranges 0.0.0.0/0

As the VM instances are tagged by a network tag, this allows applying firewall rules and routes to a specific instance or set of instances.

Test VPN connection

Let’s now test if the VPN connection works. Install a WireGuard client on a platform of your choice and configure it to connect to the VPN server:

[Interface]
Address = 172.30.0.2/32
PrivateKey = abcdef123456alicePrivateKey==

[Peer]
PublicKey = abcdef123456serverPublicKey==
Endpoint = vpn.example.com:51820
AllowedIPs = 0.0.0.0/0

Install the Cloud Console Mobile app

I recommend to install the Cloud Console Mobile app to start and stop WireGuard server instances from your smartphone. You can thus fire up any of the pre-installed instances in the region you just need using your smartphone, very handy thing!

Final notes

In this post, I have shown you how to build a self-managed, on-demand VPN infrastructure ready to tunnel your private internet traffic using WireGuard and Google Cloud Platform. You can now own and run the whole infrastructure on-demand with minimum effort and costs end scape the VPN subscription and fake reviews hell.

If you like the article or have any feedback, feel free to let me know or share the post.

Also, consider donating to WireGuard project to help with the development of this great OSS tool ❤️