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
If you haven’t, login to you Cloudflare account to obtain a certificate.
cloudflared tunnel loginThis saves a cert.pem file to
${HOME}/.cloudflared/cert.pemCreate 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-tunnelThis writes a tunnel credential files to
${HOME}/.cloudflared/{GUID}.jsonand prints aTunnel Tokento the terminalExtract the secret value and send to
.envrcfor use in secret templateGUID=... # 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\"" >> .envrcSubstitute 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)
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.comRepeat this process for all (sub)domains to be proxied over Cloudflared Tunnel
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 parent — flux reconcile kustomization flux-cluster --with-source.