Skip to main content

Threat model

Inspect 1.36

The defense is easiest to understand from the attacker's side. Each position below asks the same things: what the attacker must achieve to win, what blocks them, what detects them, and what stays open. The positions follow one another: an internet scanner who finds an open service becomes a workload-level attacker, and a workload that escapes becomes a node-level attacker. Each section assumes the attacker already holds the position, however they reached it, and asks what it is worth. The positions map onto the , the pieces that make up a cluster. summarizes this in a table; this page works through each position in detail.

Note

Every control named here is on by default unless the text says otherwise. The gaps are named too: a limit you know about is one you can plan around.

Internet scanner#

The attacker probes the node's public IP from anywhere. Every node has one; the platform runs directly on public networks with no perimeter in front.

What they must do to win. Find a listening service on the node and exploit it, or reach the API server and find a credential that works.

What blocks them. The host firewall runs default-deny on the public interface, enforced in eBPF (which filters packets before they reach a socket). A port is not open or closed by number; each allow rule names its consumer. The kubelet port (10250) answers only to other nodes, the local host, and the metrics-server pod; the overlay (8472), WireGuard (51871), and health ports (4240, 4244) answer only to cluster identities; 6443 answers only to the control-plane load balancer, so the API server is unreachable from here even though the node has a public IP. The management tunnel is dialed outward over mutual TLS, so there is no inbound listener to find. Everything else is dropped.

What detects them. Dropped probes are not individually alarmed; default-deny makes them noise. Connections that reach the API server's front gate and fail are recorded: the logs each rejected non-TLS or unknown-source connection.

Residual. SSH on port 100 answers from anywhere until you add a deny policy (authentication is key-only, but the port is reachable); see . Any service you expose yourself is your attack surface and sits outside this section.

Compromised workload#

The attacker runs code inside a non-privileged container, the most likely position they reach.

What they must do to win. Escape to the host, load kernel code, reach node or control-plane services the pod has no business reaching, or abuse the admission chain to lock administrators out.

What blocks them.

  • Escape: every non-privileged container runs an enforcing AppArmor profile and the RuntimeDefault seccomp filter, cutting the syscall and filesystem surface an exploit needs.
  • Kernel code: MODULE_SIG_FORCE means an unsigned kernel module does not load, whatever privileges the attacker gathers.
  • Lateral movement to the host: the host firewall applies identity to pods too. Only named platform pod identities reach a host port (metrics-server to 10250, hubble-relay to 4244); no other pod identity is allowed anywhere. The component metrics ports are closed twice: absent from every allow-list, and bound to 127.0.0.1 (etcd 2381, kube-controller-manager 10257, kube-scheduler 10259, KubeGate 8080, cilium-agent 9962, cilium-operator 9963, Hubble 9965, CSI node 9189), which a pod's own network namespace can never reach.
  • Admission lockout: a built-in CEL policy, syself-restrict-tenant-webhooks, rejects any tenant webhook that targets the admission-registration, authentication, or authorization APIs, cluster-scoped RBAC, or resource wildcards; syself-protect-platform-policies keeps tenants from editing the guard itself. system:masters can always delete a broken webhook. See .

What detects them. The Kubernetes API audit log records every request the workload's service account makes; auditd records command execution in login sessions and changes to identity and control files; Hubble flow export, if enabled, records network-policy verdicts.

Residual. Pods are default-allow and no Pod Security level is set on a fresh cluster, so a compromised pod reaches every other pod until you add and . A privileged container is outside this section entirely: granting privileged: true hands over the node. Run hostile code under the , which puts the pod in its own VM.

Node-level attacker#

The attacker has root on a running node. The question is no longer whether the node is compromised but what the compromise is worth and whether it is visible.

What they must do to win. Modify the OS, persist unseen, or turn one node into the whole cluster.

What blocks them. The OS is sealed. The base, the add-on layers, and the per-node config are each dm-verity images, so root cannot edit a sealed layer in place, cannot install software, and cannot load unsigned modules. The model is keyless: authenticity comes from content-hash pinning, so the node stores no signing key an attacker could steal. The provisioner records each layer's root hash in /var/lib/syself/sealed-layers.json, and the control plane keeps its own record.

What detects them. The health daemon compares the kernel's dm-verity state against the pinned hashes and latches SealedOSTampered on a mismatch; a change to protected files under /var latches NodeTampered. Latching means reverting the change does not clear the condition, and both are published to the Node object in the workload cluster's API server, so they survive a disk wipe of the worker node. auditd's ruleset is immutable and locked at boot; root can stop the daemon, but that stop is itself an audit event. Ship the logs off-node so the record outlives the node. See .

Residual. /var stays writable because Kubernetes needs it, so it is tamper-evident, not sealed; the baseline is taken when the daemon starts, so a change made while it is down is adopted on restart. Root on a node also reads the secrets of pods scheduled there and the node's own credentials. This is inherent to Kubernetes; detection limits how long it goes unseen, but does not stop the read. Recovery is a reprovision, never a repair in place.

Disk or boot-line attacker#

The attacker can write to the disk outside the running OS: physical access, a hijacked provisioning flow, or firmware compromise.

What they must do to win. Rewrite the kernel command line on the EFI partition and point it at a replacement kernel or root hash, booting an OS they control.

What blocks them. Nothing on the node; this is the platform's stated trust boundary. Below the boot line the protection is complete: the base's dm-verity root hash sits on the kernel command line, and the kernel verifies every block against it from the first read, so every in-place edit to a sealed layer fails. The attacker cannot modify the OS you released; they can only boot a different one.

What detects them. External comparison, not the node itself. The base image builds reproducibly (fixed salt, filesystem UUIDs, timestamps), so anyone can recompute its root hash from the released image with veritysetup and compare it against the node's /proc/cmdline. The control plane knows which hashes it handed each node, so a node presenting different pins is a mismatch. See .

Residual. There is no Secure Boot and no TPM-backed measured boot, and neither is planned: a boot-line rewrite is detectable by comparison but not blocked. Physical access to the hardware is Hetzner's layer, covered by their datacenter certifications.

Supply-chain attacker#

The attacker compromises something upstream: a source tarball, the image in transit, or a component with a known vulnerability.

What they must do to win. Get modified or vulnerable code into the image a node boots, or swap the image in transit.

What blocks them.

  • Source: every component is pinned by sha256 digest. The kernel pin verifies the release tag's PGP signature against the kernel.org maintainer keys at pin time, then locks the sha256, so a compromised mirror fails the hash check.
  • Delivery: the provisioner pins each layer's dm-verity root hash from the image manifest before the node boots it, and the kernel verifies every block at runtime. A swapped image does not match its pin and does not run.
  • Known vulnerabilities: critical CVEs in the image are patched within 72 hours.

What detects them. Every release ships an SBOM and OpenVEX advisories; the OS is closed source, so the SBOM is the reviewer's evidence to audit, diff, and feed to a scanner. On the node, a delivery-path mismatch surfaces as SealedOSTampered.

Residual. The keyless model has an upside and a downside. The upside is that no signing key exists to steal. The downside is that a compromised build pipeline would pin the compromised bytes, and no signature check would flag them, because there is none. The external checks are the reproducible base build and the per-release SBOM. Verifying the images your own workloads pull is your side of the line; see .

The boot line and the shared-responsibility line#

The model organizes around the boot line and the shared-responsibility line. The boot line on the EFI partition: below it, kernel dm-verity makes the OS unmodifiable and content-hash pinning makes it verifiable; at and above it, the platform offers detection by external comparison, not prevention. The shared-responsibility line inside the cluster: the platform locks down the host and control plane by default, while pod-to-pod traffic, Pod Security levels, and image verification start open and are yours to close. An attacker looks for the gaps, so here they are once more: SSH reachable until you restrict it, default-allow pods until you segment them, a /var baseline that resets with the health daemon, and a boot line that nothing on the node defends. Each has a page on how to close it or watch it.