Zero trust on public networks
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. Minimal provider dependency covers the decision, and the security model 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 | Restrict API server access |
| Cross-node pod encryption (WireGuard) | Off | Encrypt pod traffic with WireGuard |
| etcd encryption at rest | Off | Encrypt etcd |
| Pod-to-pod segmentation | Default-allow | Write network policies |
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. Networking and Cilium 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
100is 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: Ports and listeners .
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 Restrict API server access .
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.
The tunnel is two host daemons, the server on control planes and the agent on every node, and both run in a strict systemd sandbox: no new privileges after start (NoNewPrivileges), a read-only filesystem except one state directory (ProtectSystem=strict), zero Linux capabilities (an empty bounding set), a restricted syscall filter (SystemCallFilter=@system-service), and no writable-executable memory (MemoryDenyWriteExecute). You can verify the directives in the unit files at /usr/local/lib/systemd/system/syself-tunnel-agent.service and syself-tunnel-server.service. The sandbox limits the damage: a bug in the tunnel is still a bug, but the exploit lands in a process with no capabilities and one writable directory.
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 Encrypt pod traffic with WireGuard .
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 Segment with network policies .
What Syself can and cannot do in your cluster
The automation provisions and reconciles your cluster and touches none of your data or workloads, with concrete limits you can verify yourself.
Node and OS security
How every server running your workloads is protected: a sealed OS that cannot change at runtime, a default-deny firewall, key-only SSH, and a BSI C5 audit trail.