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 → 216.239.32.x/x

priority: 999

TCP/UDP:7844 → CF Edge

priority: 1000

deny-all

priority: 10000

Google Load Balancer

216.239.32.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 Load Balancer Traffic (Required for internal service communication)
gcloud compute firewall-rules create allow-lb-traffic \
--network=cloudrun \
--direction=egress \
--action=allow \
--rules=tcp:443 \
--destination-ranges=216.239.32.0/19 \
--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 Load Balancer Traffic
resource "google_compute_firewall" "allow_lb_traffic" {
name = "allow-lb-traffic"
network = google_compute_network.vpc_network.id
direction = "EGRESS"
priority = 999
description = "Allow egress to Google Load Balancers for internal Cloud Run communication"
allow {
protocol = "tcp"
ports = ["443"]
}
destination_ranges = ["216.239.32.0/19"] # Google Front End IP range
}
# 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: Get the complete, up-to-date list of Cloudflare IPs
# You can fetch them programmatically with:
# curl https://api.cloudflare.com/client/v4/ips | jq .
# or https://www.cloudflare.com/ips-v4
destination_ranges = [
# Example Cloudflare IP ranges (not exhaustive)
"103.21.244.0/22",
"103.22.200.0/22",
"103.31.4.0/22",
"104.16.0.0/13",
"104.24.0.0/14",
"108.162.192.0/18",
"131.0.72.0/22",
"141.101.64.0/18",
"162.158.0.0/15",
"172.64.0.0/13",
"173.245.48.0/20",
"188.114.96.0/20",
"190.93.240.0/20",
"197.234.240.0/22",
"198.41.128.0/17"
]
}
# 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
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 goes through Google’s load balancer (216.239.x.x)
  3. The private IPs are used for VPC integration but not for direct HTTP communication
  4. All egress to 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 Load Balancer (required for internal communication)
    • 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
curl https://api.cloudflare.com/client/v4/ips | 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:

    • Verify firewall rules allow traffic to 216.239.x.x/x
    • Check service ingress settings
    • Verify both services are in the same region
  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