Skip to main content

Zero trust on public networks

Inspect 1.36

Every node runs directly on the public internet, with no private network in front. Security comes from how each connection is authenticated and encrypted, not from where it comes from: no location on the network is trusted. This page walks through each protection.

Per-connection identity, not a perimeter#

Most clusters work the other way. Perimeter security puts cluster traffic on a private network, the firewall checks it at the edge, and everything inside is trusted. That holds until one service inside is compromised. From there an attacker reaches the others, because inside the perimeter nothing checks the caller's identity.

Syself Autopilot checks the caller's identity on every connection, wherever the caller is. That identity is a certificate the cluster's own certificate authority issues and checks. A compromised service inherits no trust from where it sits on the network.

Perimeter (private network) Syself Autopilot (per-connection identity)
What makes a connection trusted It came from inside the network It presented a valid certificate, checked on this connection
After one service is compromised Attacker moves sideways freely Each next connection is checked for a valid identity again
Depends on The provider's network product Nothing outside the cluster's own CA
Where nodes can run Only behind the perimeter Directly on the public internet

Why there is no private network#

Syself Autopilot gives you no private network, and none of the protection here needs one. Hetzner's private network drops packets silently at scale, so the design does not depend on it. It would not be a security boundary anyway: it hands servers addresses on a shared network with no isolation and no encryption. Provider dependency stays at servers and load balancers only. covers the decision, and is the concept behind this page.

What is on by default#

The posture is made of separate controls, and not all are on by default:

Protection Default To turn it on
Identity-based host firewall Always on Nothing to do
mTLS reverse tunnel Always on Nothing to do
Sandboxed tunnel daemons Always on Nothing to do
Certificate rotation Always on Nothing to do
API server lockdown (port 6443) Always on Applies once the control-plane load balancer has an IP
Source-CIDR allow-list at the API server Off
Cross-node pod encryption (WireGuard) Off
etcd encryption at rest Off
Pod-to-pod segmentation Default-allow

The default-deny host firewall#

Cilium runs a host firewall in default-deny on each node's host interface, enforced in eBPF, so traffic to anything not on an explicit allow-list is dropped. covers the datapath and the two network layers in full. The allow rules name cluster identities, not IP ranges, so a rule keeps matching the right peers when addresses change. Each allowed port lists its consumers:

  • The kubelet API (10250) admits other nodes, the local host, and the metrics-server pod.
  • The reverse tunnel (8180) admits other nodes and the local host, never the world.
  • etcd (2379, 2380) admits every other node of the cluster and the local host.
  • SSH on port 100 is the only port open to the internet from the node's IP; it accepts keys only, password login disabled.

The identity match decides who can connect; the TLS behind each port authenticates the connection the firewall lets through. Full map: .

API server lockdown and KubeGate#

A Cilium deny policy blocks port 6443 from the internet except from the control-plane load balancer, so the API server is not directly reachable even though the node has a public IP. The rendered object is a CiliumClusterwideNetworkPolicy named control-plane-apiserver-lockdown, and that is what you inspect on a cluster; the restrictWorldIngress toggle (on by default) controls whether it renders. It fails open: with no real load-balancer IP assigned yet, the policy does not render and 6443 stays reachable from the internet behind the API server's own authentication, so a missing IP can never lock you out.

The load balancer is the one path the lockdown leaves open, and KubeGate guards it. KubeGate is a static pod on every control plane that binds the node IP on 6443 and moves the real API server to 127.0.0.1:6443, so every load-balancer request passes through it first. By default it forwards every connection and the API server's own authentication rejects anything without a valid credential. Gate tickets and an optional source-CIDR policy sit in front of that:

  • Gate tickets are short-lived tokens signed by the cluster CA that let platform traffic and joining nodes through before they have a cluster identity. They are verified fresh on each connection and expire after 2h.
  • A source-CIDR policy restricts which other clients may reach the API server through the load balancer. It is off by default; turn it on with kubeGatePolicySecretRef. The full how-to, including why an empty list is rejected and why PROXY protocol must be active first, is .

The mutual-TLS reverse tunnel#

A node cannot be dialed from outside. The node dials out to the control plane and keeps the connection open, and the API server sends traffic back through it. That tunnel uses mutual TLS: both ends verify each other with a certificate against the cluster CA before the connection is used, so the control plane knows it is talking to a legitimate node, the node knows it is talking to the legitimate control plane, and the connection is encrypted end to end.

Certificates and cross-node encryption#

Kubelet server and client certificates are bootstrapped when a node joins and rotated automatically, so a leaked credential is useful only briefly. Pod-to-pod traffic between nodes can be encrypted with WireGuard (UDP 51871, off by default), covering cross-node traffic only; see .

Pod traffic is a separate layer#

Everything above secures the connections between your nodes and the platform. Pod-to-pod traffic is yours to segment, and it starts default-allow: on a fresh cluster any pod can reach any other pod, across every namespace. One NetworkPolicy per namespace closes that; see .