You already know what's running on your cluster. Your tunnel tool doesn't — so you look up an address, copy it wrong, and debug a 502. Now the agent reads the inventory itself and hands you a list to pick from.
Every tunnel needs a target. On a laptop that's localhost:3000 and nobody
thinks about it. On a Kubernetes cluster it's
http://grafana.monitoring.svc.cluster.local:3000 — four parts, each of
which you have to be right about, none of which you remember.
So you go and look. kubectl get svc -A, squint, find the port, notice it's
named http-web and not the number you assumed, type it into the dashboard, get
a 502, and start again. On Docker it's the same story with different spelling: is it the
published host port, the container port, the compose service name, or the IP that changes
every time the container is recreated?
None of this is hard. It's just friction, repeated once per service, and it's the reason the fifth thing you meant to expose never gets exposed.
An HLE agent now inventories what is running around it and reports the list to the dashboard. You pick something from it, and it becomes a tunnel. No address to look up, no port to get wrong.
Run it on the machine itself to see exactly what the dashboard will show:
hle agent services
Discovered services (k8s, docker)
Name Where Address Suggested label
grafana monitoring http://grafana.monitoring.svc… grafana
gitea git http://gitea.git.svc.cluster… gitea
plex media http://172.18.0.4:32400 plex
The address is always from the agent's point of view. A cluster-internal DNS name is exactly right there, and is exactly what gets stored — the agent is inside the cluster, so that name resolves for it even though it means nothing on your laptop.
Two providers ship today, and they self-detect. An agent on a plain VM reports nothing and the dashboard hides the section rather than showing you an empty table with an explanation you didn't ask for.
The HLE operator chart now runs one agent by default. Create a token under Connections → Agents → New (it's shown once), keep it in a Secret, and install:
kubectl create namespace hle
kubectl -n hle create secret generic hle-credential \
--from-literal=credential=hle_your_key_here
helm upgrade --install hle oci://ghcr.io/hle-world/charts/hle-operator \
--namespace hle \
--set credential.existingSecret=hle-credential
Prefer GitOps? HLETunnel resources and ingressClassName: hle
Ingresses are declared through the same agent, and show up read-only in the dashboard.
Set gitops.enabled=false for a pure dashboard agent.
The pod dials out. There's no Service, no Ingress, no published port, and no inbound path into the cluster — which was the point of using HLE in the first place.
The permission it gets is one ClusterRole, and this is the whole of it:
rules:
- apiGroups: [""]
resources: ["services", "endpoints"]
verbs: ["get", "list", "watch"]
No secrets. No pods. No write verb anywhere. And if you'd rather not grant even that, set
agent.discovery.enabled=false — the pod then stops mounting a
ServiceAccount token altogether, and you type addresses in by hand as before.
kube-system, kube-public and kube-node-lease never
appear in the list. Neither do ports named metrics, telemetry,
health or probe. Cluster plumbing isn't what you're looking for,
and in kube-system's case it's actively dangerous to put on the internet.
The same agent, on a Docker host, reads running containers and their ports:
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
Where a container has a network alias, that's preferred over its raw IP — so the endpoint keeps working after the container is recreated with a different address, which is the failure mode you'd otherwise hit a week later and blame on HLE.
containers
list and inspect — and the mount should be :ro. But
mounting it at all is a real trust decision, so it stays opt-in and off by default. If that
trade isn't worth it to you, don't mount it: everything else still works.
In the dashboard, open Connections → Agents → Discovered. Pick a service. The discovered address becomes the target and the suggested label becomes the subdomain — both editable before you save.
From there it's an ordinary endpoint. It appears in your tunnel list, obeys the same access controls as everything else, gets the same traffic chart, and the agent starts serving it within seconds. Nothing about the resulting tunnel remembers that it was discovered rather than typed.
Discovery is the imperative path: see what exists, click, done. That's the opposite of what
you want if your cluster is reconciled from a Git repository, and the
operator is still the answer there
— HLETunnel CRDs, Ingress adoption, and a commit for every change.
They coexist deliberately. The same chart installs either or both, and neither reconciles the other's tunnels. A common shape: the operator for production services that belong in Git, the agent for the long tail you're still deciding about — where the discovery list is the fastest way to find out what that tail contains.
Free tier, no card. Discovery is read-only and off unless you grant it.