NKP teams with native RBAC and quotas: each developer in their namespace

Published
Reading
11 min

Tested onNKP 2.19Kubernetes 1.36.2Microsoft Entra ID FreeLab date On a different version, check the compatibility matrix and the release notes before applying.

Ler em português

Post cover: NKP teams with native RBAC and quotas, each developer in their namespace

Summary: in the previous article, Entra ID started answering who signs in to NKP. This one shows what each team can do and how much it can consume, using only native Kubernetes objects: one namespace per team, a RoleBinding that ties the Entra group to the edit role, and a ResourceQuota with a LimitRange. Carla, from the developers group, was blocked before the binding, then could create in her namespace and nowhere else, and the quota held an 8-replica deployment at 4. Before handing access over, the admin tests it by impersonating the group, in seconds. For people who should not use the UI, the cluster itself issues a 24-hour certificate bound to one namespace. The recipe does not depend on workspaces or projects, so it also fits NKP Pro and Starter.

The result in one table

Question to the clusterBefore the RoleBindingAfter the RoleBinding
Can Carla create deployments in the team namespace?noyes
And in default?nono
Can she create RoleBindings (grant herself more)?nono
Can she list nodes?ForbiddenForbidden
Deployment asking for 8 replicas—4 of 8 running: the rest blocked by the quota
Pod without declared resources—Gets 50m/64Mi requests and 200m/128Mi limits from the LimitRange
Access without the UI (Davi)Result
Certificate issued by the clusterCN=davi.local, valid for 24 h
In his namespaceyes: creates and deploys
default, kube-system Secrets, nodes, RoleBindingsno on all of them

From sign-in to team access

Signing in with Entra ID answers the first question: who are you? The token that reaches the cluster carries the user and their groups. The second question belongs to Kubernetes: what can this group do, and where?

In the Entra ID article, Carla got access through an NKP project, which creates the namespace and the bindings from the UI. Workspaces and projects are part of NKP Ultimate, the lab license. Here the same result comes from native Kubernetes RBAC: three or four YAML objects that work on any edition, including clusters where the team prefers to keep access definitions in Git.

Workspaces and projects (Ultimate)Native RBAC (all editions)
Where it is configuredNKP UI, onceYAML with kubectl apply, on each cluster
New cluster inherits accessYes, NKP replicates itNo: apply the same YAML (or GitOps)
Namespace, binding and quotaThe project creates themOne file per team
How to keep it in orderUIYAML files in Git, with review

How RBAC works: who, where and what

Every Kubernetes permission is a sentence with three parts:

RBAC only adds permissions: there is no deny rule. If no binding granted it, the answer is Forbidden. That is why starting with no access and granting the minimum is the natural path.

Kubernetes ships four ready-made roles that cover most teams:

RoleCanCannotFor whom
viewRead pods, deployments, services and logsRead Secrets; change anythingSupport, audit, developers in production
editCreate, change and delete almost everything in the namespaceTouch permissions (Roles and RoleBindings)Developers in staging
adminEverything in edit + RoleBindings in the namespaceChange the quota; cluster resourcesTeam tech lead
cluster-adminEverything, in every namespace—Platform team, few people

The edit role reads Secrets

The edit role can read the namespace Secrets. For a team that should only see what runs, view is the starting point; for people who deploy, edit in the staging namespace and view in production.

The most common combination for teams is a reusable ClusterRole (edit) bound by a RoleBinding in each namespace. A ClusterRoleBinding applies to the whole cluster, including whatever is created later: keep it for the platform team.

What to prepare first

PrerequisiteWhyHow to check
Sign-in with groups working (Entra ID article)The binding points to the group that arrives in the tokenCarla signs in to the UI and generates her kubeconfig (Generate Token)
The group name as it arrives in the tokenOn NKP with Entra, it is oidc: + the group Object ID, not the display namekubectl auth whoami with Carla’s kubeconfig
Cluster admin kubeconfigWho creates the namespace, the quota and the bindingkubectl auth can-i '*' '*' = yes
One kubeconfig per person in the terminalGenerate Token writes to the active kubeconfigexport KUBECONFIG=~/.kube/carla.conf before pasting the commands

Best practice: keep the group in a variable

The group Object ID does not need to show on screen or sit in Git in plain text. In the lab, the YAML has a placeholder (__GRUPO_DEVS__) replaced at apply time by the value stored in a file on the bastion. To confirm it is the same group as the token: kubectl auth whoami -o jsonpath='{.status.userInfo.groups}' | grep -qF "$GRUPO_DEVS" && echo OK.

1. The team space: namespace, quota and resource defaults

One file per team. The quota is the namespace ceiling; the LimitRange gives default values to anyone who does not declare resources, which keeps the quota from rejecting a pod for lack of requests.

apiVersion: v1
kind: Namespace
metadata:
name: time-dev-demo
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: quota-time-dev
namespace: time-dev-demo
spec:
hard:
pods: "4"
requests.cpu: "1"
requests.memory: 1Gi
limits.cpu: "2"
limits.memory: 2Gi
services.loadbalancers: "0" # o time publica por Ingress, sem consumir IP do load balancer
---
apiVersion: v1
kind: LimitRange
metadata:
name: padrao-time-dev
namespace: time-dev-demo
spec:
limits:
- type: Container
defaultRequest: {cpu: 50m, memory: 64Mi} # para quem não declarar requests
default: {cpu: 200m, memory: 128Mi} # para quem não declarar limits
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: devs-edit
namespace: time-dev-demo
roleRef: {apiGroup: rbac.authorization.k8s.io, kind: ClusterRole, name: edit}
subjects:
- {apiGroup: rbac.authorization.k8s.io, kind: Group, name: "__GRUPO_DEVS__"}

To show the before and after, the admin first applies everything except the RoleBinding:

Terminal window
sed "s|__GRUPO_DEVS__|$GRUPO_DEVS|" time-dev.yaml | yq 'select(.kind != "RoleBinding")' | kubectl apply -f -
kubectl describe resourcequota -n time-dev-demo
namespace/time-dev-demo created
resourcequota/quota-time-dev created
limitrange/padrao-time-dev created
Resource Used Hard
-------- ---- ----
limits.cpu 0 2
limits.memory 0 2Gi
pods 0 4
requests.cpu 0 1
requests.memory 0 1Gi
services.loadbalancers 0 0

2. Authenticated, but no access

With her kubeconfig, Carla asks the cluster who she is and what she can do:

$ kubectl auth whoami
ATTRIBUTE VALUE
Groups [oidc:<id> system:authenticated]
$ kubectl auth can-i create deployments -n time-dev-demo
no
$ kubectl get pods -n time-dev-demo
Error from server (Forbidden): pods is forbidden: User "[email protected]" cannot list resource "pods" in API group "" in the namespace "time-dev-demo"

Entra authenticated Carla, with MFA. The cluster knows who she is and which group she belongs to, but no binding granted that group any permission in this namespace.

3. Access for the group, tested before handing it over

The admin applies the whole file, now with the RoleBinding, and impersonates the group to ask the cluster before telling the team:

Terminal window
sed "s|__GRUPO_DEVS__|$GRUPO_DEVS|" time-dev.yaml | kubectl apply -f -
kubectl auth can-i create deployments -n time-dev-demo --as=teste --as-group="$GRUPO_DEVS"
kubectl auth can-i create deployments -n default --as=teste --as-group="$GRUPO_DEVS"
namespace/time-dev-demo unchanged
resourcequota/quota-time-dev unchanged
limitrange/padrao-time-dev unchanged
rolebinding.rbac.authorization.k8s.io/devs-edit created
yes
no

Best practice: test by impersonation before handing over access

Impersonation (--as and --as-group) answers in seconds whether the group can do what it needs, and only there. It is the same test that validated the profiles in the Entra ID article, and it avoids finding a wrong binding through the team’s support ticket.

4. Carla, only in her namespace

$ kubectl auth can-i create deployments -n time-dev-demo
yes
$ kubectl auth can-i create deployments -n default
no
$ kubectl auth can-i create rolebindings -n time-dev-demo
no
$ kubectl get nodes
Error from server (Forbidden): nodes is forbidden: User "[email protected]" cannot list resource "nodes" in API group "" at the cluster scope

With edit, she creates in the team namespace, does not touch the others, does not see the nodes and cannot grant herself more permission: the role does not include Roles or RoleBindings. Least privilege at work.

5. The quota holding consumption

Carla deploys an application without declaring resources, and the LimitRange fills them in:

Terminal window
kubectl create deployment podinfo --image=ghcr.io/stefanprodan/podinfo:6.7.1 -n time-dev-demo
kubectl -n time-dev-demo rollout status deploy/podinfo
kubectl get pod -n time-dev-demo -o jsonpath='{.items[0].spec.containers[0].resources}'
deployment.apps/podinfo created
Waiting for deployment "podinfo" rollout to finish: 0 of 1 updated replicas are available...
deployment "podinfo" successfully rolled out
{"limits":{"cpu":"200m","memory":"128Mi"},"requests":{"cpu":"50m","memory":"64Mi"}}

Then she asks for 8 replicas, twice what the quota allows:

Terminal window
kubectl scale deployment podinfo --replicas=8 -n time-dev-demo
kubectl get deploy -n time-dev-demo
kubectl get events -n time-dev-demo --field-selector reason=FailedCreate | tail -1
NAME READY UP-TO-DATE AVAILABLE AGE
podinfo 4/8 4 4 17s
15s Warning FailedCreate replicaset/podinfo-7f5848f5f6 Error creating: pods "podinfo-7f5848f5f6-xqgr5" is forbidden: exceeded quota: quota-time-dev, requested: pods=1, used: pods=4, limited: pods=4

The deployment does not fail: it stays at 4/8, and the reason is in the events, not in the deployment. On the admin side, the quota shows the consumption:

Resource Used Hard
-------- ---- ----
limits.cpu 800m 2
limits.memory 512Mi 2Gi
pods 4 4
requests.cpu 200m 1
requests.memory 256Mi 1Gi
services.loadbalancers 0 0

Best practice: leave quota headroom for rollouts

A deployment update (rolling update) starts new pods before stopping the old ones. With the pod quota at its limit, the extra pod does not start and the update does not progress. Size the quota with one or two replicas of headroom, or adjust the deployment maxSurge.

6. kubectl-only access, without the UI

Some people should not use the NKP UI: an automation, a vendor for a limited time, an exception. For them, Kubernetes itself issues a client certificate through the CertificateSigningRequest API, and RBAC binds that user to one namespace. It works on any NKP edition, because it is standard Kubernetes.

The flow, in six steps (Davi is a local user, davi.local):

  1. The key and the certificate request are generated with CN=davi.local (the CN becomes the user name).
  2. The admin creates the CertificateSigningRequest with a 24-hour validity and approves it.
  3. Davi’s kubeconfig points to the same API server, with his certificate.
  4. Davi authenticates, still without permission: can-i = no.
  5. The admin creates the acesso-direto-demo namespace and an edit RoleBinding just for davi.local.
  6. Davi works in his namespace and nowhere else.
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
name: davi-local-demo
spec:
request: <CSR in base64>
signerName: kubernetes.io/kube-apiserver-client
expirationSeconds: 86400 # 24 hours
usages: [client auth]
Terminal window
kubectl certificate approve davi-local-demo
kubectl create rolebinding davi-edit --clusterrole=edit --user=davi.local -n acesso-direto-demo
NAME AGE SIGNERNAME REQUESTOR REQUESTEDDURATION CONDITION
davi-local-demo 1s kubernetes.io/kube-apiserver-client kubernetes-admin 24h Approved,Issued
subject=CN=davi.local
notAfter=Nov 11 18:31:31 2026 GMT
$ kubectl auth whoami # with Davi's kubeconfig, before the RoleBinding
ATTRIBUTE VALUE
Username davi.local
Groups [system:authenticated]
can-i create deployments -n acesso-direto-demo: yes
can-i create deployments -n default: no
can-i get secrets -n kube-system: no
can-i list nodes: no
can-i create rolebindings -n acesso-direto-demo: no

A certificate cannot be revoked

Kubernetes has no revocation list for client certificates. To remove Davi’s access before it expires, delete the RoleBinding: the certificate still authenticates, but with no permission at all. That is why the validity is short (24 hours) and the use is for exceptions only. Never put system:masters in the certificate organization field (O=): that group has full access and bypasses RBAC. In a production environment, proceed with caution.

For people, day to day, the path is still the Entra sign-in: MFA, offboarding and team changes stay in one place.

A team model to start with

Entra groups on one side, namespaces on the other, the role in the middle. Example names:

Entra groupWhereRoleBinding
nkp-plataformaWhole clustercluster-adminClusterRoleBinding
nkp-dev-sistemaxsistemax-hmleditRoleBinding
nkp-dev-sistemaxsistemax-prdviewRoleBinding
nkp-lider-sistemaxsistemax-hmladminRoleBinding
nkp-suporteSystem namespacesviewRoleBinding in each one

Golden rule: permission for groups, never for people. Who joins or leaves the team changes in Entra, not in the cluster.


The numbers

MeasureValue
Objects per team (namespace, quota, LimitRange, RoleBinding)4, in a single file
From namespace created to Carla working only in her namespace2 s
Binding test by impersonation2 commands, answer within the same second
Full rehearsal, from namespace to cleanup24 s (including a deliberate 15 s wait)
Example team quota4 pods · 1 vCPU / 1 GiB requests · 2 vCPU / 2 GiB limits
LimitRange default per container50m / 64Mi request · 200m / 128Mi limit
Deployment asking for 8 replicas4 running, 4 blocked by the quota
Consumption with 4 replicas200m CPU and 256Mi memory in requests (20% and 25% of the quota)
From certificate request to Davi’s pod running5 s
Certificate validity for UI-less access24 h
Generate Token kubeconfig validity24 h

What I learned

  1. Sign-in and permission are separate layers. Entra authenticates; the RoleBinding authorizes. Carla authenticated and blocked in step 2 is the expected behavior.
  2. The binding group must be the token group. On NKP with Entra, it is oidc:<Object ID>. Check it with kubectl auth whoami before writing the YAML.
  3. Test by impersonation before handing over access. --as-group answers in seconds and also shows what the team cannot do.
  4. edit is the right role for developers in staging, and it does not let the team widen its own access. In production, view.
  5. A quota without a LimitRange rejects pods without resources. The LimitRange sets the default and lets the quota do its job.
  6. Quota errors show up in the events, not in the deployment. kubectl get events --field-selector reason=FailedCreate is the first place to look.
  7. Certificate access is an exception, with short validity. Removing access means deleting the binding; the certificate only stops authenticating when it expires.

Checklist: one namespace per team

  • Sign-in with groups working and the group name checked in the token (kubectl auth whoami)
  • One namespace per system and per environment (-hml, -prd)
  • RoleBinding for the group, never for the person
  • edit in staging, view in production, admin only for the tech lead
  • ResourceQuota in every team namespace, with headroom for rollouts
  • LimitRange with default requests and limits
  • Binding tested with kubectl auth can-i --as-group before telling the team
  • One YAML per team in Git, with review
  • Certificate access only for exceptions: 24 h, never system:masters, binding deleted at the end
  • Periodic review: who has cluster-admin today?

What I haven’t tested yet

  • An Entra user on kubectl only, with browser sign-in straight from the terminal (kubelogin through Dex), without going through the NKP UI.
  • The same YAML files applied through GitOps to several clusters at once.
  • Quotas calibrated by real consumption after 30 days of a team’s usage.
  • A cluster with a Pro or Starter license. The recipe uses only native Kubernetes objects, which do not depend on the edition, but it ran in the lab, with an Ultimate license.

References

Nutanix

Projects and community

Comments

Comments use GitHub Discussions; you need a GitHub account.