Skip to content
Login

Kubernetes

HLE runs in Kubernetes two ways, and they solve different problems. One Helm chart ships both, and either can be installed alone.

OperatorAgent
Tunnels are declared inHLETunnel CRDs and Ingress resourcesthe dashboard
SuitsGitOps — Flux, Argo, anything where the cluster is the source of truthtrying things, homelabs, clusters you administer by hand
Finds services for youNo. You name them.Yes — discovery lists every Service
Changing a tunnel meansa commita click
Processesone tunnel connection per HLETunnelone 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:

Terminal window
git clone https://github.com/hle-world/hle-operator
cd 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_here

Get 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:

Terminal window
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-agent

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.

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

ValueDefaultMeaning
agent.enabledfalseInstall 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.enabledtrueBind the read-only discovery ClusterRole
agent.env[]Extra environment, e.g. HLE_SERVER_URL for a self-hosted relay
operator.enabledtrueInstall 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

Terminal window
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-key

Then declare a tunnel:

apiVersion: hle.world/v1alpha1
kind: HLETunnel
metadata:
name: grafana
namespace: monitoring
spec:
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.