A Kubernetes operator that automates Towonel self-hosted tunnels — provisioning tunnels, deploying agents, and connecting them, declaratively.
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 via two resources:
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.
You can author those resources directly, or annotate a Service, Gateway, or
HTTPRoute and let the operator materialize them (the dogfooding path). Set
towonel.io/auto-routes: "true" on a Gateway to tunnel its same-namespace HTTPRoutes
without annotating each one — see Gateway auto-routes.
A Gateway can extend auto-routes to other namespaces with an opt-in towonel.io/auto-routes-namespaces allowlist (off by default) — note the security implications of public cross-namespace exposure.
See docs/architecture.md for the why behind the two-resource split.
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 (currently v1.0.1).
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. The operator does not manage DNS yet — read the tunnel's status
(authorizedHostnames, portAllocations[].edge.addresses) and create the records in your
DNS provider. See docs/dns.md.
kubectl get towoneltunnel app -n towonel-system -o yaml # inspect statusTo 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/:
- Installation · Architecture
TowonelTunnel·TowonelAgentreferences- Source layer (annotations) · Direct-path connectivity · DNS
- Examples
- 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!
MIT