Cloud Architecture·8 min read·

Multi-Tenant Isolation With Separate VPS Nodes

Put each tenant on a separate VPS node so disk, network, and process blast radius stay bounded when a noisy neighbor fails or is compromised.

NB

Netbay Engineering

Netbay Engineering

On this page

If you host more than one customer on the same kernel, you do not have tenants. You have roommates. A runaway query, a leaked application secret, a fork bomb, or a CVE in a shared interpreter becomes everyone else's incident in the same second. The operational architecture that actually isolates tenants on a VPS fleet is boring on purpose: one tenant, one node, one public IP, one firewall, and a control plane that never shares a filesystem with customer code.

The Isolation Boundary That Matters

Containers on a shared host still share a kernel. Application-level tenancy still shares a database process. Process-level tenancy still shares memory and a disk quota that a single writer can exhaust. Those models are cheaper until the day they are not. A separate VPS node is a hard boundary: its own kernel, its own High-Speed SSD volume, its own vCPU budget on Intel Xeon Platinum, and its own network stack as seen from the guest. A noisy tenant can saturate its own CPU and fill its own disk. It cannot fill yours.

This is not a managed cluster product. Netbay does not sell a Kubernetes control plane you dump pods into. Isolation here means you provision a distinct Linux VPS per tenant, or per trust domain such as production versus staging, or payments versus marketing. You treat the node as the unit of blast radius, because that is the unit you can rebuild, snapshot, and shut down without a meeting.

Map Tenants to Nodes, Not Rows

Keep a small control table that maps tenant_id to node identity. The control plane lives on a VPS you never give customers SSH to. DNS and the edge proxy are generated from that table so a new tenant is a row plus a reload, not a late-night config edit that only exists in one engineer's head.

sql
CREATE TABLE tenant_nodes (
  tenant_id     text PRIMARY KEY,
  hostname      text NOT NULL UNIQUE,
  public_ip     inet NOT NULL,
  ssh_user      text NOT NULL DEFAULT 'deploy',
  plan_code     text NOT NULL,
  status        text NOT NULL CHECK (status IN ('provisioning','live','draining','retired')),
  created_at    timestamptz NOT NULL DEFAULT now()
);

When a request arrives, the edge looks up the tenant from the Host header and forwards to that node's address. Do not keep that mapping only as comments in an nginx file. If the table and the live proxy disagree, you will route a paying customer onto a neighbor's origin and spend the afternoon proving it was a typo.

Network and Disk Are the Real Walls

Each tenant node gets its own public IPv4, an nftables or ufw default-deny policy, and a dedicated High-Speed SSD volume that you snapshot on a schedule the tenant cannot disable. L3/L4 DDoS filtering at the network edge still does not excuse an open Redis or an unbound PostgreSQL. Private traffic between your control plane and tenant nodes should not ride the open internet if you can avoid it. If nodes only have public IPs, restrict source addresses and use WireGuard or SSH tunnels. Never bind data stores to 0.0.0.0.

bash
# On each tenant node: default deny, SSH from jump host, HTTPS from the world
ufw default deny incoming
ufw default allow outgoing
ufw allow from 203.0.113.10 to any port 22 proto tcp
ufw allow 443/tcp
ufw --force enable

# Confirm nothing else is listening on public interfaces
ss -lntup

Disk isolation is not a courtesy quota inside a shared volume. It is a volume the hypervisor will not let another guest write to. When tenant A fills /var/lib/postgresql, tenant B's checkpoints still complete. That sentence is the entire point of paying for a second node.

Blast Radius Drills

Write the failure modes down and rehearse them:

  • Tenant A fills disk: Tenant B's writes still succeed.
  • Tenant A is compromised: you snapshot, rebuild from image, rotate only that tenant's secrets. Other tenants keep their keys.
  • Tenant A needs a noisy migration: you size that node up without touching the fleet.

The drill teams skip is the rebuild. If restoring a tenant means restoring a shared database dump that also contains every other customer, you do not have isolation. You have a very expensive backup. Per-tenant Postgres on that tenant's node, or a schema you can dump and restore independently, is the practical bar. Shared-everything tenancy is a product decision, not an accident of how the first prototype was deployed.

Tenant equals one VPS node Edge nginx Host header map Control plane tenant_nodes table Tenant A node kernel + SSD + IP own Postgres Tenant B node kernel + SSD + IP own Postgres Tenant C node kernel + SSD + IP own Postgres Lucknow DC01 no shared kernel

Provisioning Is a Script, Not a Wiki

A new tenant should be a loop you can run twice without creating two half-configured boxes. Create the VPS, wait until SSH answers, apply the base image, insert the mapping row, reload the edge, and hit that hostname with a smoke check. If any step fails, mark the row provisioning and do not route traffic.

The control plane is allowed to know every tenant. Customer code is not. Do not install a panel agent that can list sibling IPs. Do not mount a shared NFS of everyone's uploads. Do not put a master SSH key for the whole fleet in a tenant's environment. Least privilege starts at the node boundary and is then tightened inside it.

Takeaway

Shared-kernel multi-tenancy is a cost optimisation that steals from your incident budget. Separate VPS nodes give you a rebuild unit, a snapshot unit, and a billing unit that match how customers actually fail. Size the node to the tenant, not the tenant to a leftover slice of a crowded box.

You can provision a dedicated Ubuntu 24.04 VPS per tenant on Netbay in Lucknow (DC01) in under 60 seconds and keep the blast radius honest — netbayhosts.in.

Keep reading

Follow along on a real VPS

Deploy Linux in under 60 seconds

These guides are written against Ubuntu, Debian, and RHEL-family images — the same ones on NetBay.

Deploy an instance