7 min read | by Jordi Prats
Once we have Capsule setup we'll need to start managing the tenants and their permissions. In this post, we'll see how to assign permissions to a user, cordoning a tenant, and enforcing resource quotas at the tenant level.
By default, when we add a user to the owners list, they are going to be able to act as the admin of all the tenant namespaces:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
owners:
- name: jordi
kind: User
Writing the the above manifest is equivalent to setting clusterRoles
to admin
and capsule-namespace-deleter
:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
owners:
- name: jordi
kind: User
clusterRoles:
- admin
- capsule-namespace-deleter
The capsule controller is going to translate this definition into the following RoleBindings
:
$ kubectl get rolebinding
NAME ROLE AGE
capsule-demo-0-admin ClusterRole/admin 2d20h
capsule-demo-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 2d20h
We can also create a user and scope it to view-only permissions:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
owners:
- name: jordi
kind: User
- name: view-only
kind: User
clusterRoles:
- view
In this case, the capsule controller is going to create a RoleBinding
with the view
ClusterRole:
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
creationTimestamp: "2025-02-23T10:27:02Z"
labels:
capsule.clastix.io/role-binding: eaaa0637389ad533
capsule.clastix.io/tenant: demo
name: capsule-demo-2-view
namespace: jordi-1st-ns
ownerReferences:
- apiVersion: capsule.clastix.io/v1beta2
blockOwnerDeletion: true
controller: true
kind: Tenant
name: demo
uid: 90a1f6a1-2284-44dd-8f58-2b9f75b8616f
resourceVersion: "37840"
uid: 6eaec6f6-7718-44ba-bca5-310479d368ce
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: view-only
By automatically creating a RoleBinding
to all the namespaces owned by the tenant, the user is going to be able to list resources in all the tenant namespaces but not in any other namespaces:
$ KUBECONFIG=view-only-demo.kubeconfig kubectl auth can-i get pod -n jordi-1st-ns
yes
$ KUBECONFIG=view-only-demo.kubeconfig kubectl auth can-i get pod -n kube-system
no
We can also define RoleBindings using the tenant.spec.additionalRoleBindings
field, it is intended to be used for much more generic use cases:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
owners:
- name: jordi
kind: User
additionalRoleBindings:
- clusterRoleName: 'view'
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: additional-viewer
In this case, we are going to achieve the same result as before. Using the additionalRoleBindings
field is a way to make sure we are not assigning unintended permissions to the user since the default list of clusterRoles
include admin permissions to the tenant.
$ kubectl get rolebinding
NAME ROLE AGE
capsule-demo-0-admin ClusterRole/admin 2d20h
capsule-demo-1-capsule-namespace-deleter ClusterRole/capsule-namespace-deleter 2d20h
capsule-demo-2-view ClusterRole/view 16m
capsule-demo-3-view ClusterRole/view 4s
$ kubectl get rolebinding capsule-demo-3-view -o yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
creationTimestamp: "2025-02-23T10:43:47Z"
labels:
capsule.clastix.io/role-binding: 3bfe3bdd56040ecf
capsule.clastix.io/tenant: demo
name: capsule-demo-3-view
namespace: jordi-1st-ns
ownerReferences:
- apiVersion: capsule.clastix.io/v1beta2
blockOwnerDeletion: true
controller: true
kind: Tenant
name: demo
uid: 90a1f6a1-2284-44dd-8f58-2b9f75b8616f
resourceVersion: "39661"
uid: 6982812b-7cd1-4ea8-808b-7baf5a7e5403
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: view
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: additional-viewer
In the same way we can cordon a node, we can cordon a tenant to prevent any workload to be updated. We might want to do this, for example, during cluster upgrades or production freezes:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
cordoned: true
owners:
- name: jordi
kind: User
As soon as the Tenant is cordoned, the admission controller is going to prevent any update to the tenant workloads:
$ kubectl get tenant
NAME STATE NAMESPACE QUOTA NAMESPACE COUNT NODE SELECTOR AGE
demo Cordoned 1 2d20h
$ KUBECONFIG=jordi-demo.kubeconfig kubectl auth can-i update pod -n jordi-1st-ns
yes
$ KUBECONFIG=jordi-demo.kubeconfig kubectl edit pod demo
Error from server (Forbidden): pods "demo" is forbidden: User "jordi" cannot get resource "pods" in API group "" in the namespace "default"
$ KUBECONFIG=jordi-demo.kubeconfig kubectl create ns jordi-2nd-ns
Error from server (Forbidden): admission webhook "namespaces.projectcapsule.dev" denied the request: the selected Tenant is freezed
We can also have a service account acting as a tenant owner as follows:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
owners:
- name: jordi
kind: User
- name: system:serviceaccount:argocd:default
kind: ServiceAccount
The service account needs to be part of the Capsule group, so we'll have to update the CapsuleConfiguration
object to include the service account group to the list of user groups:
apiVersion: capsule.clastix.io/v1beta2
kind: CapsuleConfiguration
metadata:
annotations:
meta.helm.sh/release-name: capsule
meta.helm.sh/release-namespace: capsule-system
creationTimestamp: "2025-02-22T13:37:53Z"
generation: 1
labels:
app.kubernetes.io/instance: capsule
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/name: capsule
app.kubernetes.io/version: 0.7.0
helm.sh/chart: capsule-0.7.0
name: default
resourceVersion: "554"
uid: e16fdd92-0aa1-4149-b95d-51ba3f08272d
spec:
enableTLSReconciler: true
forceTenantPrefix: false
nodeMetadata:
forbiddenAnnotations:
denied: []
deniedRegex: ""
forbiddenLabels:
denied: []
deniedRegex: ""
overrides:
TLSSecretName: capsule-tls
mutatingWebhookConfigurationName: capsule-mutating-webhook-configuration
validatingWebhookConfigurationName: capsule-validating-webhook-configuration
protectedNamespaceRegex: ""
userGroups:
- projectcapsule.dev
- system:serviceaccount:argocd
Bear in mind that all the service accounts in the same namespace are included in this group.
One of the features of Capsule is the ability to enforce resource quotas at the tenant level instead of doing so just at the namespace level. We can achieve this by using the resourceQuotas
field in the Tenant object:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: demo
spec:
owners:
- name: jordi
kind: User
resourceQuotas:
scope: Tenant
items:
- hard:
pods: "2"
We can test this by maxing out the number of pods in the tenant in one namespace and trying to create a new pod on a different namespace belonging to the same tenant:
$ kubectl get ns -l capsule.clastix.io/tenant=demo
NAME STATUS AGE
jordi-1st-ns Active 2d21h
jordi-2nd-ns Active 27s
$ KUBECONFIG=jordi-demo.kubeconfig kubectl run demo-2 --image nginx -n jordi-1st-ns
pod/demo-2 created
$ KUBECONFIG=jordi-demo.kubeconfig kubectl run demo-3 --image nginx -n jordi-1st-ns
Error from server (Forbidden): pods "demo-3" is forbidden: exceeded quota: capsule-demo-0, requested: pods=1, used: pods=2, limited: pods=2
$ KUBECONFIG=jordi-demo.kubeconfig kubectl run demo-3 --image nginx -n jordi-2ns-ns
Error from server (Forbidden): pods is forbidden: User "jordi" cannot create resource "pods" in API group "" in the namespace "jordi-2ns-ns"
Capsule is achieving this by dynamically updating the ResourceQuota
object in all the tenant namespaces, while maintaining a global count of the resources used by the tenant as an annotation:
$ kubectl get resourcequota capsule-demo-0 -n jordi-1st-ns -o yaml
apiVersion: v1
kind: ResourceQuota
metadata:
annotations:
quota.capsule.clastix.io/hard-pods: "2"
quota.capsule.clastix.io/used-pods: "2"
creationTimestamp: "2025-02-23T11:25:41Z"
labels:
capsule.clastix.io/resource-quota: "0"
capsule.clastix.io/tenant: demo
name: capsule-demo-0
namespace: jordi-1st-ns
ownerReferences:
- apiVersion: capsule.clastix.io/v1beta2
blockOwnerDeletion: true
controller: true
kind: Tenant
name: demo
uid: 90a1f6a1-2284-44dd-8f58-2b9f75b8616f
resourceVersion: "45001"
uid: 4b062e21-0643-4c72-8979-500bd03d374d
spec:
hard:
pods: "2"
status:
hard:
pods: "2"
used:
pods: "2"
$ kubectl get resourcequota capsule-demo-0 -n jordi-2nd-ns -o yaml
apiVersion: v1
kind: ResourceQuota
metadata:
annotations:
quota.capsule.clastix.io/hard-pods: "2"
quota.capsule.clastix.io/used-pods: "2"
creationTimestamp: "2025-02-23T11:28:23Z"
labels:
capsule.clastix.io/resource-quota: "0"
capsule.clastix.io/tenant: demo
name: capsule-demo-0
namespace: jordi-2nd-ns
ownerReferences:
- apiVersion: capsule.clastix.io/v1beta2
blockOwnerDeletion: true
controller: true
kind: Tenant
name: demo
uid: 90a1f6a1-2284-44dd-8f58-2b9f75b8616f
resourceVersion: "45002"
uid: d865a8d4-8be0-44f6-b4a6-3757a562c83f
spec:
hard:
pods: "0"
status:
hard:
pods: "0"
used:
pods: "0"
Posted on 27/02/2025