Skip to content

cfgate.iocfgate v0.2.0-alpha.11 · Release documentation

CloudflareTunnel

Manages the lifecycle of a Cloudflare Tunnel and its cloudflared daemon deployment.

API Version: cfgate.io/v1alpha1 Kind: CloudflareTunnel Short Names: cft, cftunnel Scope: Namespaced

CloudflareTunnel handles tunnel creation or adoption, credential management, and deploys cloudflared pods that establish secure connections to Cloudflare’s edge network. It follows a composable architecture where tunnel lifecycle is separate from DNS management. Use CloudflareDNS with a tunnelRef to create DNS records pointing to this tunnel’s domain.

A tunnel is zone-agnostic: one tunnel can serve any number of domains across different zones. The tunnel itself does not bind to any particular domain; DNS records are created separately via CloudflareDNS resources.

Tunnel name resolution is idempotent. The controller resolves the tunnel by name and creates it if it does not exist. Multiple CloudflareTunnel resources with the same tunnel name will adopt the same Cloudflare tunnel rather than creating duplicates. The resolved tunnel ID is stored in .status.tunnelId.

FieldTypeDefaultRequiredDescription
spec.tunnel.namestringnoneYesTunnel name in Cloudflare. Idempotent: creates if absent, adopts if existing. Must be 1-63 chars, lowercase alphanumeric with hyphens, matching ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$.
spec.cloudflare.accountIdstringnoneNoCloudflare Account ID. Max 32 chars. Either accountId or accountName must be specified.
spec.cloudflare.accountNamestringnoneNoCloudflare Account name. Resolved via API lookup (requires Account Settings Read permission). Max 255 chars. Either accountId or accountName must be specified.
spec.cloudflare.secretRef.namestringnoneYesName of the Secret containing the Cloudflare API token. 1-253 chars.
spec.cloudflare.secretRef.namespacestring(resource namespace)NoNamespace of the credentials Secret. Defaults to the tunnel’s namespace. Max 63 chars.
spec.cloudflare.secretKeys.apiTokenstringCLOUDFLARE_API_TOKENNoKey name within the Secret for the Cloudflare API token. Max 253 chars.
spec.cloudflared.replicasint322NoNumber of cloudflared replicas. Min 1, max 10. Each replica establishes an independent connection for high availability.
spec.cloudflared.imagestringghcr.io/inherent-design/cloudflared:2026.9.3-h2c.1@sha256:6c46ca006f9d6af5e973e59f2d71f5d6d3dc138c8a5484380d5797092171e3adNoContainer image for the cloudflared daemon. See Image below. Max 255 chars.
spec.cloudflared.imagePullPolicystringIfNotPresentNoImage pull policy. One of: Always, Never, IfNotPresent.
spec.cloudflared.protocolstringautoNoTunnel transport protocol. One of: auto, quic, http2.
spec.cloudflared.resourcescorev1.ResourceRequirementsnoneNoResource requests and limits for cloudflared containers. Standard Kubernetes resource spec.
spec.cloudflared.nodeSelectormap[string]stringnoneNoNode selector for cloudflared pod scheduling. Max 50 entries.
spec.cloudflared.tolerations[]corev1.TolerationnoneNoTolerations for cloudflared pods. Max 20 items.
spec.cloudflared.podAnnotationsmap[string]stringnoneNoAnnotations added to cloudflared pods. Max 50 entries.
spec.cloudflared.extraArgs[]stringnoneNoAdditional CLI arguments passed to cloudflared. Max 20 items.
spec.cloudflared.metrics.enabledbooltrueNoDeclares the metrics container port for scraping; the shared health listener remains enabled.
spec.cloudflared.metrics.portint3244483NoPort for the metrics endpoint. Min 1, max 65535. The pod listener serves both /metrics and health probes.
spec.originDefaults.connectTimeoutstring30sNoTimeout for connecting to origin/backend services. Format: `^[0-9]+(smh)$`.
spec.originDefaults.noTLSVerifyboolfalseNoDisables TLS certificate verification for origin connections. Use with caution in production.
spec.originDefaults.http2OriginboolfalseNoEnables HTTP/2 for connections to origin services.
spec.originDefaults.h2cOriginboolfalseNoEnables HTTP/2 cleartext (h2c) for origin connections. Use for origins that speak HTTP/2 without TLS. Mutually exclusive with http2Origin.
spec.originDefaults.caPoolSecretRef.namestringnoneYes (if caPoolSecretRef set)Name of the Secret containing CA certificates for origin TLS verification. 1-253 chars.
spec.originDefaults.caPoolSecretRef.keystringca.crtNoKey within the Secret containing the CA certificate chain in PEM format. Max 253 chars.
spec.fallbackTargetstringhttp_status:404NoDefault service for requests that do not match any ingress rule.
spec.fallbackCredentialsRef.namestringnoneYes (if fallbackCredentialsRef set)Name of the Secret containing fallback Cloudflare API credentials. 1-253 chars.
spec.fallbackCredentialsRef.namespacestring(resource namespace)NoNamespace of the fallback credentials Secret. Max 63 chars.

Defines the tunnel identity. The controller creates a tunnel when its name does not exist in the account. An existing unclaimed tunnel requires explicit cfgate.io/adopt-existing: "true" migration opt-in; a conflicting ownership claim is rejected. The resolved tunnel ID is stored in .status.tunnelId. See authorization and ownership for the installation-scoped claim and migration requirements.

Constraints:

  • Name must be lowercase alphanumeric with hyphens (DNS subdomain-like pattern).
  • Max 63 characters.
  • A tunnel ownership claim admits one resource within the installation namespace; it is not a cross-cluster lock.
spec:
tunnel:
name: my-cluster-tunnel

Configures Cloudflare API credentials. The controller needs either accountId (preferred, no extra API call) or accountName (resolved via API lookup, requires Account Settings Read permission on the token). The resolved account ID is cached in .status.accountId.

The secretRef must point to a Kubernetes Secret containing a Cloudflare API token (not a tunnel token). By default, the token is read from the key CLOUDFLARE_API_TOKEN. Override this with secretKeys.apiToken.

Required API token permissions:

  • Account > Cloudflare Tunnel > Edit (always required)
  • Account > Account Settings > Read (required only when using accountName)
spec:
cloudflare:
accountId: "abc123def456"
secretRef:
name: cloudflare-credentials
secretKeys:
apiToken: MY_CUSTOM_TOKEN_KEY

Or using account name resolution:

spec:
cloudflare:
accountName: "My Company"
secretRef:
name: cloudflare-credentials
namespace: shared-secrets

Controls the cloudflared daemon Deployment. The controller creates a Deployment with the specified number of replicas. Each replica establishes an independent connection to Cloudflare’s edge network, providing high availability.

Protocol selection: The auto default lets cloudflared negotiate the best protocol. Use quic for UDP-based transport (lower latency, better for unstable connections) or http2 for environments where UDP is blocked.

Metrics: Enabled by default on port 44483. The endpoint serves Prometheus-compatible metrics at /metrics on each cloudflared pod. When metrics.enabled: false, cfgate omits the declared metrics container port but retains the shared listener and HTTP probes. The listener binds to pod interfaces so kubelet probes remain reachable; disabling scraping does not firewall metrics or diagnostic endpoints. Use podAnnotations to configure Prometheus scraping and administrator-managed NetworkPolicies to restrict network access. Generated connector Pods disable service-account-token mounting because they do not use the Kubernetes API.

Generated cloudflared pods are compatible with Kubernetes restricted Pod Security by default. cfgate runs them as non-root, uses the runtime-default seccomp profile, disables privilege escalation, and drops all Linux capabilities.

spec:
cloudflared:
replicas: 3
imagePullPolicy: IfNotPresent
protocol: quic
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
nodeSelector:
node-role.kubernetes.io/edge: ""
tolerations:
- key: node-role.kubernetes.io/edge
effect: NoSchedule
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "44483"
extraArgs:
- "--loglevel"
- "debug"
metrics:
enabled: true
port: 44483

The default image is ghcr.io/inherent-design/cloudflared:2026.9.3-h2c.1@sha256:6c46ca006f9d6af5e973e59f2d71f5d6d3dc138c8a5484380d5797092171e3ad, a fork of cloudflare/cloudflared maintained at inherent-design/cloudflared. The fork adds h2cOrigin support for HTTP/2 cleartext origin connections; upstream cloudflared does not support this feature (cloudflare/cloudflared#1304).

Users who do not need h2c can override the image to upstream:

spec:
cloudflared:
image: cloudflare/cloudflared:2026.9.3

The upstream image is a no-h2c mode override only. Selecting the known cloudflare/cloudflared Docker repository, including its Docker Hub aliases, replaces h2c forwarding matches with http_status:503 and emits an IncompatibleConnectorImage warning event. Ordinary HTTP routes remain enabled. Global h2c defaults also block forwarding fallbacks. Restoring the fork image or removing the h2c configuration restores eligible forwarding. Custom images remain administrator-owned compatibility choices; their names do not prove h2c support.

Default settings for how cloudflared connects to backend services in the cluster. These apply to all ingress rules unless overridden by route-specific annotations.

caPoolSecretRef: Use this when your backend services present TLS certificates signed by a private CA. The Secret must contain the CA certificate chain in PEM format. cfgate mounts the selected Secret key into cloudflared at /etc/cfgate/origin-ca-pool/ca.pem and sends originRequest.caPool with that path in the remote tunnel configuration. If key is omitted or empty, cfgate reads ca.crt. Without this, connections to services using private CA certificates will fail TLS verification (unless noTLSVerify is set, which is not recommended for production).

The route annotation cfgate.io/origin-ca-pool can select that same managed path for a specific ingress rule. Alpha.5 rejects arbitrary annotation paths and rejects the annotation when this Secret ref is not configured, because cfgate cannot guarantee any other file exists inside cloudflared.

If the referenced Secret or key is missing, cfgate does not deploy cloudflared and marks CloudflaredDeployed=False and Ready=False.

spec:
originDefaults:
connectTimeout: "10s"
http2Origin: true
caPoolSecretRef:
name: internal-ca
key: ca-chain.pem

The catch-all service for requests that do not match any ingress rule. Defaults to returning HTTP 404. Can be set to any cloudflared-supported origin format (e.g., http://fallback-svc.default.svc.cluster.local:8080).

spec:
fallbackTarget: "http_status:404"

References a Secret containing fallback Cloudflare API credentials. Used during resource deletion when the primary credentials Secret (referenced by spec.cloudflare.secretRef) has already been deleted. This enables cleanup of Cloudflare-side resources (tunnel deletion, config removal) even if the per-tunnel credentials Secret is removed first.

The fallback Secret must contain the same key structure as the primary credentials Secret.

spec:
fallbackCredentialsRef:
name: cloudflare-admin-credentials
namespace: cfgate-system
FieldTypeDescription
status.tunnelIdstringCloudflare-assigned tunnel ID.
status.tunnelNamestringTunnel name in Cloudflare.
status.tunnelDomainstringTunnel’s CNAME target domain ({tunnelId}.cfargotunnel.com). Used by CloudflareDNS for DNS record creation.
status.accountIdstringResolved Cloudflare account ID (cached from accountName lookup).
status.replicasint32Total number of cloudflared replicas (desired).
status.readyReplicasint32Number of ready cloudflared replicas.
status.observedGenerationint64Last .metadata.generation observed by the controller.
status.lastSyncTimemetav1.TimeLast time the tunnel configuration was synced to Cloudflare.
status.lastFullReconcileTimemetav1.TimeLast successful credentials, tunnel, Deployment, and configuration reconciliation; configuration-only passes do not advance it.
status.lifecycleDependencyHashstringDigest of checked Secret identities/revisions and Deployment generation; contains no Secret data.
status.connectedRouteCountint32Number of routes currently connected to this tunnel.
status.conditions[]metav1.ConditionStandard Kubernetes conditions (see below).

The controller checks the full tunnel lifecycle at least every 30 minutes when reconciliation can complete. A full pass reads the remote configuration, including h2cOrigin, and repairs drift even when the local configuration hash matches. Applied hashes include both account and tunnel identity, so a replacement tunnel cannot inherit a prior tunnel’s applied state. Between these checks, it may synchronize configuration without repeating credential, tunnel, and Deployment operations. Changes to the referenced credential, connector-token, or origin-CA Secret, or the connector Deployment generation, invalidate this optimization. Missing dependencies also force a full reconciliation. Existing resources without the lifecycle status fields receive a full reconciliation on upgrade. The fallback deletion credential is resolved during deletion, which never uses this optimization.

ConditionDescription
ReadyTunnel is fully operational: credentials valid, tunnel exists, config synced, pods running.
CredentialsValidAPI credentials in the referenced Secret have been validated against the Cloudflare API.
TunnelReadyTunnel exists in Cloudflare (either created or adopted).
ConfigurationSyncedIngress configuration has been successfully synced to Cloudflare.
CloudflaredDeployedCloudflared pods are running and ready.
ColumnJSONPathDescription
Ready.status.conditions[?(@.type=='Ready')].statusWhether configuration is synchronized and all desired connector replicas are available (True/False/Unknown); origin reachability is not tested.
Tunnel ID.status.tunnelIdCloudflare tunnel ID.
Replicas.status.readyReplicasNumber of ready cloudflared replicas.
Age.metadata.creationTimestampAge of the resource.
apiVersion: cfgate.io/v1alpha1
kind: CloudflareTunnel
metadata:
name: prod-tunnel
namespace: cfgate-system
spec:
tunnel:
name: prod-cluster
cloudflare:
accountId: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
secretRef:
name: cloudflare-api-token
Section titled “Full-featured tunnel with HA and monitoring”
apiVersion: cfgate.io/v1alpha1
kind: CloudflareTunnel
metadata:
name: prod-tunnel
namespace: cfgate-system
spec:
tunnel:
name: prod-cluster
cloudflare:
accountName: "Acme Corp"
secretRef:
name: cloudflare-api-token
secretKeys:
apiToken: CF_TOKEN
cloudflared:
replicas: 3
protocol: quic
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
nodeSelector:
topology.kubernetes.io/zone: us-west-2a
podAnnotations:
prometheus.io/scrape: "true"
prometheus.io/port: "44483"
metrics:
enabled: true
port: 44483
originDefaults:
connectTimeout: "10s"
http2Origin: true
caPoolSecretRef:
name: internal-ca
key: ca.crt
fallbackTarget: "http_status:404"
fallbackCredentialsRef:
name: cloudflare-admin-credentials
namespace: cfgate-system

Tunnel with custom secret key and namespace isolation

Section titled “Tunnel with custom secret key and namespace isolation”
apiVersion: cfgate.io/v1alpha1
kind: CloudflareTunnel
metadata:
name: staging-tunnel
namespace: staging
spec:
tunnel:
name: staging-cluster
cloudflare:
accountId: "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4"
secretRef:
name: cf-credentials
namespace: shared-secrets
secretKeys:
apiToken: STAGING_CF_TOKEN
cloudflared:
replicas: 1
protocol: auto
tolerations:
- key: workload-type
value: tunnel
effect: NoSchedule
originDefaults:
noTLSVerify: false
connectTimeout: "30s"

The controller adds the finalizer cfgate.io/tunnel-cleanup to every CloudflareTunnel resource. When the resource is deleted, the controller attempts to delete the Cloudflare tunnel and its credentials Secret before removing the finalizer.

If cleanup fails, the controller blocks indefinitely and requeues every 10 seconds. It never removes the finalizer automatically. Before deletion has been pending for 2 minutes, the controller emits Warning events with reason CleanupFailed. After that warning threshold, failed attempts emit CleanupBlocked. This threshold does not bound API calls or stop retries. Cleanup can finish on a later retry or after credentials, permissions, or connectivity are repaired.

To skip Cloudflare cleanup and remove the finalizer immediately, set the cfgate.io/deletion-policy=orphan annotation on the CloudflareTunnel resource. The controller will leave tunnel resources in Cloudflare and remove the finalizer without attempting cleanup.

Terminal window
kubectl annotate cloudflaretunnel my-tunnel -n cfgate-system \
cfgate.io/deletion-policy=orphan

The manager’s /healthz checks process responsiveness. Its /readyz additionally waits for the controller cache to synchronize; neither endpoint tests Cloudflare or origin reachability. Connector /healthcheck and /ready distinguish process health from edge connectivity. Tunnel Ready additionally requires the current Deployment generation, exactly the desired total and updated replicas, and all desired replicas to be ready and available. Old healthy replicas or extra surge replicas do not establish completion of the current template rollout. A partial token rollout remains unready until these conditions hold.

Connector token changes update the managed Secret before changing a controlled Pod-template revision annotation. The annotation contains only the Secret UID and resource version. An unchanged token does not restart Pods; a failed Secret write does not start a rollout.

Normal tunnel deletion first scales the owned connector Deployment to zero and waits for matching Pods to terminate before deleting Cloudflare connections and the tunnel. Matching orphan or foreign Pods block cleanup rather than being deleted. Cleanup remains retryable and failures retain the finalizer. The explicit cfgate.io/deletion-policy: orphan escape skips remote cleanup and permits Kubernetes garbage collection of owned resources.

ReferenceGrant discovery distinguishes a missing optional API from authorization or connectivity failures. Transient failures receive bounded retries with a five-second request timeout; exhausted failures and missing required Gateway API resources fail startup clearly. Restart the manager after installing or removing Gateway API CRDs.

FlagDefaultMeaning
--cluster-domaincluster.localKubernetes DNS suffix used in generated backend Service URLs; a final dot is normalized.
--installation-namespacePOD_NAMESPACENamespace whose persistent UID identifies the installation for DNS ownership. Explicitly set this when running outside Kubernetes.
--cloudflare-request-timeout30sPositive maximum duration of each Cloudflare API attempt; earlier caller deadlines still apply.
--max-ingress-rules1000Positive maximum rules per tunnel configuration, including the fallback.
--max-configuration-bytes1048576Positive maximum serialized tunnel configuration size.

Each reconciliation has a two-minute deadline. The SDK performs at most two retries, and API operations and pagination preserve cancellation. The metric cfgate_controller_last_completed_reconcile_timestamp_seconds, labeled by controller name, records completed iterations including handled failures. It measures worker progress rather than successful Cloudflare changes. Tunnel lastFullReconcileTime separately records successful full lifecycle checks. Dependency events may fan out to multiple tunnels; the work limits constrain configuration construction and publication, not the number of Kubernetes objects watched.

See Connector hardening for network isolation guidance and its prerequisites.

Existing local connector Secrets and Deployments must carry the expected controller owner UID. cfgate does not overwrite foreign or unowned objects merely because their names match. Existing remote tunnels require an immutable claim ConfigMap in the installation namespace, keyed by account and tunnel ID; conflicting claims or another CloudflareTunnel referencing that identity are rejected. New tunnels acquire claims automatically. A remote lookup failure never permits adoption or deletion.

For inspected legacy tunnels, cfgate.io/adopt-existing: "true" permits acquiring an absent claim. It cannot replace a foreign claim. Claims serialize ownership only within the same installation namespace; they are not a Cloudflare-wide lock. Normal deletion verifies the claim, drains connectors, confirms remote absence, then deletes the claim with UID/resourceVersion preconditions. Orphan deletion retains the claim for explicit recovery.

Cross-namespace Gateway-to-Tunnel and credential references require explicit ReferenceGrants. See authorization and ownership for administrator RBAC, migration steps, and coordination limits.

HTTPRoutes can opt into an explicit cfgate.io/access-required: namespace/name dependency. See Access-required routing for the supported subset, grants, remote checks, deletion ordering and asynchronous limitations. Existing routes remain public unless explicitly opted in.

If ingress or Access dependency limits are exceeded, cfgate replaces the tunnel’s configuration with a single HTTP 503 response. This stops the entire tunnel from forwarding until the configuration fits, including any custom fallback target. It prevents an oversized update from preserving previously granted access. Cleanup receipts are cleared only after Cloudflare confirms withdrawal; an API failure can delay withdrawal. Reduce the configuration or raise the appropriate limit to restore forwarding.

The selected origin CA Secret key must contain PEM certificates. Changing its certificate data rolls the connector Pods so new transports load the updated trust pool; changes to other Secret keys do not trigger a rollout.

Tunnel metadata names longer than 63 characters use bounded generated labels and resource names with a hash suffix. Existing valid generated names and selectors remain unchanged. Kubernetes owner UIDs continue to determine ownership.