Kubernetes
HLE runs in Kubernetes two ways, and they solve different problems. One Helm chart ships both, and either can be installed alone.
| Operator | Agent | |
|---|---|---|
| Tunnels are declared in | HLETunnel CRDs and Ingress resources | the dashboard |
| Suits | GitOps — Flux, Argo, anything where the cluster is the source of truth | trying things, homelabs, clusters you administer by hand |
| Finds services for you | No. You name them. | Yes — discovery lists every Service |
| Changing a tunnel means | a commit | a click |
| Processes | one tunnel connection per HLETunnel | one process holding every endpoint |
If you already run Flux or Argo, use the operator. If you want to see what is in the cluster and expose it without writing YAML, use the agent. Running both is supported and sometimes correct: the operator for the services that belong in Git, the agent for the ones you are still deciding about.
The agent
Install
The chart lives in the operator repository — there is no Helm repo to add yet:
git clone https://github.com/hle-world/hle-operatorcd hle-operator
helm install hle ./chart/hle-operator \ --namespace hle --create-namespace \ --set operator.enabled=false \ --set agent.enabled=true \ --set agent.token.value=hlea_your_token_hereGet the token from Connections → Agents → New Agent in the dashboard. It is shown once.
To keep the token out of your shell history and out of Helm values, create the Secret yourself:
kubectl -n hle create secret generic hle-agent \ --from-literal=agent-token=hlea_your_token_here
helm install hle ./chart/hle-operator -n hle \ --set operator.enabled=false \ --set agent.enabled=true \ --set agent.token.existingSecret=hle-agentThe 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 it can see
agent.discovery.enabled defaults to true and binds one ClusterRole:
rules: - apiGroups: [""] resources: ["services", "endpoints"] verbs: ["get", "list", "watch"]That is the whole grant. No secrets, no pods, no write verb anywhere, and the agent never creates, patches or deletes anything in the cluster.
Set it to false and the agent stops mounting a ServiceAccount token at all
(automountServiceAccountToken: false) — it still serves tunnels, you just type
service addresses into the dashboard yourself.
Exposing something
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.
kube-system, kube-public and kube-node-lease are skipped, along with ports
named metrics, telemetry, health or probe. See
discovery for the full filtering rules.
Values worth knowing
| Value | Default | Meaning |
|---|---|---|
agent.enabled | false | Install the agent Deployment |
agent.token.value | "" | Enrollment token; the chart creates the Secret |
agent.token.existingSecret | "" | Name of a Secret you created (takes precedence) |
agent.discovery.enabled | true | Bind the read-only discovery ClusterRole |
agent.env | [] | Extra environment, e.g. HLE_SERVER_URL for a self-hosted relay |
operator.enabled | true | Install the operator, its RBAC and the CRD |
There is deliberately no agent.replicaCount. An enrollment identifies one
machine; a second pod on the same token would be a second claimant on the same
endpoints. The Deployment is fixed at one replica with a Recreate strategy.
The operator
kubectl -n hle create secret generic hle-api-key \ --from-literal=api-key=hle_your_key_here
helm install hle ./chart/hle-operator -n hle --create-namespace \ --set apiKey.existingSecret=hle-api-keyThen declare a tunnel:
apiVersion: hle.world/v1alpha1kind: HLETunnelmetadata: name: grafana namespace: monitoringspec: serviceRef: name: grafana port: 3000 label: grafana accessControl: sso: allow: ["you@example.com"]Or adopt an existing Ingress with ingressClassName: hle. Full CRD reference is
in the operator repository.
Running both
They coexist. The operator owns what its CRDs describe; the agent owns what the dashboard describes. Neither reconciles the other’s tunnels, and both can point at the same cluster and the same account.
The usual reason to do it: the operator for production services that belong in Git, and the agent for the long tail you are still evaluating — the agent’s discovery list is the fastest way to find out what that tail contains.
Related
- Service discovery — how the k8s and docker providers work
- Agent — enrollment, endpoints, and what an agent is
- Traffic & metrics — per-endpoint charts