Skip to content
Go back

Capsule: Multi-Tenancy for Home Lab k3s

· Updated:
By SumGuy 14 min read
Capsule: Multi-Tenancy for Home Lab k3s
Contents

You Gave Someone cluster-admin and Now You Regret It

You’ve got a k3s node, maybe two, humming away in your homelab. Your friend wants to deploy a side project. Your partner wants to spin up a wiki. A dev environment needs to share the same cluster because building another VM feels like overkill.

So you do the sensible thing: you create a kubeconfig and hand it over. Then you realize you gave them cluster-admin. Now they can list your secrets. Delete your ingresses. Accidentally kubectl delete ns production (yes, even in a homelab, this hurts).

Bare RBAC roles are the other option, and if you’ve tried to handcraft a Role and RoleBinding that gives someone “everything they need but nothing they shouldn’t have,” you know it’s a miserable experience. You’ll forget to add services/finalizers or some other nonsense and spend 45 minutes debugging why helm install keeps erroring.

There’s a better path: Capsule.


What Capsule Actually Is

Capsule is a Kubernetes operator built by Clastix, now a CNCF Sandbox project. It introduces a Tenant CRD that wraps a set of namespaces under a logical owner boundary. From the tenant owner’s perspective, they feel like a cluster-admin for their slice of the cluster. From the actual cluster’s perspective, they’re locked inside a well-defined box.

The core idea:

Capsule is not virtualization. It is soft multi-tenancy with real admission-time enforcement, which is the right fit for a homelab where you trust people, but not that much.


Installing Capsule on k3s

Standard Helm install, but check your Kubernetes version first. Capsule supports only the latest minor release of Kubernetes, and the 0.14 line requires 1.36.0 or newer:

Terminal window
kubectl version

If that reports 1.36 or later, install the current chart. As of September 2026 that is 0.14.3:

Terminal window
helm install capsule oci://ghcr.io/projectcapsule/charts/capsule \
--namespace capsule-system \
--create-namespace \
--version 0.14.3

On an older k3s release, stay on the 0.13 line (--version 0.13.11) until you upgrade the cluster. Installing 0.14 against 1.35 or earlier is not supported upstream, and the admission webhook is the last component in your cluster you want behaving oddly.

Wait for the controller pod to come up:

Terminal window
kubectl -n capsule-system get pods -w

That’s it. No CRD pre-install dance, Helm handles it. At this point the cluster runs exactly as before. Capsule only starts enforcing anything once you create Tenant objects.

k3s-specific note

k3s ships Traefik as the default ingress controller, running in kube-system. When you configure ingressOptions in your Tenant spec, use traefik as the class name unless you’ve replaced it with ingress-nginx or something else. Capsule is agnostic about which controller you run, it just needs you to name the classes you permit.


Your First Tenant

Let’s make a tenant for Alice, who wants to run a couple of apps.

tenant-alice.yaml
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: alice
spec:
owners:
- name: alice
kind: User
namespaceOptions:
quota: 3 # Alice can create up to 3 namespaces
resourceQuotas:
scope: Tenant
items:
- hard:
requests.cpu: "4"
requests.memory: 8Gi
limits.cpu: "8"
limits.memory: 16Gi
limitRanges:
items:
- limits:
- type: Pod
max:
cpu: "2"
memory: 4Gi
min:
cpu: 50m
memory: 64Mi
networkPolicies:
items:
- podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
# Alice's own namespaces can talk to each other
- namespaceSelector:
matchLabels:
capsule.clastix.io/tenant: alice
# k3s runs Traefik here. Without this, her Ingress returns 502.
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
egress:
- to:
- ipBlock:
cidr: 0.0.0.0/0
ingressOptions:
allowedClasses:
allowed:
- traefik
allowedHostnames:
allowedRegex: "^.*\\.alice\\.lab\\.local$"
storageClasses:
allowedRegex: "^(local-path|longhorn)$"
containerRegistries:
allowedRegex: "^(docker\\.io|ghcr\\.io|registry\\.alice\\.lab\\.local)$"

Two details in that spec are easy to get wrong, so let’s call them out.

allowedClasses takes a list of names under allowed. It also accepts allowedRegex, a default, and label selectors under matchLabels and matchExpressions. matchLabels is a map of label keys to values, so a bare list of class names underneath it fails validation. If you only want to name classes, allowed is the field you want.

The second one bites at runtime instead of at apply time. A podSelector: {} ingress rule scoped to Alice’s own tenant label blocks every other source in the cluster, and that includes the ingress controller. Her pods stay Running, her Ingress object looks correct, and every request through Traefik returns 502. The kube-system selector above is what lets the controller reach her workloads. If you moved Traefik into its own namespace, point the selector there instead.

One more thing about this spec, and it matters when you upgrade. Capsule is moving policy enforcement into a newer rules and replications system, and in 0.14.3 several of the fields above carry deprecation notices: resourceQuotas, limitRanges, networkPolicies, containerRegistries, podOptions, and the metadata fields under namespaceOptions. All of them still work in 0.14.3. Upstream also describes the replacement as not final yet, which is why this walkthrough uses the older fields. Read the rules documentation before you jump a minor version, because this is where the breaking change will land.

Apply it:

Terminal window
kubectl apply -f tenant-alice.yaml
kubectl get tenants

Now when Alice creates a namespace, she authenticates as the alice user in her kubeconfig. Capsule’s webhook intercepts the namespace creation, checks whether she’s a tenant owner, and if she is, mutates the namespace to attach the tenant label and injects the quotas automatically.

Terminal window
# As alice (her kubeconfig context)
kubectl create namespace alice-blog
kubectl create namespace alice-wiki

Both namespaces get:

Alice can do what she likes inside those namespaces. She cannot use storage classes or ingress classes outside the allowlist. If she tries to pull an image from quay.io, the admission webhook rejects the pod.


Capsule Proxy: Making kubectl Work At All

Plain Capsule has a rough edge, and it is the opposite of what most people expect. Listing namespaces is a cluster-scoped operation. Alice is scoped to her tenant, so she holds no cluster-scoped list permission, and the API server turns her down flat:

Error from server (Forbidden): namespaces is forbidden: User "alice"
cannot list resource "namespaces" in API group "" at the cluster scope

She can work inside alice-blog all day if she names it explicitly. She just can’t ask the cluster what she owns. kubectl get ns fails, and so does anything that enumerates namespaces first, which covers most dashboards and a fair amount of tooling.

Capsule Proxy solves that. It’s a lightweight HTTPS gateway that sits in front of the Kubernetes API server, intercepts cluster-scoped list and watch calls, and answers them with the subset the caller actually owns. As of September 2026 the current chart is 0.14.1:

Terminal window
helm install capsule-proxy oci://ghcr.io/projectcapsule/charts/capsule-proxy \
--namespace capsule-system \
--version 0.14.1

Alice’s kubeconfig points at the Capsule Proxy endpoint instead of the real API server. She runs kubectl get ns and gets back alice-blog and alice-wiki, with no Forbidden error and no sight of anyone else’s namespaces.

The proxy terminates its own TLS, so it needs a certificate. The chart generates one through a post-install job or consumes one from cert-manager, and the project documentation covers wiring the kubeconfig to match. Node visibility is a separate switch: you decide whether tenant users can list nodes. In a homelab you probably allow it, because it isn’t sensitive and it answers “why won’t my pod schedule.”


Locking It Down with Pod Security Standards

Capsule does not implement its own pod security engine. It propagates the labels that drive the built-in Kubernetes Pod Security Admission controller, and Kubernetes does the enforcing. The restricted profile blocks:

You set it through the same namespaceOptions block from earlier. Replace the block you already have, because a second namespaceOptions key in one spec is a YAML duplicate key:

tenant-alice.yaml (namespaceOptions, replacing the earlier block)
namespaceOptions:
quota: 3
additionalMetadata:
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted

Capsule stamps those labels onto every namespace the tenant owns, including ones Alice creates later. The Kubernetes admission controller does the rest. Alice can’t deploy a container running as root, and she gets an error naming the specific field that violated the policy.

One sharp edge: podOptions in the Tenant spec carries additionalMetadata only, meaning annotations and labels stamped onto tenant pods. There is no podOptions.securityContext, and no per-tenant forbiddenSysctls or allowedUnsafeSysctls. Invent fields under podOptions and server-side validation rejects the manifest, while older clients prune them silently, which is worse. Sysctl restrictions come from the PSS profile and the kubelet’s allowed-unsafe-sysctls flag, not from the Tenant.

This is where Capsule earns its keep over hand-rolled RBAC. Wiring PSS enforcement, resource quotas, and network isolation together across a dozen namespaces with raw RBAC and LimitRange objects is a weekend you don’t want to spend.


Pairing Capsule with ArgoCD

If you’re running ArgoCD on the same cluster, give each tenant their own AppProject. Alice can then deploy only to her namespaces, and only from approved Git repos.

appproject-alice.yaml
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: alice
namespace: argocd
spec:
description: Alice's tenant project
sourceRepos:
- "https://github.com/alice/*"
- "https://gitea.alice.lab.local/*"
destinations:
- namespace: "alice-*"
server: https://kubernetes.default.svc
clusterResourceWhitelist: [] # no cluster-level resources
namespaceResourceBlacklist:
- group: ""
kind: ResourceQuota
- group: ""
kind: LimitRange
roles:
- name: alice-deployer
policies:
- p, proj:alice:alice-deployer, applications, *, alice/*, allow
groups:
- alice

The destinations pattern alice-* only holds up if Alice’s namespaces really are named that way, and Capsule can guarantee it. Set forceTenantPrefix: true at the top level of the Tenant spec and every namespace she creates must be named alice-<something>. kubectl create ns blog gets rejected. kubectl create ns alice-blog goes through.

spec:
forceTenantPrefix: true

That is a top-level boolean, not part of namespaceOptions. The forbiddenLabels and forbiddenAnnotations fields under namespaceOptions do something different: they stop tenants from setting particular metadata keys on their namespaces. Useful, and unrelated to naming.

One thing to keep straight about this pairing: ArgoCD applies manifests as its own service account, not as Alice. Capsule’s owner checks never see her identity in that path, so the AppProject is what bounds where ArgoCD may deploy on her behalf. The ResourceQuota, LimitRange, and NetworkPolicy objects Capsule wrote into her namespaces are ordinary Kubernetes objects, so they still apply to whatever lands there, whoever submitted it.

With that in place, Alice pushes to her Git repo, ArgoCD picks it up, deploys only to alice-* namespaces, and her quotas and network policies apply on the way in. Clean.


How Capsule Compares to the Alternatives

A few other projects live in this space.

vCluster gives each tenant an actual virtual Kubernetes cluster with its own API server and scheduler, running as pods in the host cluster. Isolation is much closer to complete, and it is heavier than Capsule. It is also lighter than its reputation suggests. The upstream chart requests 200m CPU and 256Mi of memory for a control plane, and idle usage in practice lands around 300Mi to 500Mi. Three tenants cost you under a gigabyte, not the six you might budget from guesswork. Capsule’s overhead is one operator pod, so it still wins on resources, by a smaller margin than the folklore claims. Pick vCluster when tenants need their own CRDs or their own API server version.

HNC (Hierarchical Namespace Controller) came out of Kubernetes SIG Multi-tenancy and let namespaces form parent and child relationships with policy propagating downward. It is retired. The repository was archived in April 2025 and now sits in the kubernetes-retired GitHub organization. Don’t build anything new on it.

vCluster Platform is the commercial console in this space, and the naming has moved twice, so older write-ups are confusing. The kiosk project was archived in April 2024. Loft Labs renamed itself vCluster Labs, loft.sh redirects to vcluster.com, and the management product is now vCluster Platform. Look at it if you want a web UI and a support contract. You are taking on an external dependency and a paid tier if you grow. Capsule is upstream open source, and the YAML surface area is manageable once you’ve read it through once.

For a homelab k3s cluster where you want to give two to five people their own space without building another VM or paying for anything, Capsule is the call.


The Honest Limits

Capsule is soft multi-tenancy enforced through admission webhooks. That means:

For a homelab with trusted users who want isolation because accidents happen, that’s fine. You’re not running a public cloud. You’re stopping your brother-in-law from deleting your Jellyfin namespace, not defending against a nation-state.

Capsule also needs your users to authenticate as distinct identities. If everyone shares one kubeconfig, Capsule can’t tell them apart and every rule collapses into a single tenant. In k3s you can issue separate ServiceAccount tokens or generate client certificates per user against the built-in CA. The Capsule docs walk through both.


Should You Bother?

If your k3s cluster is just for you: no. Capsule adds complexity you don’t need.

If you want to share your cluster with one or two people and you’re currently doing it by handing out cluster-admin or hand-writing RBAC: yes. The install is ten minutes, the Tenant CRD is readable YAML, and what you get (namespace isolation, resource quotas, registry allowlisting, ingress class control) would take you a weekend to rebuild from raw Kubernetes primitives.

This fits the homelab that’s already running k3s and ArgoCD, where you want to hand a friend their own slice without babysitting them. That’s what Capsule was built for.

Add Capsule Proxy so tenants can list what they own. Pair it with ArgoCD AppProjects if you’re doing GitOps. Add the PSS restricted labels so nobody is running privileged containers in your cluster while you sleep.

It’s the multi-tenancy setup that doesn’t make you regret adding housemates to your homelab.

Common Questions

Does Capsule work on a single node k3s cluster?

Yes. Capsule is one controller pod plus admission webhooks, so it runs fine on a single node. Node count changes nothing about how tenants are enforced. The real limit on one node is the resource quotas you hand out, because every tenant competes for the same CPU and memory.

What Kubernetes version does Capsule need?

Capsule 0.14 requires Kubernetes 1.36.0 or newer. The project supports only the latest minor release upstream, so it moves quickly. On an older k3s cluster, pin the chart to the 0.13 line rather than running 0.14 against an unsupported API server, then upgrade k3s before upgrading Capsule.

Do tenants need Capsule Proxy to use kubectl?

Tenants need Capsule Proxy for anything that lists namespaces. Without the proxy, kubectl get ns returns a Forbidden error, because listing namespaces is a cluster-scoped call. Named operations still work, so kubectl -n alice-blog get pods succeeds either way. Most dashboards enumerate namespaces first, so they need the proxy.

Can Capsule stop a tenant from deleting their own namespace?

Yes. Tenant owners get the admin and capsule-namespace-deleter cluster roles by default. Drop the second one by setting clusterRoles: ["admin"] on the owner and namespace deletion stops being permitted. A separate preventDeletion: true flag protects the Tenant object itself from accidental deletion.

Is Capsule enough to host untrusted users?

No. Capsule enforces policy at the Kubernetes admission layer, so it does not isolate the kernel, the container runtime, or etcd. One container escape crosses every tenant boundary at once. For untrusted workloads, add a sandboxed runtime such as gVisor or Kata Containers, or give each tenant a separate cluster.


Share this post on:

Send a Webmention

Written about this post on your own site? Send a webmention and it'll show up above once verified.


Previous Post
Goldilocks + VPA: Right-Size Pods Without Guessing
Next Post
Sigstore + Gitsign: Signed Commits Without GPG Pain

Discussion

Powered by Garrul . Sign in with GitHub or Google, or post anonymously.

Related Posts