Skip to content

Setting up Cloudflare Tunnel on Google Cloud Run

This guide provides instructions for setting up a Cloudflare Tunnel using Google Cloud Run, showing both command-line (gcloud) and Infrastructure as Code (Terraform) approaches.

Google Cloud

VPC: cloudrun (10.0.10.0/24)

Network Components

Internal Services

(1) Internal Request

(2) Route

(3) Response

(4) Return

(5) Tunnel

(6) Egress

Firewall Rules

TCP:443 → Google Infrastructure

priority: 999

TCP/UDP:7844 → CF Edge

priority: 1000

deny-all

priority: 10000

Google Infrastructure

(observed: 216.239.x.x)

Cloudflared

(10.0.10.x)

httpbun

(10.0.10.y)

Cloud Router

Cloud NAT

Cloudflare Edge

198.41.192.0/24

198.41.200.0/24

Terminal window
# Create VPC network
gcloud compute networks create cloudrun \
--subnet-mode=custom \
--description="Network for Cloudflare tunnel services"
# Create subnet
gcloud compute networks subnets create cfd \
--network=cloudrun \
--region=europe-west4 \
--range=10.0.10.0/24
# Create Cloud Router
gcloud compute routers create cloudrun-router \
--network=cloudrun \
--region=europe-west4
# Create Cloud NAT
gcloud compute routers nats create cloudrun-nat \
--router=cloudrun-router \
--router-region=europe-west4 \
--region=europe-west4 \
--nat-all-subnet-ip-ranges \
--auto-allocate-nat-external-ips
# Firewall Rules
# 1. Allow Google Infrastructure Traffic (Required for internal service communication)
# IMPORTANT: Google's infrastructure IP ranges are dynamically allocated and change frequently.
# The ranges below are commonly observed, but you MUST monitor your VPC flow logs
# to identify the specific ranges used in your deployment.
# Common observed ranges include 216.239.32.0/19, but these are NOT guaranteed.
gcloud compute firewall-rules create allow-google-apis \
--network=cloudrun \
--direction=egress \
--action=allow \
--rules=tcp:443 \
--destination-ranges=216.239.32.0/19,35.190.0.0/16,130.211.0.0/22 \
--priority=999
# 2. Allow Cloudflare Edge Traffic
gcloud compute firewall-rules create allow-cf-traffic \
--network=cloudrun \
--direction=egress \
--action=allow \
--rules=tcp:7844,udp:7844 \
# Port 7844 is used by cloudflared to connect to Cloudflare
--destination-ranges=$(curl -s https://www.cloudflare.com/ips-v4 | paste -sd "," -) \
# Fetches current Cloudflare IPv4 ranges
--priority=1000
# 3. Deny all other egress (recommended)
gcloud compute firewall-rules create deny-all \
--network=cloudrun \
--direction=egress \
--action=deny \
--rules=all \
--destination-ranges=0.0.0.0/0 \
--priority=10000
# Google Cloud Provider configuration
provider "google" {
project = var.project_id
region = var.region
}
# VPC Network
resource "google_compute_network" "vpc_network" {
name = "cloudrun-vpc"
auto_create_subnetworks = false
description = "Network for Cloudflare tunnel services"
}
# Subnet
resource "google_compute_subnetwork" "cfd_subnet" {
name = "cfd-subnet"
ip_cidr_range = "10.0.10.0/24"
region = var.region
network = google_compute_network.vpc_network.id
private_ip_google_access = true
}
# Cloud Router
resource "google_compute_router" "router" {
name = "cloudrun-router"
region = var.region
network = google_compute_network.vpc_network.id
}
# Cloud NAT
resource "google_compute_router_nat" "nat" {
name = "cloudrun-nat"
router = google_compute_router.router.name
region = google_compute_router.router.region
nat_ip_allocate_option = "AUTO_ONLY"
source_subnetwork_ip_ranges_to_nat = "ALL_SUBNETWORKS_ALL_IP_RANGES"
log_config {
enable = true
filter = "ERRORS_ONLY"
}
}
# Firewall Rules
# 1. Allow Google Infrastructure Traffic
resource "google_compute_firewall" "allow_google_apis" {
name = "allow-google-apis"
network = google_compute_network.vpc_network.id
direction = "EGRESS"
priority = 999
description = "Allow egress to Google infrastructure for internal Cloud Run communication"
allow {
protocol = "tcp"
ports = ["443"]
}
# IMPORTANT: Google infrastructure IP ranges are dynamically allocated and change frequently.
# These ranges are commonly observed but NOT guaranteed. You MUST monitor VPC flow logs
# to identify the actual ranges used in your deployment.
destination_ranges = [
"216.239.32.0/19", # Commonly observed range (NOT guaranteed)
"35.190.0.0/16", # Google APIs and services (may vary)
"130.211.0.0/22" # Additional Google services (may vary)
]
}
# 2. Allow Cloudflare Edge Traffic
resource "google_compute_firewall" "allow_cf_traffic" {
name = "allow-cf-traffic"
network = google_compute_network.vpc_network.id
direction = "EGRESS"
priority = 1000
description = "Allow egress to Cloudflare Edge for Tunnel connection"
allow {
protocol = "tcp"
ports = ["7844"] # Primary port for cloudflared connections to Cloudflare
}
allow {
protocol = "udp"
ports = ["7844"] # UDP fallback for cloudflared connections
}
# IMPORTANT: This list should be updated regularly. For production, use the
# dynamic data source approach shown in the "Automating IP Updates" section.
# To get latest IPs: curl https://api.cloudflare.com/client/v4/ips | jq .
# For China network: curl 'https://api.cloudflare.com/client/v4/ips?networks=jdcloud' | jq .
destination_ranges = [
"173.245.48.0/20",
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"141.101.64.0/18",
"108.162.192.0/18",
"190.93.240.0/20",
"188.114.96.0/20",
"197.234.240.0/22",
"198.41.128.0/17",
"162.158.0.0/15",
"104.16.0.0/13",
"104.24.0.0/14",
"172.64.0.0/13",
"131.0.72.0/22"
]
}
# 3. Deny all other egress
resource "google_compute_firewall" "deny_all_egress" {
name = "deny-all"
network = google_compute_network.vpc_network.id
direction = "EGRESS"
priority = 10000
description = "Deny all other egress traffic"
deny {
protocol = "all"
}
destination_ranges = ["0.0.0.0/0"]
}
Terminal window
gcloud run deploy httpbun \
--image=kennethreitz/httpbin \
--platform=managed \
--region=europe-west4 \
--port=80 \
--network=cloudrun \
--subnet=cfd \
--ingress=internal
Terminal window
gcloud run deploy cloudflared \
--image=docker.io/cloudflare/cloudflared:latest \
--platform=managed \
--region=europe-west4 \
--command=cloudflared \
--args="tunnel,--no-autoupdate,--metrics,0.0.0.0:10000,--metrics-update-freq,5s,run,--token,your-tunnel-token" \
--network=cloudrun \
--subnet=cfd \
--port=10000 \
--ingress=internal \
--min-instances=1
# Internal service (httpbun)
resource "google_cloud_run_v2_service" "httpbun_service" {
name = "httpbun"
location = var.region
template {
containers {
image = "kennethreitz/httpbin"
ports {
container_port = 80
}
}
vpc_access {
network_interfaces {
network = google_compute_network.vpc_network.id
subnetwork = google_compute_subnetwork.cfd_subnet.id
}
egress = "ALL_TRAFFIC"
}
}
ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"
traffic {
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
percent = 100
}
}
# Cloudflared service
resource "google_cloud_run_v2_service" "cloudflared_service" {
name = "cloudflared"
location = var.region
template {
scaling {
min_instance_count = 1 # Maintains tunnel connectivity
}
containers {
image = "cloudflare/cloudflared:latest"
args = [
"tunnel",
"--no-autoupdate",
"--metrics",
"0.0.0.0:10000",
"--metrics-update-freq",
"5s",
"run",
"--token",
var.tunnel_token # Provided via a variable or secret manager
]
ports {
container_port = 10000
}
}
vpc_access {
network_interfaces {
network = google_compute_network.vpc_network.id
subnetwork = google_compute_subnetwork.cfd_subnet.id
}
egress = "ALL_TRAFFIC"
}
}
ingress = "INGRESS_TRAFFIC_INTERNAL_ONLY"
traffic {
type = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
percent = 100
}
}
  1. Log in to your Cloudflare account
  2. Navigate to Zero Trust > Access > Tunnels
  3. Click “Create Tunnel”
  4. Follow the setup wizard to create a tunnel
  5. Note the tunnel token which will be used in the cloudflared service
# Configure Cloudflare provider
# Note: Using v4 syntax which is stable. v5 has breaking changes and known issues.
provider "cloudflare" {
api_token = var.cloudflare_api_token
}
# Create Cloudflare Tunnel
resource "cloudflare_tunnel" "gcp_cloudrun_tunnel" {
account_id = var.cloudflare_account_id
name = "gcp-cloudrun-tunnel"
# secret attribute is optional - Cloudflare will generate one if not provided
}
# Configure the tunnel
resource "cloudflare_tunnel_config" "gcp_cloudrun_config" {
account_id = var.cloudflare_account_id
tunnel_id = cloudflare_tunnel.gcp_cloudrun_tunnel.id
config {
ingress_rule {
hostname = var.public_hostname # e.g., "app.example.com"
# Cloud Run URI includes https:// already, so we need to extract just the hostname
service = "http://${trimprefix(google_cloud_run_v2_service.httpbun_service.uri, "https://")}"
}
# Default catch-all rule
ingress_rule {
service = "http_status:404"
}
}
}
# Create DNS record for the tunnel
resource "cloudflare_record" "tunnel_dns" {
zone_id = var.cloudflare_zone_id
name = var.subdomain # e.g., "app" for app.example.com
value = "${cloudflare_tunnel.gcp_cloudrun_tunnel.id}.cfargotunnel.com"
type = "CNAME"
proxied = true
}
# Output the tunnel token (sensitive - handle with care)
output "tunnel_token" {
value = cloudflare_tunnel.gcp_cloudrun_tunnel.tunnel_token
sensitive = true
description = "Token for authenticating cloudflared service to Cloudflare"
}
  1. Create Cloudflare Tunnel:

    • Deploy the Cloudflare resources first to obtain a tunnel token
    • Store the token securely (e.g., in Google Secret Manager)
  2. Deploy Google Cloud Resources:

    • Set up VPC, subnets, router, NAT and firewall rules
    • Deploy internal service (httpbun)
    • Deploy cloudflared service with the tunnel token
  3. Configure Secret Management (recommended for production):

    • Store the tunnel token in Google Secret Manager
    • Reference the secret in your Cloud Run service
# Example: Store token in Secret Manager
resource "google_secret_manager_secret" "tunnel_token" {
secret_id = "cf-tunnel-token"
replication {
automatic = true
}
}
resource "google_secret_manager_secret_version" "tunnel_token_version" {
secret = google_secret_manager_secret.tunnel_token.id
secret_data = cloudflare_tunnel.gcp_cloudrun_tunnel.tunnel_token
}
  1. Cloud Run services are automatically assigned private IPs in the VPC subnet range (10.0.10.x)
  2. Even with internal ingress, service-to-service communication routes through Google’s infrastructure:
    • Internal requests are processed by Google’s routing layer
    • Traffic may appear to originate from various Google-owned IP ranges (commonly 216.239.x.x)
    • The exact IP ranges can vary based on region, service configuration, and Google’s internal routing
  3. The private IPs are used for VPC integration but HTTP(S) requests between services use Google’s internal routing
  4. All egress to external destinations (like Cloudflare) occurs through Cloud NAT
  1. Internal ingress ensures services are not accessible from the public internet
  2. Firewall rules restrict egress to only necessary destinations:
    • Google infrastructure ranges (required for internal service communication and Google APIs)
    • Cloudflare edge servers (required for tunnel)
  3. Deny-all rule blocks any other egress traffic
  4. Services maintain private IP addresses but cannot be accessed directly via these IPs
  5. Important: Always use the complete list of Cloudflare IP ranges from https://www.cloudflare.com/ips/ for your firewall rules
  1. Always use min_instance_count=1 for cloudflared to maintain tunnel connectivity
  2. Configure proper firewall rules to restrict egress
  3. Regularly update your Cloudflare IP allowlists using:
Terminal window
# Standard Cloudflare IPs
curl https://api.cloudflare.com/client/v4/ips | jq .
# Including China network (if needed)
curl 'https://api.cloudflare.com/client/v4/ips?networks=jdcloud' | jq .
  1. Enable Cloud NAT logging for troubleshooting
  2. Monitor VPC flow logs for unexpected traffic
  3. Use Terraform for infrastructure as code to ensure consistency
  4. Store sensitive information like tunnel tokens in a secret manager
  5. Use separate Terraform modules or states for Cloudflare and GCP resources if needed

For production environments, consider automating the Cloudflare IP updates:

# Example: Dynamic Cloudflare IP ranges in Terraform
data "http" "cloudflare_ips" {
url = "https://api.cloudflare.com/client/v4/ips"
request_headers = {
Accept = "application/json"
}
}
locals {
cloudflare_ip_data = jsondecode(data.http.cloudflare_ips.response_body)
cloudflare_ipv4_cidrs = local.cloudflare_ip_data.result.ipv4_cidrs
}
# Then use local.cloudflare_ipv4_cidrs for your firewall rule destination_ranges
  1. Service-to-service communication failing:

    • Check VPC flow logs to identify which Google IP ranges are being used
    • Verify firewall rules allow traffic to observed Google infrastructure ranges
    • Check service ingress settings (should be set to internal)
    • Verify both services are in the same region
    • Consider temporarily allowing broader Google IP ranges for debugging
  2. Tunnel connectivity issues:

    • Verify firewall rules allow all Cloudflare IPs
    • Check NAT configuration
    • Review cloudflared logs
    • Ensure tunnel token is correct
  3. Configuration challenges:

    • For multi-environment setups, use Terraform workspaces or separate state files
    • Handle chicken-and-egg dependencies by using outputs between separate applies
Terminal window
# Check service configuration
gcloud run services describe SERVICE_NAME --region=europe-west4
# View logs
gcloud run services logs read SERVICE_NAME --region=europe-west4
# Check NAT status
gcloud compute routers nats describe cloudrun-nat \
--router=cloudrun-router \
--router-region=europe-west4