Skip to main content

Tuna Kubernetes operator — tunnels, domains, and Ingress as cluster resources

· 5 min

To expose a service from a cluster, you typically need a LoadBalancer, a public IP, and an ingress controller. In the cloud you pay for this, and many clusters don't have a public address at all: an air-gapped environment, kind on a laptop, k3s on a Raspberry Pi behind a provider's router.

For such clusters, we built a Kubernetes operator. Install it with a Helm chart, write a regular Ingress with the tuna class — and the service is accessible from the internet. The cluster only makes outbound connections to Tuna; no inbound ports need to be open.

Animation: the operator in the cluster opens an outbound tunnel to a Tuna node; a request to my-app.a.2na.am/api comes through the tunnel and goes to Service api, a request to the root goes to Service web
The cluster opens a tunnel to Tuna on its own, and the operator routes requests by Ingress paths.
In brief
  • A real Ingress controller. Any Helm chart that can create an Ingress is published through Tuna with a single line in values.
  • Each tunnel has its own key. It can only start tunnels, doesn't access the API, and is revoked along with the resource. The operator's own key cannot start tunnels.
  • Domains and ports as manifests. Subdomains, custom domains, and TCP ports are reserved as resources alongside the application and live in git.
  • Installation, key setup, and all resources — in the documentation.

Before: a pod with tuna http​

Publishing a cluster through Tuna was possible before: run tuna http in a pod. One of our users used this to expose ingress-nginx:

spec:
replicas: 1
strategy: {type: Recreate}
template:
spec:
containers:
- name: tuna
image: yuccastream/tuna:latest
args: [http, ingress-nginx-controller.ingress-nginx.svc.cluster.local:80, --inspect=false]
env:
- {name: TUNA_TOKEN, valueFrom: {secretKeyRef: {name: app-tuna, key: TUNA_TOKEN}}}
- {name: TUNA_SUBDOMAIN, valueFrom: {secretKeyRef: {name: app-tuna, key: TUNA_SUBDOMAIN}}}

It works, but everything is assembled by hand: the subdomain is reserved in the dashboard, the Secret is created manually, and a separate ingress-nginx sits in front of the tunnel. And TUNA_TOKEN is the entire account's token: if it leaks from the Secret, it can be used to start as many tunnels as you want on any address.

Now: a regular Ingress​

With the operator, Tuna is just another ingress controller in the cluster. The chart doesn't need to know anything about Tuna — just specify the class. Here's podinfo on a reserved subdomain:

values.yaml
ingress:
enabled: true
className: tuna
hosts:
- host: podinfo.a.2na.am
paths:
- path: /
pathType: Prefix

Paths within a single Ingress are routed to different Services: /api to the backend, / to the frontend. The operator creates one tunnel per host and applies new paths and backends without reconnecting. If the chart doesn't support setting a class, tuna can be made the default class.

The subdomain itself is also described as a manifest — a Domain resource alongside the application. Custom domains and TCP ports are reserved the same way, and for a standalone service there's a short Tunnel resource without Ingress.

A key for every tunnel​

The main thing we designed into the operator is that leaking one secret should not compromise the entire account.

  • Each tunnel has its own ephemeral key. It can only start a tunnel: one protocol, no more than one tunnel at a time, no API access. Delete the resource — the key is revoked. Keys for resources deleted while the operator was down are revoked automatically.
  • The operator's key cannot start tunnels. It only needs write access to "Endpoints and domains": reserving addresses and issuing keys for tunnels.
  • Own namespace only. Tunnel and Ingress can only publish Services from their own namespace. IP addresses, external hosts, and other namespaces are rejected — otherwise the right to create an Ingress in one namespace would grant access to everything the operator pod can see.
  • Pods without extra privileges. Non-privileged user, read-only root filesystem, no capabilities.

GitOps-friendly​

If the cluster is managed by Argo CD or Flux, the operator stays out of the way:

  • a domain or port that already exists in the account is adopted under management rather than failing with an error;
  • with reclaimPolicy: Retain, the reservation survives resource deletion — rebuild the cluster, and the domain is still there;
  • a reservation deleted manually in the dashboard is recreated by the operator.

How to try it​

Create an API key for the operator and install the chart:

helm repo add tuna https://code.tuna.am/api/packages/tuna/helm
helm install tuna-operator tuna/tuna-k8s-operator \
--namespace tuna-system --create-namespace \
--set credentials.apiKey=<API-key>

Which permissions the key needs, how to install the operator via Nelm, and where to store the key besides --set — in the documentation.

What's next​

This is the first version: API v1alpha1, resource fields may still change. Traffic currently flows through the operator pod, and tunnels reconnect during updates.

Coming soon:

  • Tunnels directly from Service. A Service of type LoadBalancer with loadBalancerClass: tuna will get a tunnel on its own, without Ingress and without a Tunnel resource.
  • Secret synchronization. The operator will sync Tuna secrets into cluster Secrets and keep them updated on changes.
  • Routing on Tuna nodes. Ingress rules will move to our nodes, traffic will stop flowing through the operator pod, and Gateway API support will follow.

Missing a feature or the operator didn't work? Contact support.

Leave feedback​

If you enjoy using Tuna, or on the contrary you are not happy with something, please leave feedback.

News​

We announce new features and posts on our Telegram channel. Subscribe so you don't miss anything.

Help​

We value our users and carefully review every request. If you have any problems with tuna, please contact us in one of the following ways: