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.
Install
Section titled “Install”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.
- In the dashboard, open Connections → Agents → New and create an agent token. It is shown once.
- 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:
kubectl create namespace hlekubectl -n hle create secret generic hle-credential \ --from-literal=credential=hle_your_key_here- Install the chart, pointing it at that Secret:
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \ --namespace hle \ --set credential.existingSecret=hle-credentialcredential.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.
Pin a version
Section titled “Pin a version”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).
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \ --version <chart version> \ --namespace hle \ --set credential.existingSecret=hle-credentialThe 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.
What agent mode does
Section titled “What agent mode does”- 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.
HLETunnelresources and Ingresses withingressClassName: hleare 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. WithsyncPolicy: initialthe 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.0are always refused.
This mode is not yet covered by our automated end-to-end tests.
Declaring a tunnel
Section titled “Declaring a tunnel”apiVersion: hle.world/v1alpha1kind: HLETunnelmetadata: name: grafana namespace: monitoringspec: serviceRef: name: grafana port: 3000 label: grafana syncPolicy: strict accessControl: allowedUsers: - email: you@example.com provider: googleOr annotate an existing Ingress with ingressClassName: hle. The full CRD
reference ships with the chart.

Response timeout
Section titled “Response timeout”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: 90On 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.
Exposing something from the dashboard
Section titled “Exposing something from the dashboard”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.
Values worth knowing
Section titled “Values worth knowing”| Value | Default | Meaning |
|---|---|---|
credential.existingSecret | "" | Name of a Secret you created (takes precedence over value) |
credential.value | "" | The credential inline; the chart creates the Secret |
credential.secretKey | credential | Key within the Secret |
mode | "" | "" (auto), agent or legacy. Auto picks agent unless only apiKey.* is set |
gitops.enabled | true | Declare 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.clusterWideSecretGet | false | Cluster-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.enabled | true | Zero-drop upgrades — see below |
agent.discovery.enabled | true | Read-only discovery of Services and Endpoints. false removes the discovery RBAC |
agent.discovery.excludeNamespaces | kube-system, kube-public, kube-node-lease | Namespaces never reported (passed to the agent as HLE_DISCOVERY_EXCLUDE_NAMESPACES) |
agent.discovery.excludeLabel | hle.world/expose=denied | Namespaces carrying this label are skipped too |
agent.firepuncher.enabled | false | Off for cluster agents: a cluster agent should not double as a dialer into other machines’ services |
allowRawUrls | false | Allow 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:
kubectl label namespace secrets hle.world/expose=deniedZero-drop upgrades
Section titled “Zero-drop upgrades”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):
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \ -n hle --reuse-values \ --set handover.enabled=falseMigrating from the legacy operator
Section titled “Migrating from the legacy operator”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.
-
Create an agent token (Connections → Agents → New).
-
Upgrade with it:
Terminal window helm upgrade hle oci://ghcr.io/hle-world/charts/hle-operator \-n hle \--set credential.value=hle_your_key_heremodeis on auto, so setting acredential.*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.
Rolling back
Section titled “Rolling back”helm rollback hle -n hleor 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.
Related
Section titled “Related”- Service discovery — how the k8s and docker providers work
- Agent — enrollment, endpoints, and what an agent is
- Traffic & metrics — per-endpoint charts