Cloudflare

Argo Tunnel

Cloudflare Tunnel provides you with a secure way to connect your resources to Cloudflare without a publicly routable IP address. With Tunnel, you do not send traffic to an external IP — instead, a lightweight daemon in your infrastructure (cloudflared) creates outbound-only connections to Cloudflare’s edge.

Prerequisites

Install the cloudflared CLI

Setup

github example docs documentation config

  1. If you haven’t, login to you Cloudflare account to obtain a certificate.

    cloudflared tunnel login

    This saves a cert.pem file to ${HOME}/.cloudflared/cert.pem

  2. Create a tunnel, change example-tunnel to the name you want to assign to your tunnel.

    # list existing
    cloudflared tunnel list
    # create new
    cloudflared tunnel create k8s-argo-tunnel

    This writes a tunnel credential files to ${HOME}/.cloudflared/{GUID}.json and prints a Tunnel Token to the terminal

  3. Extract the secret value and send to .envrc for use in secret template

    GUID=... # from above step
    SECRET_CLOUDFLARE_TUNNEL_CREDS=$(kubectl create secret generic tunnel-credentials \
    --from-file=credentials.json="${HOME}/.cloudflared/${GUID}.json" \
    --output=yaml \
    --dry-run=client | grep credentials.json | awk '{ print $2 }')
    echo "export SECRET_CLOUDFLARE_TUNNEL_CREDS=\"$SECRET_CLOUDFLARE_TUNNEL_CREDS\"" >> .envrc
  4. Substitute and encrypt the secret

    # substitute
    envsubst < ./path/to/cloudflared/secret.sops.yaml.tmpl >! ./path/to/cloudflared/secret.sops.yaml
    
    # encrypt
    sops --encrypt --in-place ./path/to/cloudflared/secret.sops.yaml

Deploy tunnel for app (START HERE IF TUNNEL ALREADY DEPLOYED)

  1. Associate your Tunnel with a DNS record.

    Via CLI:

    cloudflared tunnel route dns k8s-argo-tunnel "<SUBDOMAIN>.${SECRET_DOMAIN}"

    Via WebUI: Log into Cloudflare portal and create a CNAME record for <SUBDOMAIN> with target <TUNNEL_ID>.cfargotunnel.com

    Repeat this process for all (sub)domains to be proxied over Cloudflared Tunnel

  2. Deploy cloudflared by applying its manifest (managed by Flux kustomization).

    When Cloudflare receives traffic for the DNS or Load Balancing hostname you configured in the previous step, it will send that traffic to the cloudflareds running in this deployment. Those cloudflared instances will proxy the request to your app’s Service.

Secrets & substitution

cloudflared pulls its identity from three distinct sources — do not conflate them:

Name Origin Provides Consumed by
cloudflared-tunnel bootstrap secret (SOPS, kubernetes/bootstrap/) TUNNEL_ID pod env TUNNEL_ID, and Flux ${TUNNEL_ID} build-time substitution (DNSEndpoint)
cloudflared-credential ExternalSecret → 1Password (network.cloudflared) credentials.json pod volume mounted at /etc/cloudflared/creds/credentials.json
cluster-secrets ClusterExternalSecret → 1Password SECRET_DOMAIN, etc. Flux ${SECRET_DOMAIN} substitution (config, HTTPRoutes)

credentials.json is rendered by ExternalSecrets from 1Password and is no longer stored as a SOPS secret in-repo. The legacy secret.sops.yaml* flow above is kept for historical reference.

${TUNNEL_ID} substitution (important)

dnsendpoint.yaml builds the tunnel CNAME ${TUNNEL_ID}.cfargotunnel.com via Flux postBuild substitution. This app’s ks.yaml intentionally does not set postBuild.substituteFrom — the parent flux-cluster Kustomization owns that field (see gotcha #4 in the repo-root AGENTS.md). cloudflared-tunnel is merged in via a targeted JSON6902 append in kubernetes/flux/cluster/ks.yaml, yielding [cluster-secrets, cloudflared-tunnel].

If ${TUNNEL_ID} fails to resolve (strict-mode variable not set, blocking this Kustomization): confirm the cloudflared-tunnel secret exists in network with a TUNNEL_ID key and the parent’s append patch is present, then reconcile the parentflux reconcile kustomization flux-cluster --with-source.