Skip to main content

Expose a reverse proxy without a load balancer

This page shows how to run a reverse proxy such as Traefik or nginx directly on your nodes with no Hetzner load balancer in front. For the usual setup with a load balancer, see .

By default, every Syself node runs a default-deny host firewall. How you deploy the proxy decides whether you have to open ports in it yourself. With hostPort you do not. With hostNetwork you do, and the policy has to be written correctly or part of your traffic is dropped without an error.

What changes without a load balancer 

Without a load balancer you own the address, the health checking, and the failover. Clients reach the nodes directly, and every node listed in DNS has to serve the request on its own.

  • Nothing checks health for you, so a failed node keeps receiving traffic until you change DNS. Run the proxy on more than one node and give it a PodDisruptionBudget, as in .
  • A bare-metal node keeps its address when it is reprovisioned. A cloud node gets a new one, so update its A record.
  • The proxy does see the client's real address, because nothing sits in front to rewrite it, so there is no PROXY protocol to set up. Your backends read the client from X-Forwarded-For as they would behind any proxy. See .

Run the proxy with hostPort 

Leave hostNetwork off, which is the default, and publish 80 and 443 with hostPort. The node answers on both ports, and Cilium hands each request to the proxy's pod before the checks it, so no policy is required.

The same bypass means no host rule can narrow who reaches those ports. The proxy answers on the public IP of every node it runs on, from anywhere on the internet. Restrict access in the proxy itself or with a selecting its pod.

traefik-values.yamlyaml
				service:
  enabled: false
 
ports:
  web:
    hostPort: 80
  websecure:
    hostPort: 443
 
nodeSelector:
  node.example.com/ingress: "true"
			

Set that label on the worker pool rather than with kubectl label, so nodes still carry it after they are replaced. See .

If you need hostNetwork instead 

Some proxies run with hostNetwork: true to reach a network only the node can see. The request then ends at the node, where the host firewall applies and denies every port it is not configured to allow. To allow the proxy's ports, 80 and 443, see . With a Service in front of the proxy, the rule also needs remote-node, or forwarded requests are dropped silently.

Bind ports 80 and 443 with hostNetwork 

A hostNetwork pod binds its ports in the node's namespace, where anything below 1024 is privileged. Most proxy images run as non-root, so they cannot bind 80 or 443 there. A chart that drops all capabilities strips NET_BIND_SERVICE even from root, so grant it back.

For the Traefik chart, override the ports and set both security contexts. podSecurityContext runs the pod as root, and securityContext adds the capability back to the container:

traefik-values.yamlyaml
				hostNetwork: true
 
ports:
  web:
    port: 80
  websecure:
    port: 443
 
podSecurityContext:
  runAsUser: 0
  runAsGroup: 0
  runAsNonRoot: false
 
securityContext:
  capabilities:
    drop: [ALL]
    add: [NET_BIND_SERVICE]
			

Traefik's own defaults are 8000 and 8443, which are above 1024 and need none of these settings. Override them only when the node itself has to answer on 80 and 443.

Verify it 

From outside the cluster, request each node IP you published:

				$ curl -sS -o /dev/null -w '%{http_code}\n' http://<node-ip>/
200
			

Any answer from the proxy means the path works, including a 404 before you add a route. A timeout means the host firewall dropped the packet. A refused connection means nothing was listening on that port. shows how to read the drops.