This blog will give you a brief overview for exposing services from a private network to the public network using zero trust tool, CloudFlare Zero Trust Tunnels. They have a generous free tier that you can leverage. Please note that this post does not encompass a fully prescriptive set of security best practices, merely it gets the setup working. Getting this right took me longer than I expected. Hopefully its helpful to you.
Due to to my homelab setup, namespaces are created by ArgoCD which kicks off ahead of terraform apply. You may need to make a namespace cloudflared
.
Access
Based on the guide from cloudflares docs you'll need to create a API token:
Note: When creating your api token know that "Cloudflare Tunnel" is not the same thing as Zero Trust in their permissions. This was confusing to me because tunnels are now named "zero trust tunnels". For the sake of permissions make sure you provider "Cloudflare Tunnel: Edit".
Terraform
The following code deploys a single tunnel and sets up DNS records for all sites indicated in "services". TLS verify is disabled below so update it if you want.
variable "cloudflare_zone_id" {
description = "Zone ID for your domain"
type = string
sensitive = true
}
variable "cloudflare_account_id" {
description = "Account ID for your Cloudflare account"
type = string
sensitive = true
}
variable "cloudflare_email" {
description = "Email address for your Cloudflare account"
type = string
sensitive = true
}
variable "services" {
description = "Values for the services to be exposed via Cloudflare Tunnel"
type = map(object({
hostname = string # public domain name. ex: "service.example.com"
service = string # private service endpoint. ex: service.apps.internaldomain.com
}))
}
locals {
cf_tunnel_secret = jsonencode({
"AccountTag" : "${var.cloudflare_account_id}",
"TunnelSecret" : "${base64sha256(random_password.tunnel_secret.result)}",
"TunnelID" : "${cloudflare_zero_trust_tunnel_cloudflared.homelab.id}"
})
ingress_rules = [
for service in var.services : {
hostname = service.hostname
origin_request = {
no_tls_verify = true
}
service = service.service
}
]
}
resource "random_password" "tunnel_secret" {
length = 64
}
resource "cloudflare_zero_trust_tunnel_cloudflared" "homelab" {
name = "homelab-tunnel"
config_src = "cloudflare"
account_id = var.cloudflare_account_id
tunnel_secret = base64sha256(random_password.tunnel_secret.result)
}
resource "cloudflare_zero_trust_tunnel_cloudflared_config" "homelab" {
account_id = var.cloudflare_account_id
tunnel_id = cloudflare_zero_trust_tunnel_cloudflared.homelab.id
config = {
ingress = concat(local.ingress_rules, [
{
origin_request = {
connect_timeout = 0
keep_alive_connections = 0
keep_alive_timeout = 0
tcp_keep_alive = 0
tls_timeout = 0
}
service = "http_status:503"
},
])
warp_routing = {
enabled = false
}
}
}
resource "kubernetes_secret" "cloudflare_credentials" {
metadata {
name = "tunnel-credentials"
namespace = "cloudflared"
}
data = {
"credentials.json" = local.cf_tunnel_secret
}
}
resource "cloudflare_dns_record" "vault_homelab_tunnel" {
for_each = var.services
zone_id = var.cloudflare_zone_id
comment = "${each.key} tunnel record"
content = join(".", [cloudflare_zero_trust_tunnel_cloudflared.homelab.id, "cfargotunnel.com"])
name = each.value.hostname
proxied = true
ttl = 1
type = "CNAME"
}
Kubernetes manifest:
In my setup, k8s resources are mostly deployed via ArgoCD. Below, this manifest deploys cloudflared pod and connects it to the tunnel homelab-tunnel
from your account. It authenticates via the public cloudflare API using credentials from a kubernetes secret tunnel-credentials
which is created by Terraform.
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: cloudflared
namespace: cloudflared
spec:
selector:
matchLabels:
app: cloudflared
replicas: 1
template:
metadata:
labels:
app: cloudflared
spec:
containers:
- name: cloudflared
image: cloudflare/cloudflared:latest
args:
- tunnel
- --credentials-file
- /etc/cloudflared/creds/credentials.json
- --protocol # https://github.com/cloudflare/cloudflared/issues/1176#issuecomment-2404546711
- http2 # didnt seem to need the sysctl changes
- --metrics
- 0.0.0.0:2000
- run
- homelab-tunnel
livenessProbe:
httpGet:
path: /ready
port: 2000
failureThreshold: 1
initialDelaySeconds: 10
periodSeconds: 10
volumeMounts:
- name: creds
mountPath: /etc/cloudflared/creds
readOnly: true
volumes:
- name: creds
secret:
secretName: tunnel-credentials
Disclaimer: The following code stores the secret to joining your tunnel in your terraform statefile in 2 locations random_password.tunnel_secret
and cloudflare_zero_trust_tunnel_cloudflared.homelab
. With 1.10 we have ephemeral resources which will remove that but the random provider doesnt have it yet. Hopefully it will be removed from tunnel resource once available.
Top comments (0)