Skip to content

Controller Options

This document describes all configuration options for the controller binary. For Helm chart values, see Helm Values.

Command Line Flags

Flag Environment Variable Default Description
--controller-name CF_CONTROLLER_NAME cf.k8s.lex.la/tunnel-controller Controller name matching GatewayClass spec.controllerName
--cluster-domain CF_CLUSTER_DOMAIN (auto-detect) Kubernetes cluster domain. Also appended to any --proxy-endpoints host ending in .svc
--metrics-addr CF_METRICS_ADDR :8080 Metrics endpoint address
--health-addr CF_HEALTH_ADDR :8081 Health probe endpoint address
--log-level CF_LOG_LEVEL info Log level (debug, info, warn, error)
--log-format CF_LOG_FORMAT json Log format (json, text)
--leader-elect CF_LEADER_ELECT false Enable leader election for HA
--leader-election-namespace CF_LEADER_ELECTION_NAMESPACE Namespace for leader election lease
--leader-election-name CF_LEADER_ELECTION_NAME cloudflare-tunnel-gateway-controller-leader Leader election lease name
--proxy-endpoints CF_PROXY_ENDPOINTS Proxy config API endpoints for L7 proxy sync (required). A host ending in .svc gets the resolved cluster domain appended, which is how the chart passes the proxy's headless Service. The Service each one names has requirements, see Proxy Endpoints
--proxy-auth-token CF_PROXY_AUTH_TOKEN Bearer token for proxy config push authentication, as a direct value. For callers outside the chart only -- the chart always sets --proxy-auth-secret-ref instead, for both the bring-your-own and generated cases. Overridden by --proxy-auth-secret-ref when both are set
--proxy-auth-secret-ref CF_PROXY_AUTH_SECRET_REF Shared-proxy config-API auth-token Secret to resolve, in <namespace>/<name> form, and use for the controller's own push auth. Resolved directly via the API, not a pod-level secretKeyRef (which the controller creating its own dependency would deadlock on). The single mechanism the chart uses for both the bring-your-own and generated cases; combine with --proxy-auth-secret-generate
--proxy-auth-secret-key CF_PROXY_AUTH_SECRET_KEY auth-token Data key to read within the --proxy-auth-secret-ref Secret
--proxy-auth-secret-generate CF_PROXY_AUTH_SECRET_GENERATE false Allow creating the --proxy-auth-secret-ref Secret (with a random token) when it does not exist. false is the bring-your-own contract: the Secret must already exist. The chart sets this true only when proxy.authTokenSecretRef.name is empty
--proxy-config-ca-secret CF_PROXY_CONFIG_CA_SECRET Config API CA Secret in <namespace>/<name> form. Setting it turns on config API TLS: the controller creates the CA once, issues every data plane's serving certificate from it, and pushes only to planes presenting one (https:// endpoints only). Empty keeps the plaintext push and creates nothing. Set together with --proxy-config-tls-secret; the chart sets both when proxy.configAPITLS.enabled is true
--proxy-config-tls-secret CF_PROXY_CONFIG_TLS_SECRET Shared data plane's config API serving-certificate Secret in <namespace>/<name> form, created and renewed in place by the controller and mounted by the shared proxy. Set together with --proxy-config-ca-secret
--proxy-token-secret CF_PROXY_TOKEN_SECRET Tunnel-token Secret to watch in <namespace>/<name> form; the controller rolls the proxy Deployment when the Secret data changes. Empty disables the watcher
--proxy-deployment-label CF_PROXY_DEPLOYMENT_LABEL app.kubernetes.io/component=proxy Label selector (key=value) identifying the proxy Deployment(s) to roll on tunnel-token change. Per-Gateway data planes (Deployments controlled by a Gateway) are skipped: they roll when the token Secret their GatewayConfig names changes
--tunnel-protocol CF_TUNNEL_PROTOCOL auto Edge transport protocol (auto, http2, quic); used to warn when GRPCRoutes are present on an explicit quic tunnel
--ws-idle-timeout CF_WS_IDLE_TIMEOUT Idle bound (Go duration) rendered onto per-Gateway data planes: an established WebSocket session carrying no bytes in either direction for this long is closed. Empty leaves the proxy's own 1h default. The chart passes proxy.websocket.idleTimeout here, so one value covers the shared plane and the per-Gateway ones
--proxy-config-api-port CF_PROXY_CONFIG_API_PORT 8081 Config-API port per-Gateway data planes listen on, rendered into their container port, headless Service and NetworkPolicy, and used for the controller's pushes to them. The chart passes proxy.configAPIPort here when it is not 8081, so one value covers the shared plane and the per-Gateway ones
--mirror-max-in-flight CF_MIRROR_MAX_IN_FLIGHT 0 Mirror dispatches each RequestMirror filter may keep in flight, rendered onto per-Gateway data planes. 0 or less leaves the proxy's own default of 64. The chart passes proxy.mirror.maxInFlight here, so one value covers the shared plane and the per-Gateway ones
--proxy-allow-x-original-host CF_PROXY_ALLOW_X_ORIGINAL_HOST false Render per-Gateway data planes that trust the client-supplied X-Original-Host header. Test deployments only (details). The chart passes proxy.allowXOriginalHost here, so one value covers the shared plane and the per-Gateway ones; GatewayConfig has no field for it
--tracing-enabled CF_TRACING_ENABLED false Enable OpenTelemetry distributed tracing
--tracing-endpoint CF_TRACING_ENDPOINT OTLP/gRPC collector endpoint (defers to OTEL_EXPORTER_OTLP_ENDPOINT when empty)
--tracing-sample-rate CF_TRACING_SAMPLE_RATE 1.0 Head-sampling probability in [0,1]
--proxy-image CF_PROXY_IMAGE Container image for per-Gateway rendered proxy Deployments (per-Gateway isolation). When BOTH this flag and the GatewayConfig.spec.image override are empty, rendering is skipped and the Gateway surfaces a Warning Event instead of a data plane. The Helm chart always sets it; manual installs should pass it (or set spec.image per GatewayConfig)
--hostname-ownership-enforce CF_HOSTNAME_OWNERSHIP_ENFORCE false Controller-side hostname-ownership layer: routes whose hostnames fall outside their namespace's allowed-suffix label are rejected (HostnameNotPermitted) and never programmed. Independent of the chart's ValidatingAdmissionPolicy — see Multi-Tenancy
--hostname-ownership-label-key CF_HOSTNAME_OWNERSHIP_LABEL_KEY cf.k8s.lex.la/hostname-suffix Namespace label carrying the tenant's allowed hostname suffix
--hostname-ownership-namespace-selector CF_HOSTNAME_OWNERSHIP_NAMESPACE_SELECTOR Label selector (kubectl syntax) scoping which namespaces are policed; empty polices every namespace (fail-closed)
--controller-pod-selector CF_CONTROLLER_POD_SELECTOR Label selector (kubectl syntax) identifying this controller's own pods, AND'd with the controller namespace in the rendered per-Gateway NetworkPolicy; empty admits every pod in that namespace. The chart sets it from the controller's own labels
--monitoring-namespace-selector CF_MONITORING_NAMESPACE_SELECTOR Label selector (kubectl syntax) for namespaces additionally allowed to reach a per-Gateway proxy's config-API/metrics port in the rendered NetworkPolicy; empty admits the controller pod only — see Per-Gateway Isolation
--render-network-policy CF_RENDER_NETWORK_POLICY true Render the per-Gateway config-API NetworkPolicy. Wired from the chart's proxy.networkPolicy.enabled. Set false on strict CNIs where node-sourced kubelet probes are blocked by the policy's namespaceSelector ingress rule; a previously-rendered policy is then deleted

For full distributed-tracing setup, see Distributed Tracing.

Proxy Endpoints

Each --proxy-endpoints URL names the Service the controller pushes proxy config to. The chart passes its own headless proxy Service, and the controller renders one per Gateway data plane; both meet the requirements below. A Service you bring yourself must meet them too.

  • It is headless (clusterIP: None), so its name resolves to every proxy pod. A ClusterIP Service name resolves to one virtual IP, so each push reaches a single pod and the other replicas never get a config.
  • It sets publishNotReadyAddresses: true, so DNS also returns NotReady pods. A joining proxy stays NotReady until it has its first config, so without the setting DNS never returns it and it never receives that config.
  • Its name resolves through the controller's own DNS. A name that only an egress proxy set in HTTP_PROXY/HTTPS_PROXY can resolve fails the push instead of being sent through that proxy.

Environment Variables

All flags can be set via environment variables with the CF_ prefix. Dashes in flag names are replaced with underscores.

Examples:

  • --controller-name → CF_CONTROLLER_NAME
  • --log-level → CF_LOG_LEVEL
  • --leader-elect → CF_LEADER_ELECT

Cluster Domain Auto-Detection

The controller automatically detects the Kubernetes cluster domain from /etc/resolv.conf search domains. If detection fails, it falls back to cluster.local.

To override auto-detection:

--cluster-domain=my-cluster.local

Or via environment variable:

CF_CLUSTER_DOMAIN=my-cluster.local

Leader Election

For high availability deployments with multiple controller replicas, enable leader election:

--leader-elect=true
--leader-election-namespace=cloudflare-tunnel-system

Only the leader processes events; other replicas remain on standby for automatic failover.

flowchart LR
    subgraph Replicas
        R1[Replica 1<br/>Leader]
        R2[Replica 2<br/>Standby]
        R3[Replica 3<br/>Standby]
    end

    LEASE[(Lease)]

    R1 -->|holds| LEASE
    R2 -.->|watches| LEASE
    R3 -.->|watches| LEASE

Logging

Log Levels

Level Description
debug Detailed debugging information
info Normal operational messages
warn Warning conditions
error Error conditions

Log Formats

Format Description
json Structured JSON logs (recommended for production)
text Human-readable text logs (useful for development)

Example JSON log:

{
  "level": "info",
  "ts": "2024-01-15T10:30:00Z",
  "msg": "HTTPRoute reconciled",
  "route": "default/my-app",
  "hostnames": ["app.example.com"]
}

Health Endpoints

The controller exposes health endpoints for Kubernetes probes:

Endpoint Port Description
/healthz 8081 Liveness probe
/readyz 8081 Readiness probe

These are used by Kubernetes to determine pod health:

livenessProbe:
  httpGet:
    path: /healthz
    port: 8081
readinessProbe:
  httpGet:
    path: /readyz
    port: 8081

Metrics Endpoint

The controller exposes Prometheus metrics on the metrics port:

Endpoint Port Description
/metrics 8080 Prometheus metrics

See Metrics & Alerting for available metrics.