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)