Role-Based Access Control (RBAC)¶
Who can do what, on the under cluster and inside a tenant cluster.
There are three surfaces, and they are independent:
| Surface | Authority | Who grants it |
|---|---|---|
| The under cluster | Its own API server's RBAC | You, with ordinary ClusterRoles and bindings |
| A tenant's Environments, on the under cluster | The same API server, but the bindings are written by Capsule | You, by naming ClusterRoles in the Tenant |
| Inside a tenant cluster | That cluster's own API server | The tenant, who holds cluster-admin there |
The middle row is the one kMetal shapes, and the one worth reviewing before onboarding a tenant.
kMetal ships no roles
There is no kmetal-platform-admin, no kmetal-cluster-creator, and no tenant role installed by the platform.
Every role a human or a tenant binds to is one you author.
The roles shown below are the ones the reference deployment defines, not roles you will find on a fresh install.
Platform administrators¶
Operating kMetal means writing cluster-scoped objects — the KMetal object, Tenants, ClusterClasses, provider networks, ClusterProfiles, Flux objects — so in practice the platform team holds cluster-admin on the under cluster.
Two KMetal fields decide how that interacts with tenancy rather than with RBAC, and they are easy to conflate with it:
multiTenancy.administratorsmakes a subject an owner of everyTenant, which is what a GitOps controller writing inside tenant Environments needs.multiTenancy.ignoreUserWithGroupstakes a subject out of Capsule's admission path entirely, which is what a cluster administrator needs when the identity provider puts everyone in one group.
Neither grants any permission. See Exempting the platform team.
What a tenant can do on the under cluster¶
Tenants are not bound to roles directly.
A Tenant names ClusterRoles, and Capsule creates a RoleBinding for each one in every Environment the tenant owns:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: dynamo
spec:
owners:
- kind: ServiceAccount
name: system:serviceaccount:kmetal-tenants:dynamo
clusterRoles:
- admin # built-in, aggregated
- capsule-namespace-deleter # from Capsule
- tenants-clusterrole # yours
- kmetal-networking-tenant # yours
# What Capsule wrote, and against which ClusterRole
kubectl get rolebindings -n dynamo-prod -o custom-columns='NAME:.metadata.name,ROLE:.roleRef.name,SUBJECT:.subjects[0].name'
Omit clusterRoles and Capsule defaults the owner to admin and capsule-namespace-deleter.
Set it and you replace that default outright, which is what makes a curated tenant role possible rather than only additive.
The list is per owner, so it is a per-customer decision taken at onboarding — see Choose what the owner may do.
The platform-authored roles are small on purpose, and each one is a capability you either offer a tenant or do not:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: tenants-clusterrole
rules:
# Run clusters.
- apiGroups: ["cluster.x-k8s.io"]
resources: ["clusters"]
verbs: ["*"]
# See the health check the platform wrote for them, and nothing more.
- apiGroups: ["cluster.x-k8s.io"]
resources: ["machinehealthchecks"]
verbs: ["get", "list", "watch"]
# Self-service add-ons, into their own clusters only.
- apiGroups: ["config.projectsveltos.io"]
resources: ["profiles"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kmetal-networking-tenant
rules:
- apiGroups: ["network.kmetal.io"]
resources: ["*"]
verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: kmetal-tenant-data-protector
rules:
- apiGroups: ["backup.kmetal.io"]
resources: ["clusterbackups"]
verbs: ["*"]
clusters is all CAPI needs from a tenant: the topology controller creates the MachineDeployments, Machines and the KamajiControlPlane on the tenant's behalf, and a tenant who cannot touch any of them can still run a cluster.
machinehealthchecks is separated from it deliberately, and read-only.
The MachineHealthCheck for a tenant cluster is written by the platform, not by the tenant — see React to clusters instead of matching them — so a tenant needs to see why their worker was replaced, and has no reason to edit or delete the object that replaced it.
Granting * on both resources in one rule, as a single cluster.x-k8s.io entry invites, hands the tenant the platform's remediation policy along with their own cluster.
profiles is the self-service half of fleet management.
Profile is the namespaced counterpart of ClusterProfile: it matches only clusters in its own namespace, and its policyRefs may name only ConfigMaps and Secrets in that same namespace — the API requires their namespace field to be empty and fills in the Profile's own.
So a tenant writing a Profile in their Environment delivers into the clusters they own and has no way to address anybody else's, which makes it the one Sveltos type that is safe to hand over.
That gives a clean split: the platform keeps the mandatory tier as ClusterProfiles — the CNI, whatever else is not optional — and tenants layer their own add-ons on top without asking for a manifest to be merged.
Two things to settle before granting it
Set your mandatory profiles to a low tier. Conflicts go to the lowest tier, and the default is 100 for everyone. A platform profile left at the default can lose an object to a tenant's Profile that arrived first.
deploymentType: Local delivers into the under cluster, not the tenant's. It is a field on policyRefs, so RBAC cannot exclude it, and Sveltos applies what it finds with its own permissions unless Sveltos' own multi-tenancy is in play — a RoleRequest plus the projectsveltos.io/serviceaccount-name and -namespace labels on the Profile, which makes it deploy as that ServiceAccount instead. Pair the grant with that, or with an admission policy that rejects Local, before handing profiles to tenants.
kmetal-tenant-data-protector is separate for the same reason in the other direction: whether a tenant takes their own backups is a per-customer decision, so it is a role you add to an owner rather than a rule buried in the one they already have.
What that set adds up to¶
For a tenant owner holding the role set above, in their own Environment:
| Can | Cannot |
|---|---|
Create and delete Clusters |
Create MachineDeployments, Machines or TenantControlPlanes |
Create every network.kmetal.io claim — VpcClaim, SubnetClaim, EipClaim |
Read ClusterClasses, DataStores, StorageClasses or Nodes |
| Read Secrets in the Environment, including the cluster kubeconfig | Edit or delete the platform's MachineHealthChecks |
See the MachineHealthCheck covering their workers |
Create ClusterProfiles, or anything in a namespace they do not own |
Deliver add-ons into their own clusters with a Profile |
Reach another tenant's clusters with one |
Create VirtualMachines and DataVolumes directly |
Create ClusterBackups, without kmetal-tenant-data-protector |
| Create namespaces, up to the tenant's quota, and delete their own | Create ResourceQuotas, or raise the ones Capsule set |
Write Roles and RoleBindings inside their Environments |
Escalate beyond the roles they hold — the API server's own escalation check applies |
Reproduce any row with impersonation:
kubectl auth can-i create clusters.cluster.x-k8s.io -n dynamo-prod \
--as=system:serviceaccount:kmetal-tenants:dynamo
kubectl auth can-i --list -n dynamo-prod \
--as=system:serviceaccount:kmetal-tenants:dynamo
Three rows worth deciding on deliberately¶
admin carries more than applications.
It is the built-in aggregated ClusterRole, so it picks up every CRD labelled rbac.authorization.k8s.io/aggregate-to-admin — which on this platform includes kubevirt.io, cdi.kubevirt.io and the Flux groups.
A tenant owner can therefore create raw VirtualMachines and DataVolumes beside their clusters, bounded only by their quota rather than by intent.
If that is not what you meant to offer, replace admin with a curated role in owners[].clusterRoles — Capsule binds whatever you name, and nothing requires admin to be one of them.
network.kmetal.io: * makes EIPs self-service.
Networking Tasks describes EipClaims as a platform grant — the admin decides which external addresses a tenant may publish on.
That is policy, and this role does not enforce it: a tenant holding * on the group creates their own claims.
Narrow the role to the claims a tenant should own if the grant model is meant to hold:
rules:
- apiGroups: ["network.kmetal.io"]
resources: ["subnetclaims", "vpcclaims"]
verbs: ["*"]
- apiGroups: ["network.kmetal.io"]
resources: ["eipclaims"]
verbs: ["get", "list", "watch"]
Backups are a role you hand out, not a default.
Data Protection has the tenant creating ClusterBackups in their own Environment, which takes kmetal-tenant-data-protector on that owner.
A tenant without it cannot create one, and the platform takes backups on their behalf instead — both are coherent, and the choice belongs in the Tenant:
spec:
owners:
- kind: Group
name: dynamo-platform
clusterRoles:
- admin
- capsule-namespace-deleter
- tenants-clusterrole
- kmetal-networking-tenant
- kmetal-tenant-data-protector # this customer owns their backups
Give it to the owner that should hold the S3 credentials too: the archive is written with a Secret in the same Environment, so the ability to create a ClusterBackup and the ability to read that Secret should not sit with different people.
What a tenant cannot see at all¶
Nothing grants tenants cluster-scoped reads, and kMetal installs no capsule-proxy, so a tenant cannot list ClusterClasses, DataStores, StorageClasses or Nodes.
One cluster-scoped API is withheld for a stronger reason than discovery.
Creating a KubernetesNodeUpgrade (yaki.clastix.io) runs privileged code as root on the platform's own machines, so it belongs to the subjects that hold cluster-admin on the under cluster and to no role a Tenant binds — see Node Upgrades.
Every value they have to name in a Cluster — the tier, the datastore, a boot volume size, a Kubernetes version — is therefore something you tell them out of band, and something they get wrong silently if the platform changed it.
Publish those values as part of onboarding rather than expecting discovery, and see MultiCluster Management for the list.
resourceNames does not accept patterns
A rule scoped to resourceNames: ["*-kubeconfig"] matches a Secret literally named *-kubeconfig and nothing else.
Restricting a tenant to their own kubeconfig Secrets is done by the namespace boundary — which is what Capsule already gives you — not by a name pattern.
RBAC inside a tenant cluster¶
The admin kubeconfig Kamaji generates is cluster-admin of that cluster.
From there, RBAC is ordinary Kubernetes and belongs to the tenant: their Roles, their bindings, their groups.
The platform has exactly one way to put RBAC inside a tenant cluster, and it is not RBAC on the under cluster — it is a Sveltos ClusterProfile delivering the objects, which is also how you would mandate a break-glass binding or a monitoring ServiceAccount across the fleet:
# Delivered into every tenant cluster, drift-corrected
apiVersion: config.projectsveltos.io/v1beta1
kind: ClusterProfile
metadata:
name: platform-break-glass
spec:
clusterSelector:
matchExpressions:
- key: cluster.x-k8s.io/cluster-name
operator: Exists
syncMode: ContinuousWithDriftDetection
policyRefs:
- kind: ConfigMap
name: break-glass-rbac
namespace: kmetal-system
See Fleet Management for how delivery and drift correction work.
Anything a tenant does to their own cluster's RBAC is invisible to the under cluster, and a tenant can undo a delivered binding — for as long as it takes Sveltos to notice.
Authentication¶
Everything this page grants is granted on the under cluster, and every subject it names authenticates there. kMetal adds no authentication path of its own: whatever that api-server already trusts — normally the organization's OIDC provider — is what platform administrators and tenant owners both present.
So the strings in multiTenancy.users and in a Tenant's owners are the username and groups your provider issues, not objects on the cluster.
Group-based ownership is the shape to aim for, because membership then changes in the provider and nothing on the cluster is edited — see How a tenant authenticates.
Hosted control planes are deliberately outside this. kMetal does not expose the tenant api-server's authentication configuration, so a tenant cluster is reached with the credential Kamaji issues for it, and access inside that cluster is RBAC on that credential rather than a second identity integration to operate.
Audit logging on the under cluster¶
Auditing is your own cluster's configuration, unchanged by kMetal — an audit policy file and --audit-policy-file on the api-server you installed.
What is worth auditing is the platform's own surface, since this is the api-server every tenant action passes through: clastix.io (the KMetal object), capsule.clastix.io (tenancy), cluster.x-k8s.io (who created and deleted clusters), and network.kmetal.io (who claimed which address).
apiVersion: audit.k8s.io/v1
kind: Policy
omitStages:
- RequestReceived
rules:
- level: RequestResponse
verbs: ["create", "update", "patch", "delete"]
resources:
- group: "clastix.io"
- group: "capsule.clastix.io"
- group: "cluster.x-k8s.io"
- group: "network.kmetal.io"
- level: Metadata
Check what a subject can do¶
# A tenant owner, in one of their Environments
kubectl auth can-i --list -n dynamo-prod \
--as=system:serviceaccount:kmetal-tenants:dynamo
# A single question, including the negative ones worth confirming
kubectl auth can-i create machinedeployments.cluster.x-k8s.io -n dynamo-prod \
--as=system:serviceaccount:kmetal-tenants:dynamo
# What Capsule bound, per Environment
kubectl get rolebindings -A -l capsule.clastix.io/managed-by \
-o custom-columns='NS:.metadata.namespace,ROLE:.roleRef.name,SUBJECT:.subjects[0].name'
# Whether a namespace is inside a tenant at all
kubectl get namespace dynamo-prod -o jsonpath='{.metadata.labels}'
A namespace with no capsule.clastix.io/tenant label is outside tenancy, which means none of this applies to it — see the silent failure that produces one.