Skip to content
Login

Kubernetes

Verified 2026-11-13

HLE runs in Kubernetes as one Deployment: a cluster agent enrolled with an agent token. You control it from the dashboard and the hle CLI — see what is running, expose Services, set access rules, read status. If you also want the cluster to be the source of truth, HLETunnel resources and ingressClassName: hle Ingresses are declared through the same agent. This is agent mode, and it is the default.

The chart is published as an OCI artifact — there is no Helm repo to add. The chart version matches the release version, with .0 appended: release v2610.4 is chart 2610.4.0.

  1. In the dashboard, open Connections → Agents → New and create an agent token. It is shown once.
  2. Put it in a Secret, so it stays out of your shell history and Helm values. Create the namespace first, so the Secret has somewhere to live:
Terminal window
kubectl create namespace hle
kubectl -n hle create secret generic hle-credential \
--from-literal=credential=hle_your_key_here
  1. Install the chart, pointing it at that Secret:
Terminal window
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \
--namespace hle \
--set credential.existingSecret=hle-credential

credential.secretKey defaults to credential, the key used above. To let the chart create the Secret instead, use --set credential.value=hle_your_key_here (the credential then lands in your shell history). helm upgrade --install is idempotent, so the same command upgrades later; roll back with helm rollback hle -n hle.

Helm installs the newest chart for an OCI reference. Pin one with --version — chart versions are the release tag without the v, with .0 appended (release v2610.4 is chart 2610.4.0).

Terminal window
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \
--version <chart version> \
--namespace hle \
--set credential.existingSecret=hle-credential

The pod dials out. There is no Service, no Ingress, and no published port — a cluster with no inbound path from the internet stays that way.

  • One embedded agent. One connection per release carries every tunnel. The Deployment is fixed at one replica: an enrollment identifies one machine, and a second pod on the same credential would fight over the endpoints.
  • Declared from the cluster. HLETunnel resources and Ingresses with ingressClassName: hle are declared to HLE, which runs them and reports each tunnel’s status back to its resource. Access rules (allowed users, PIN, visitor basic auth) travel with the declaration. Upstream credentials are read from Secrets in the cluster and never leave it.
  • Dashboard endpoints run alongside. Endpoints you add from the dashboard keep working next to the declared ones.
  • GitOps locks in the dashboard. An endpoint declared with syncPolicy: strict (the default) is shown read-only as managed in git; the cluster is the source of truth and dashboard edits do not stick. With syncPolicy: initial the settings are applied when the tunnel is created and the endpoint stays editable in the dashboard afterwards.
  • Guardrails. The agent only tunnels to in-cluster Services. The Kubernetes API, cloud metadata addresses, node IPs, loopback and 0.0.0.0 are always refused.

This mode is not yet covered by our automated end-to-end tests.

apiVersion: hle.world/v1alpha1
kind: HLETunnel
metadata:
name: grafana
namespace: monitoring
spec:
serviceRef:
name: grafana
port: 3000
label: grafana
syncPolicy: strict
accessControl:
allowedUsers:
- email: you@example.com
provider: google

Or annotate an existing Ingress with ingressClassName: hle. The full CRD reference ships with the chart.

Grafana login page reached through an HLETunnel on its public URL

The relay waits for the local service to answer before giving up with a 504. The default is 30 seconds for an exposed service and 120 for a webhook. A service that legitimately takes longer — a media app’s interactive search, a transcode starting up — raises it per tunnel with spec.responseTimeout:

spec:
serviceRef:
name: sonarr
port: 8989
label: sonarr
responseTimeout: 90

On an Ingress, the same value goes in the hle.world/response-timeout annotation. Both are an integer number of seconds, 1 to 1200; leave them out and the tunnel default applies. A relay-side timeout answers 504 with an X-HLE-Error: response-timeout header, so it is told apart from a 504 the service itself returned.

Open Connections → Agents → Discovered. Every Service in the cluster is listed with the address the agent would use — http://grafana.monitoring.svc.cluster.local:3000 and the like. Pick one, adjust the label, save. It is an ordinary endpoint from then on: same access controls, same metrics. See discovery for the filtering rules.

Discovery is a read-only view of Services and Endpoints. The agent never creates, patches or deletes anything in the cluster.

ValueDefaultMeaning
credential.existingSecret""Name of a Secret you created (takes precedence over value)
credential.value""The credential inline; the chart creates the Secret
credential.secretKeycredentialKey within the Secret
mode"""" (auto), agent or legacy. Auto picks agent unless only apiKey.* is set
gitops.enabledtrueDeclare HLETunnel resources and hle Ingresses. false gives a pure dashboard/CLI agent
gitops.secretResourceNames[]Secret names the agent may get for visitor basic auth. Get-only, by name
gitops.clusterWideSecretGetfalseCluster-wide Secret get. The widest grant in the chart; prefer secretResourceNames
scope.namespaces[]Namespaces the agent may see and expose. Empty is cluster-wide; when set, access is granted with per-namespace Roles instead of a ClusterRole
handover.enabledtrueZero-drop upgrades — see below
agent.discovery.enabledtrueRead-only discovery of Services and Endpoints. false removes the discovery RBAC
agent.discovery.excludeNamespaceskube-system, kube-public, kube-node-leaseNamespaces never reported (passed to the agent as HLE_DISCOVERY_EXCLUDE_NAMESPACES)
agent.discovery.excludeLabelhle.world/expose=deniedNamespaces carrying this label are skipped too
agent.firepuncher.enabledfalseOff for cluster agents: a cluster agent should not double as a dialer into other machines’ services
allowRawUrlsfalseAllow IP literals and non-cluster hostnames as targets. Off by default: a cluster agent exposes Services by name
relay.host, relay.port""Relay override for a self-hosted server

Keep a namespace out of discovery with the label:

Terminal window
kubectl label namespace secrets hle.world/expose=denied

A helm upgrade uses RollingUpdate (maxSurge=1, maxUnavailable=0): the new pod connects alongside the old one, HLE hands the agent over, and only then does the old pod stop, so tunnels don’t drop. If you run your own relay older than 2610.19 and an upgrade stalls with two pods (the new one restarting with “token already in use”), fall back to replacing the pod (Recreate, a few seconds of downtime per upgrade):

Terminal window
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \
-n hle --reuse-values \
--set handover.enabled=false

The previous operator authenticated with a full-account apiKey and ran one tunnel connection per resource. Agent mode replaces it with a tunnel-scoped credential and no account-wide key.

  1. Create an agent token (Connections → Agents → New).

  2. Upgrade with it:

    Terminal window
    helm upgrade hle oci://ghcr.io/hle-world/charts/hle-operator \
    -n hle \
    --set credential.value=hle_your_key_here

    mode is on auto, so setting a credential.* value flips the release to agent mode. The new pod declares the same labels the old operator pod still holds; those are briefly reported as conflicts and claimed automatically once the old pod has stopped, usually within a minute.

Existing apiKey.* and agent.token.* values still work and are mapped onto credential, and Secret names are unchanged, so an old release keeps its Secret references. An install configured with only apiKey.* stays on the legacy operator until you set a credential. If you ran the separate headless agent too, pass the same hle_ key you enrolled it with and set agent.enabled=false: its dashboard endpoints carry over. A different key leaves them with the old agent.

Terminal window
helm rollback hle -n hle

or install with --set mode=legacy to pin the legacy operator. The legacy operator resumes. The cluster’s rows stay in your account but nothing connects with that enrollment, so nothing runs twice; upgrading again declares them again. Legacy mode is deprecated.