A Kubernetes operator that automates Towonel self-hosted tunnels — provisioning tunnels, deploying agents, and connecting them, declaratively. It does not run tunnels itself (the Towonel hub, edges, and agent do), and it never talks to a DNS provider.
Warning
Agentically generated. This codebase was produced through agentic, spec-driven development: each feature began as a written design and implementation spec, then a coding agent executed the plan under human review. Tests, code review, and CI gates apply as they would for any project, but the authorship pattern is not a single human contributor — keep that in mind when evaluating fit for your environment.
Towonel exposes services behind NAT/CGNAT without inbound ports. This operator drives that from Kubernetes:
TowonelTunnel— the control plane. Reconciles a Towonel invite (token, authorized hostnames, TCP/UDP port reservations) against the Towonel API and owns the token Secret.TowonelAgent— a placement-specific workload that references a tunnel and renders the agent Deployment plus itshostname → originrouting. Multiple agents can share one tunnel for failover. Seedocs/architecture.mdfor the why behind the two-resource split.- Annotation source layer — annotate a
Service,Gateway, orHTTPRouteand let the operator materialize the agents for you (the dogfooding path). - Gateway auto-routes — set
towonel.io/auto-routes: "true"on a Gateway to tunnel its same-namespace HTTPRoutes without annotating each one; see Gateway auto-routes. - Cross-namespace auto-routes (optional) — a Gateway can extend auto-routes to other
namespaces with a
towonel.io/auto-routes-namespacesallowlist (off by default). Note the security implications of public cross-namespace exposure. - Direct-path connectivity (optional) — advertise a reachable address so peers connect over
a direct iroh path instead of relaying; see
docs/connectivity.md. - DNS handoff (optional) — with
spec.dns.mode: DNSEndpointthe operator emits an external-dnsDNSEndpointthat CNAMEs each hub-confirmed hostname to its region's edge; see DNS handoff. - Region endpoints status —
status.regionEndpointsreports the live edge IPs per region, with aRegionEndpointsAvailablecondition. Always on, informational only. - Agent metrics and dashboard (optional) — a cluster-wide Prometheus
PodMonitorand the upstream Grafana dashboard, both off by default.
Prerequisites: a Towonel account + a personal API key, and a Kubernetes
cluster. (Full prerequisites and chart values: docs/installation.md.)
1. Install the operator (Helm, OCI chart on GHCR):
kubectl create namespace towonel-system
kubectl create secret generic towonel-api -n towonel-system --from-literal=token='twk_xxx'
helm install towonel-operator \
oci://ghcr.io/jacaudi/charts/towonel-operator --version <version> \
--namespace towonel-system \
--set credentials.existingSecret=towonel-apiReplace <version> with the chart version you want — use the latest release.
2. Create a tunnel (the control plane — provisions the Towonel invite + token):
apiVersion: towonel.io/v1alpha1
kind: TowonelTunnel
metadata:
name: app
namespace: towonel-system
spec:
region: EU
apiKeySecretRef:
name: towonel-api
key: token
deletionPolicy: Retain3. Expose a service — either annotate it:
apiVersion: v1
kind: Service
metadata:
name: grafana
namespace: monitoring
annotations:
towonel.io/tunnel: enable
towonel.io/tunnel-ref: towonel-system/app
towonel.io/hostname: grafana.example.com
spec:
selector:
app.kubernetes.io/name: grafana
ports:
- name: http
port: 3000
targetPort: 3000…or author a TowonelAgent directly for full control (HTTPS + raw TCP/UDP, failover,
direct-path connectivity) — see docs/examples/.
4. Point DNS (optional). Set spec.dns.mode: DNSEndpoint on the tunnel and run external-dns with
--source=crd: the operator publishes a CNAME per confirmed hostname to Towonel's regional
edge. Or create the records yourself from status.dnsRecords. See docs/dns.md.
kubectl get towoneltunnel app -n towonel-system -o yaml # inspect statusSources in your cluster become Towonel tunnels and agents; the hub authorizes hostnames and the edge carries traffic home.
- Sources — annotated
Service,Gateway, andHTTPRouteobjects, orTowonelAgentandTowonelTunnelresources you write yourself. The operator watches them all. - towonel-operator — turns each source into a
TowonelAgent(hostname → origin) and keeps theTowonelTunnelin step with the hub. - Towonel hub — authorizes each hostname for one invite, reserves TCP/UDP ports, and reports live edges per region.
- Agents — Deployments the operator renders. They dial out over QUIC, so nothing listens on your network.
- Edge — routes by SNI to the tunnel that owns the hostname.
See docs/architecture.md for the data and control flow.
The operator declares the records; whatever reads DNSEndpoint objects applies them.
Off by default. Set spec.dns.mode: DNSEndpoint on a tunnel and the operator emits one
external-dns DNSEndpoint per tunnel, with a CNAME for every hostname the hub has confirmed.
The target is the invite region's edge (CA → edge-na.towonel.dev, EU → edge-eu.towonel.dev
on the public hub), or one you set with --dns-edge-target / chart dns.edgeTargets. Records are
withdrawn when the mode is Disabled, the invite is revoked or deleted, no edge target remains, or
the tunnel is deleted under deletionPolicy: Delete. To keep a hostname out of DNS (a zone apex
your provider can't CNAME, say), annotate its source towonel.io/dns-record: "false"; the hub
still authorizes it (opt-out). See docs/dns.md
for the consumer setup and the mandatory rollout order.
To pass settings the operator does not model to the Towonel agent — log verbosity, for example — set agentEnv in the chart. The variables are added to every operator-managed agent pod, hand-authored TowonelAgents and auto-created default agents alike:
agentEnv:
RUST_LOG: info,iroh::socket::transports=trace- Operator-wide, plain values. There is no per-agent override and no
valueFrom; each entry is aname: valuepair. - Changing it rolls every agent. All agent Deployments restart with the new environment.
- Operator-managed names win. A name the operator sets itself (invite token, rendered services, relay URL, connectivity, health address) cannot be overridden: the entry is ignored and each affected agent gets an
EnvIgnoredWarning event. - Bad entries stop the operator. An invalid or duplicate name fails the manager at startup rather than being dropped silently.
Operator-managed Towonel agent pods serve Prometheus metrics at /metrics on port 9090. Scraping is opt-in and off by default. Enable a single cluster-wide PodMonitor (selects all agent pods in every namespace) via the chart:
observability:
metrics:
agentPodMonitor:
enabled: true
# Most Prometheus installs only discover PodMonitors carrying a specific label:
additionalLabels:
release: kube-prometheus-stack- Requires the Prometheus Operator
PodMonitorCRD (monitoring.coreos.com/v1). With the toggle on,helm install/upgradefails on clusters without it. additionalLabelsis usually required. kube-prometheus-stack only selects PodMonitors carrying its release label; without it the PodMonitor is silently ignored — enabled but never scraped.- Namespace-scoped Prometheus. The PodMonitor selects pods in all namespaces (
namespaceSelector.any: true), but a Prometheus restricted bypodMonitorNamespaceSelectormust be allowed to read it and the agent namespaces. - Existing agents. The
metricsport reaches already-running agents only on their next config change; to cover them immediately, runkubectl rollout restart deploy -l app.kubernetes.io/name=towonel-agent. - Job label. Agent series carry
job="towonel-agent"(the PodMonitor setsjobLabel: app.kubernetes.io/name). Before the dashboard was added they carriedjob="<release-namespace>/towonel-operator-agents"; update any alert or query that matched the old value.
The chart bundles the upstream Towonel agent dashboard (streams, errors, throughput, edge sessions, process stats), unmodified. It is opt-in and off by default, and charts the series the agent PodMonitor above scrapes, so enable both:
observability:
metrics:
agentPodMonitor:
enabled: true
dashboards:
enabled: true- Grafana sidecar (default). Renders a ConfigMap labelled
grafana_dashboard: "1", which the kube-prometheus-stack / Grafana chart sidecar loads automatically. If the sidecar only watches Grafana's own namespace, setobservability.dashboards.namespaceto it. - grafana-operator. Set
observability.dashboards.grafanaOperator.enabled: trueandgrafanaOperator.matchLabelsto yourGrafanainstance's labels. The chart then renders aGrafanaDashboardCR (grafana.integreatly.org/v1beta1) referencing the ConfigMap and drops the sidecar label.helm install/upgradefails without that CRD, or ifmatchLabelsis empty. - Scraping agents yourself. Without the bundled
PodMonitor, give the agent seriesjob="towonel-agent", or the process panels stay empty.
Full docs live in docs/:
| Doc | Default | Covers |
|---|---|---|
| Installation | — | Helm (OCI) install, chart values, manager flags, prerequisites |
| Architecture | — | The two-resource model, why routing is agent-side, failover, the source layer |
TowonelTunnel |
— | Control-plane CRD reference: spec, status, conditions, lifecycle |
TowonelAgent |
— | Workload and routing CRD reference |
| Source layer | — | Expose a Service, Gateway, or HTTPRoute by annotation |
| Direct-path connectivity | off | Optional iroh NAT-traversal (spec.connectivity) |
| DNS | off | Public DNS handoff via external-dns DNSEndpoint |
| Examples | — | Runnable sample manifests |
- Bernd Schorgers (bjw-s) — this operator's Helm chart is built on
the bjw-s
app-templatecommon library, which does the heavy lifting of rendering the controller Deployment, RBAC, service account, and service. Thank you for an excellent, reusable chart! - Erwan Leboucher (eleboucher) — the creator of Towonel itself (codeberg · github). This operator only automates the client side; Towonel — the hub, edges, and agent — is his work. Thank you for creating a self-hosted Cloudflare Tunnel replacement!
- external-dns — applies the
DNSEndpointobjects this operator emits to any DNS provider, which is why the operator never needs provider credentials.
MIT © 2026 jacaudi