Running a service mesh without Kubernetes
Most service-mesh documentation starts with a Kubernetes cluster. The install guide assumes kubectl, the architecture diagrams show pods, and the troubleshooting section references CRDs. If you are running workloads on bare metal or plain Linux VMs, you are left to reverse-engineer what the Kubernetes-specific parts actually do and figure out the equivalent yourself. This article covers the practical differences.
What Kubernetes actually provides to a mesh
Kubernetes does three things that a service mesh depends on: it gives each workload a stable network identity (the pod IP and service DNS name), it provides a mechanism for injecting sidecars automatically (the mutating admission webhook), and it gives the control plane a watch API for tracking workload lifecycle events. When you remove Kubernetes, you need to replace all three of these yourself.
On bare metal or VMs, network identity is typically a static IP or a hostname registered in internal DNS. That is workable but requires discipline. If a workload moves, the identity needs to be updated manually or through whatever automation you have. SPIFFE-based identity sidesteps some of this by tying identity to a workload attestation rather than a network address.
Sidecar deployment without a webhook
On Kubernetes, sidecars are injected automatically by a mutating admission webhook. On bare metal, you deploy the sidecar agent alongside your process using whatever init system you have. Systemd is the common choice. The agent runs as a separate systemd unit, starts before the application unit, and intercepts traffic via iptables rules that redirect the relevant ports.
The iptables setup is the part most documentation skips. The sidecar needs to intercept outbound traffic from the application process and inbound traffic destined for the application port, without intercepting its own traffic (which would create a loop). The standard approach uses the UID of the sidecar process as an iptables owner match to exclude its own packets from redirection.
- Create a dedicated system user for the sidecar agent
- Add iptables OUTPUT rules matching the application UID, redirecting to the sidecar's inbound port
- Add iptables PREROUTING rules for inbound traffic to the application port
- Exclude the sidecar UID from the OUTPUT rules using -m owner --uid-owner
- Verify with iptables -L -n -v before starting the application
Control plane registration without a watch API
On Kubernetes, the control plane watches the API server for pod events and automatically registers and deregisters workloads. On bare metal, registration is explicit: you call the control plane's registration API when a workload starts and deregister when it stops. With systemd, this maps cleanly to ExecStartPost and ExecStopPost hooks in the unit file.
The registration payload includes the workload's SPIFFE identity, the ports it listens on, and any metadata labels you want to use in policy rules. The control plane issues an SVID to the workload after registration and begins tracking its health via a configurable heartbeat interval.
What actually gets simpler
Without Kubernetes, you lose the automatic sidecar injection and the watch API. But you gain clarity: every workload registration is explicit and auditable. There is no admission webhook to debug, no CRD version skew to manage, and no dependency on a specific Kubernetes version. For teams running stable, long-lived services on bare metal, the explicit model is often easier to reason about than the automatic one.
If you are evaluating Tuft Mesh Vault for a non-Kubernetes environment, the docs section on bare-metal deployment covers the systemd unit templates and the iptables setup in detail. The community forum has a dedicated thread for bare-metal questions.