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.
Architecture Overview
Section titled “Architecture Overview”Network Setup
Section titled “Network Setup”Using gcloud (Command Line)
Section titled “Using gcloud (Command Line)”# Create VPC networkgcloud compute networks create cloudrun \ --subnet-mode=custom \ --description="Network for Cloudflare tunnel services"
# Create subnetgcloud compute networks subnets create cfd \ --network=cloudrun \ --region=europe-west4 \ --range=10.0.10.0/24
# Create Cloud Routergcloud compute routers create cloudrun-router \ --network=cloudrun \ --region=europe-west4
# Create Cloud NATgcloud 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 Trafficgcloud 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
Using Terraform
Section titled “Using Terraform”# Google Cloud Provider configurationprovider "google" { project = var.project_id region = var.region}
# VPC Networkresource "google_compute_network" "vpc_network" { name = "cloudrun-vpc" auto_create_subnetworks = false description = "Network for Cloudflare tunnel services"}
# Subnetresource "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 Routerresource "google_compute_router" "router" { name = "cloudrun-router" region = var.region network = google_compute_network.vpc_network.id}
# Cloud NATresource "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 Trafficresource "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 Trafficresource "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 egressresource "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"]}
Service Deployment
Section titled “Service Deployment”Using gcloud (Command Line)
Section titled “Using gcloud (Command Line)”Deploy Internal Service (httpbun)
Section titled “Deploy Internal Service (httpbun)”gcloud run deploy httpbun \ --image=kennethreitz/httpbin \ --platform=managed \ --region=europe-west4 \ --port=80 \ --network=cloudrun \ --subnet=cfd \ --ingress=internal
Deploy Cloudflare Tunnel
Section titled “Deploy Cloudflare Tunnel”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
Using Terraform
Section titled “Using Terraform”# 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 serviceresource "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 }}
Cloudflare Tunnel Configuration
Section titled “Cloudflare Tunnel Configuration”Using Cloudflare Dashboard
Section titled “Using Cloudflare Dashboard”- Log in to your Cloudflare account
- Navigate to Zero Trust > Access > Tunnels
- Click “Create Tunnel”
- Follow the setup wizard to create a tunnel
- Note the tunnel token which will be used in the cloudflared service
Using Terraform
Section titled “Using Terraform”# Configure Cloudflare providerprovider "cloudflare" { api_token = var.cloudflare_api_token}
# Create Cloudflare Tunnelresource "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 tunnelresource "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 tunnelresource "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"}
Deployment Workflow
Section titled “Deployment Workflow”-
Create Cloudflare Tunnel:
- Deploy the Cloudflare resources first to obtain a tunnel token
- Store the token securely (e.g., in Google Secret Manager)
-
Deploy Google Cloud Resources:
- Set up VPC, subnets, router, NAT and firewall rules
- Deploy internal service (httpbun)
- Deploy cloudflared service with the tunnel token
-
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 Managerresource "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}
Important Notes
Section titled “Important Notes”Network Flow
Section titled “Network Flow”- Cloud Run services are automatically assigned private IPs in the VPC subnet range (10.0.10.x)
- Even with internal ingress, service-to-service communication goes through Google’s load balancer (216.239.x.x)
- The private IPs are used for VPC integration but not for direct HTTP communication
- All egress to Cloudflare occurs through Cloud NAT
Security Considerations
Section titled “Security Considerations”- Internal ingress ensures services are not accessible from the public internet
- Firewall rules restrict egress to only necessary destinations:
- Google Load Balancer (required for internal communication)
- Cloudflare edge servers (required for tunnel)
- Deny-all rule blocks any other egress traffic
- Services maintain private IP addresses but cannot be accessed directly via these IPs
- Important: Always use the complete list of Cloudflare IP ranges from https://www.cloudflare.com/ips/ for your firewall rules
Best Practices
Section titled “Best Practices”- Always use
min_instance_count=1
for cloudflared to maintain tunnel connectivity - Configure proper firewall rules to restrict egress
- Regularly update your Cloudflare IP allowlists using:
curl https://api.cloudflare.com/client/v4/ips | jq .
- Enable Cloud NAT logging for troubleshooting
- Monitor VPC flow logs for unexpected traffic
- Use Terraform for infrastructure as code to ensure consistency
- Store sensitive information like tunnel tokens in a secret manager
- Use separate Terraform modules or states for Cloudflare and GCP resources if needed
Automating IP Updates
Section titled “Automating IP Updates”For production environments, consider automating the Cloudflare IP updates:
# Example: Dynamic Cloudflare IP ranges in Terraformdata "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
Troubleshooting
Section titled “Troubleshooting”Common Issues
Section titled “Common Issues”-
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
-
Tunnel connectivity issues:
- Verify firewall rules allow all Cloudflare IPs
- Check NAT configuration
- Review cloudflared logs
- Ensure tunnel token is correct
-
Configuration challenges:
- For multi-environment setups, use Terraform workspaces or separate state files
- Handle chicken-and-egg dependencies by using outputs between separate applies
Useful Commands
Section titled “Useful Commands”# Check service configurationgcloud run services describe SERVICE_NAME --region=europe-west4
# View logsgcloud run services logs read SERVICE_NAME --region=europe-west4
# Check NAT statusgcloud compute routers nats describe cloudrun-nat \ --router=cloudrun-router \ --router-region=europe-west4