Exposing GCP VMs to the Internet with Cloudflare Tunnel


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
Google Cloud CLI installed and authenticated.
A Cloudflare Account with a registered domain.
Download the initial code.
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.
Install
cloudflared
on our local machine.Authenticate with Cloudflare by running
cloudflared tunnel login
.Create a tunnel by running
cloudflared tunnel create my-gcp-tunnel
.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.
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