Exposing GCP VMs to the Internet with Cloudflare Tunnel

Raul NaupariRaul Naupari
4 min read

In the previous article, we deployed a virtual machine (VM) on Google Cloud Platform (GCP) with a simple NGINX web server. While functional, exposing a server directly to the internet with a public IP address and firewall rules can introduce security vulnerabilities and management overhead. Today, we will build on our previous setup and introduce a more secure and robust way to expose our web server: Cloudflare Tunnel.

Cloudflare Tunnel creates a secure, outbound-only connection between our VM and the Cloudflare network. A lightweight daemon, cloudflared, runs on our VM and establishes this connection. This means we can have a VM running in our GCP project without a public IP address, making it inaccessible from the public internet and less vulnerable to direct attacks.

Here are some key benefits of using Cloudflare Tunnel:

  • Enhanced security: Our server's IP address is hidden, which protects it from direct attacks.

  • No inbound ports: We no longer need to open ports on our firewall.

  • DDoS protection: All traffic to our server goes through Cloudflare's network, which offers DDoS protection.

  • Free tier: Cloudflare provides a generous free tier for its Tunnel service, making it a cost-effective solution for personal projects and small applications.

Pre-requisites

Updating the Terraform Configuration

First, let's update our Terraform code to remove the public IP address and the firewall rule since they are no longer needed. Modify the terraform/main.tf file as shown below:

terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = ">= 4.34.0"
    }
  }
}

resource "google_compute_instance" "free_tier_vm" {
  project      = var.project_id
  machine_type = "e2-micro"
  zone         = "us-central1-a"
  name         = "my-first-vm"
  boot_disk {
    initialize_params {
      image = "debian-cloud/debian-11"
    }
  }
  network_interface {
    network = "default"
    access_config {}
  }
  metadata = {
    startup-script = file("${path.module}/startup-script.sh")
  }
}

variable "project_id" {
  type        = string
  description = "The GCP project ID to deploy resources into."
}

Notice that the tags property has been removed from the google_compute_instance resource, and the entire google_compute_firewall resource has been deleted. We need to keep the access_config block to assign a public IP address to the VM so it can still access the internet.

Setting up Cloudflare Tunnel

Setting up a Cloudflare Tunnel involves a few manual steps that we only need to do once.

  1. Install cloudflared on our local machine.

  2. Authenticate with Cloudflare by running cloudflared tunnel login.

  3. Create a tunnel by running cloudflared tunnel create my-gcp-tunnel.

  4. Create a CNAME record in Cloudflare DNS to link our subdomain to our tunnel:

cloudflared tunnel route dns my-gcp-tunnel nginx.<MY_DOMAIN>

Updating the Startup Script

Now, let's update the terraform/startup-script.sh to install and run cloudflared on the VM when it starts. To get the token and ID for our tunnel, run the following command:

cloudflared tunnel token my-gcp-tunnel
cloudflared tunnel info my-gcp-tunnel

Update the startup script with the following content:

#!/bin/bash
set -e
echo "=== Startup script started at $(date) ==="
apt-get update -y
apt-get install -y nginx
systemctl start nginx
systemctl enable nginx
wget -q https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
dpkg -i cloudflared-linux-amd64.deb
mkdir -p /etc/cloudflared/
cat > /etc/cloudflared/config.yml << EOF
tunnel: <MY_TUNNEL_ID>
credentials-file: /root/.cloudflared/<MY_TUNNEL_ID>.json
ingress:
  - hostname: nginx.<MY_DOMAIN>
    service: http://127.0.0.1:80
  - service: http_status:404
EOF
cloudflared service install <MY_TUNNEL_TOKEN> --config /etc/cloudflared/config.yml
systemctl start cloudflared
systemctl enable cloudflared
echo "=== Startup script completed at $(date) ==="

This script will now perform the following actions:

  • Install NGINX.

  • Download and install the cloudflared daemon.

  • Configure and start the cloudflared service using our tunnel token. This will connect our VM to the Cloudflare network and route traffic from our chosen hostname to the local NGINX server on port 80.

Deploy

Now, we can apply the changes using Infrastructure Manager:

gcloud infra-manager deployments apply my-first-deployment --location=us-central1 --local-source=./terraform --input-values=project_id=<MY_PROJECT_ID> --service-account=projects/<MY_PROJECT_ID>/serviceAccounts/infra-manager-sa@<MY_PROJECT_ID>.iam.gserviceaccount.com

Once the deployment is complete, our NGINX server will be accessible at the hostname you configured (nginx.<MY_DOMAIN> in our example), secured behind the Cloudflare network.

Clean Up

To remove all the resources created, we can run the same cleanup command as in the previous article:

gcloud infra-manager deployments delete my-deployment --location=us-central1

To remove the Cloudflare tunnel, we can run:

cloudflared tunnel delete my-gcp-tunnel

By using Cloudflare Tunnel, we've greatly enhanced the security of our deployment, showing a more production-ready way to expose services running on GCP. You can find all the code here. Thanks, and happy coding.

0
Subscribe to my newsletter

Read articles from Raul Naupari directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Raul Naupari
Raul Naupari

Somebody who likes to code