Skip to main content

Expose a reverse proxy without a load balancer

Inspect 1.36

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.

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. Allow the ports yourself:

allow-ingress-proxy.yamlyaml
				apiVersion: cilium.io/v2
kind: CiliumClusterwideNetworkPolicy
metadata:
  name: allow-ingress-proxy
spec:
  nodeSelector:
    matchLabels:
      node.example.com/ingress: "true"
  ingress:
    - fromEntities: [world]
      toPorts:
        - ports:
            - {port: "80", protocol: TCP}
            - {port: "443", protocol: TCP}
			

The ports in the rule are the ones the proxy binds. world covers a client reaching the node directly, which is the whole path on this page.

A Service in front of the proxy changes that, so add remote-node as well. The node that receives the request may forward it to a backend on another node. It replaces the client's address with its own, so the second node sees the request coming from a node rather than from the internet. Leave remote-node out and that traffic is dropped silently.

A Service also opens a NodePort on every node, but the rule does not use that number. Cilium rewrites the NodePort to the proxy's port before the host firewall sees the packet, so the rule still lists the proxy's port.

The policy is yours to keep. The Syself platform restores its own policies (base-rule-set, control-plane-rule-set, ssh-rule-set), so never edit those or reuse their names. See .

Note

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.