A vCluster gives a tenant its own API server, CRDs and RBAC, and for a while that feels like the whole answer. Then someone in security asks a plainer question: where do their pods actually run?
On shared nodes, the answer is short and a little uncomfortable. They run on your nodes, under your kernel, next to everything else. The vCluster tutorial put it as virtualising the control plane, not the physics of the host nodes. vCluster does have stronger topologies, and the line between them is sharper than the names suggest. Shared Nodes and Dedicated Nodes keep tenant workloads on your nodes; Private Nodes and Standalone take them off.
Two questions, four topologies
Two questions separate the topologies: does the tenant have its own nodes, and does it have its own networking and storage (CNI and CSI)? The upstream architecture table scores all four modes on them:
| Shared Nodes | Dedicated Nodes | Private Nodes | Standalone | |
|---|---|---|---|---|
| Own nodes | No | Yes | Yes | Yes |
| Own CNI and CSI | No | No | Yes | Yes |
| Needs a host cluster | Yes | Yes | Yes | No |
It is tempting to read that as a security score running left to right. Don't. Shared and Dedicated Nodes both run tenant workloads on the host cluster, with the syncer projecting the tenant's pods into it, so they differ in placement rather than in kind. Private Nodes is where the model changes: the tenant's workers join the tenant's own cluster, and there is nothing left for the syncer to project.
We'll walk up the ladder with the demo tenant from the tutorial.
Shared Nodes: check what the tenant can see
You already have this one running. The interesting bit is how little of the host it can see. From inside the tenant:
vcluster connect demo --namespace team-a
kubectl get nodes
What comes back are pseudo-nodes. With node syncing off, which is the shared-nodes default, vCluster only shows a node in the tenant while the tenant has a pod on it, and removes it when the last pod leaves. The name is real; the rest of the node object is generated. That is deliberate: the tenant can schedule and use node-aware APIs without being handed a map of your estate.
Don't treat it as a map yourself, either. The list describes the tenant's footprint rather than your host's, so it is the wrong place to reason about zones or capacity. Node-level agents such as log shippers and storage drivers belong on the host, where the real nodes are.
Now look from the other side:
vcluster disconnect
kubectl get pods -n team-a -l vcluster.loft.sh/managed-by=demo -o wide
The NODE column is the answer to security's question. Those are ordinary host nodes, running the tenant's pods next to everything else on them. This is the right choice for dev, test and internal teams who trust each other, and it is why the tutorial was careful to call it a control-plane boundary rather than a hard security one.
Dedicated Nodes: a pool of their own
The next rung gives a tenant its own pool of host nodes. You label those nodes on the host (kubectl label node <node> tenant=team-a), then tell the tenant's syncer to use only nodes with that label:
sync:
fromHost:
nodes:
enabled: false
selector:
labels:
tenant: team-a
With enabled: false the tenant still sees pseudo-nodes. Switch it to true and the tenant sees real node information for the pool, which is more disclosure than most shared setups should offer.
It helps to be precise about what this buys. The selector confines the tenant to those nodes. It does not, on its own, keep everyone else off them; reserving a pool is ordinary host-side scheduling, with taints for instance. And the pool is still part of the host cluster. Upstream is blunt about it: a dedicated pool is still the shared-node architecture, with placement scoped rather than isolation added. The host's CNI, CSI, DaemonSets and node upgrades all remain yours.
Dedicated Nodes is a good fit when the worry is where a tenant lands, whether that is a region, an architecture, spot capacity or keeping tenants off your critical infrastructure nodes. It is a poor fit when the tenant needs to own the networking or storage layer, because it does not hand either over.
Private Nodes: where the syncer stops
Private Nodes changes the model rather than tightening it. The minimal config is short:
privateNodes:
enabled: true
controlPlane:
service:
spec:
type: NodePort
Upstream's own description of the setting is plain: vCluster stops syncing resources to the host and becomes a hosted control plane that real worker nodes join, using kubeadm or Cluster API. The NodePort service exposes the tenant's API server, because nodes outside the host cluster have to reach it to join.
That changes three things you can feel:
- Where pods run. On the tenant's own nodes, as native members of the tenant cluster. Nothing is flattened into a host namespace any more.
- Who owns the data plane. The tenant installs its own CNI and CSI, because the nodes are theirs.
-
How
kubectl logsworks. The control plane reaches the nodes through a tunnel, so the host does not need direct access to the workers.
In practice a Private Nodes tenant behaves like a single-tenant cluster whose control plane happens to be hosted somewhere else. Reach for it when the tenant needs its own network and storage stack, or when the host cluster must not be the place their workloads run.
One practical point before you plan around it. Private Nodes belongs to vCluster's Free tier rather than the pure open-source build. The tier costs nothing, but it needs a licence check through the vendor's platform, so the Apache-2.0-only image we pinned in the tutorial does not cover it. Tiers change, so read the OSS versus Free page before you design around it.
Standalone takes the same idea one step further and removes the host cluster entirely, running the control plane as a binary on dedicated machines. It suits edge and bare-metal estates, and it deserves its own post.
Host-side controls follow the topology
This is where moving up the ladder bites. On shared and dedicated nodes, anything you enforce on the host sees the flattened view. The tutorial showed the syncer collapsing every tenant namespace into one host namespace, so a host NetworkPolicy with a namespaceSelector aimed at a tenant namespace has nothing to match, because those namespaces do not exist on the host.
What the host can select on is what the syncer stamps onto the objects it creates, such as the vcluster.loft.sh/managed-by label we used above. That makes the label a handle for host policy. It also means the policy lives one layer out, where the tenant cannot edit it. That is usually what you want for a baseline, and it has a cost: a tenant who sees "denied by policy" cannot tell which host control said no. Tell them where to look.
Then you move to Private Nodes and the handle is gone. The pods are not on the host, so nothing synced carries the label. The control point moves to the tenant cluster and its nodes, and a policy you wrote for shared nodes will not follow it there. Treat the move as a redesign of the controls rather than a change of node pool.
Whichever rung you choose, the tenant-side basics still apply. The policies section of the tutorial covers the built-in quota and Pod Security settings. The upstream hardening guide covers the host-side work specific to shared nodes.
Choosing a rung
Before you pick a rung, answer two questions for each tenant. Do its workloads need their own nodes, and does it need to run its own CNI or CSI? Wherever the answer is "no, the host's", the controls at that layer are yours to write and run. Put them in writing next to the tenant's own configuration: what you enforce, where, and where a tenant should look when something is denied.
My default is Shared Nodes for tenants you trust, Dedicated Nodes once placement matters, and Private Nodes when the tenant needs to own the data plane or the host cannot be where its workloads run. Pick the rung from those two answers, and treat the tenant's view of its own cluster as the least interesting part of the decision.
The next post in this series moves on to what happens once a tenant cluster matters: backing stores, backups and high availability.
Top comments (0)