NKP login with Microsoft Entra ID: from app registration to group-based access

Published
Reading
15 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 login with Microsoft Entra ID, group-based access in the dashboard and in kubectl

Summary: the lab’s NKP 2.19 now accepts sign-in with Microsoft Entra ID accounts, and Entra’s sign-in policies, such as MFA, now apply to the dashboard and to kubectl as well. From saving the Identity Provider to the first Entra login took 12 minutes. Access is granted to groups, never to individuals: three Entra groups became three NKP profiles, tested with real users. Ana administers the whole platform, Bruno operates only his workspace’s cluster and Carla works only in the project namespace. A fourth account, with no group, authenticates and sees nothing. Everything worked with Entra ID on the free license. The post shows the configuration on both sides, the right roles for each profile and the best practices that got each one ready.

The result in one table

User (Entra group)NKP rolesIn the dashboardIn kubectl (services cluster)
Ana (nkp-plataforma-admins)Global: Cluster Admin Role + dkp-kommander-adminEverything: 4 workspaces, 2 clusters, Administrationcan-i '*' '*' = yes; sees all 6 nodes
Bruno (nkp-servicos-operadores)Workspace: custom role “Cluster Operator (full)” + Kommander Workspace AdminOnly the services workspace and his clustercreate deployments in all namespaces = yes
Carla (nkp-servicos-devs)Global: dkp-kommander-view · Workspace: Kommander Workspace View · Project: App Deployer + Kommander Project EditOnly the services workspace and her projectyes in the project namespace, no in default, nodes forbidden
Eva (no group)NoneAuthenticates in Entra and gets “Not Authorized”—
NKP local adminLocal passwordFull dashboard the whole timeCertificate kubeconfig

The local admin and the certificate kubeconfigs kept working throughout the configuration. That’s the emergency (break-glass) access in case the Entra login becomes unavailable.


How identity works in NKP

NKP uses Dex, an OpenID Connect provider that runs only once, on the management cluster. Workload clusters don’t have their own Dex: each one gets a kube-oidc-proxy (for kubectl) and a traefik-forward-auth (for the web UIs), which trust the central Dex.

Three practical consequences:

  1. Entra handles authentication (password, MFA, account lockout). NKP handles authorization (what each group can do).
  2. The API servers don’t change. I checked in the lab: none of them has --oidc-* flags. OIDC comes in through kube-oidc-proxy, so there’s no control plane restart.
  3. In NKP, permissions are granted to groups. The bindings screen itself says so: “Both groups and roles must be defined before setting up cluster role bindings”. The order is always: Entra group → NKP group → role → binding.

NKP editions

Login through Dex (RBAC and single sign-on) is in every edition: Starter, Pro and Ultimate. Workspaces and projects, used in Bruno’s and Carla’s scenarios, are part of NKP Ultimate, the lab’s license. On Pro and Starter, you get the same result with native Kubernetes RBAC, applying the bindings cluster by cluster.


What to prepare first

ItemWhyHow I checked it in the lab
An Entra ID tenantNKP talks to one tenantEntra ID Free: app registration, groups and Security Defaults (MFA) are included
Verified domain (optional)Sign-ins with the company domainlab.leimar.dev verified with a TXT record
Users with the Email attribute filled inNKP uses the email as the username; without it, the login doesn’t completeEach user’s profile → Contact information → Email
Security groups in EntraThe token carries the groups4 groups, one per profile
Outbound access from management to login.microsoftonline.com and graph.microsoft.com (443)It’s the Dex pod that calls Microsoft, not the browserTest pod in the kommander namespace: 20 out of 20 HTTP 200 responses
Emergency accessIf the Entra login fails, you’re not locked outNKP local admin + certificate kubeconfig on both clusters
Synchronized clock (NTP)Tokens have a lifetime; a wrong clock gets the token rejectedBastion and API servers within the same second

Overview of the test tenant in Entra ID, with the Microsoft Entra ID Free license

Tenant properties with Security Defaults enabled, which bring MFA on the free license

Do this first: test outbound access from the management cluster

A temporary pod in the kommander namespace running curl against https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration tells you in seconds whether the network is right. Do it before opening the NKP form: a DNS or proxy block shows up here, not in the middle of the first login.


1. The app in Entra ID

NKP needs an app registration in Entra, which becomes Dex’s OIDC “client”.

  1. App registrations → New registration: name nkp-dex, Accounts in this organizational directory only.
  2. Redirect URI (Web): https://<nkp-address>/dex/callback. In the lab, https://nkp.lab.example.local/dex/callback. The browser resolves the internal name; Entra only requires https.

Registration of the nkp-dex app in Entra ID, single tenant, with a Redirect URI ending in /dex/callback

  1. Token configuration:
    • Add groups claim → Security groups, Group ID format in the ID token;
    • Add optional claim (ID token) → email and upn.

App token configuration: email, groups and upn claims

  1. API permissions → Microsoft Graph, delegated permissions: openid, profile, email, offline_access, User.Read and GroupMember.Read.All. Then Grant admin consent.

The six delegated Microsoft Graph permissions with admin consent granted

  1. Certificates & secrets → New client secret. Write down the expiration date and set a reminder 30 days before it.

The app's client secret with the expiration date and the value hidden

Best practice: grant admin consent

Without admin consent, every user sees, on their first login, a Microsoft screen asking permission for “basic profile” and “continuous access”. With consent granted on behalf of the organization, that screen goes away for everyone.

Best practice: copy the secret’s “Value”, not the “Secret ID”

The Certificates & secrets screen shows two similar columns. NKP needs the Value, which is shown only once, right after you create the secret. Store it straight in the password vault. If the login comes back with AADSTS7000215 invalid client secret, this is almost always the cause.

Write down three values for the next step: the Tenant ID, the app’s Client ID and the Object ID of each group. Entra sends the group’s ID in the token, not its name.

The four Entra ID security groups, with the Object IDs hidden


2. Entra ID as the Identity Provider in NKP

In Kommander, logged in as the local admin: Identity Providers → Add Identity Provider → OIDC.

FieldValueDetail
NameEntra IDBecomes the button text on the login screen
Issuer URLhttps://login.microsoftonline.com/<tenant-id>/v2.0
Client IDthe app’s Client ID
Client Secretthe secret’s Value
Username KeyemailThe user shows up in NKP by email
Get User Infochecked
Insecure Enable GroupscheckedWithout it, Dex drops the groups claim
Insecure Skip Email VerifiedcheckedEntra doesn’t send the email_verified claim
Workspacesall

The NKP Identity Provider OIDC form filled in, with the identifiers hidden

In this screenshot, the Client Secret field got the Entra Secret ID instead of the Value: that’s the detail from the tip in step 1. In the lab, the fix was the Identity Provider’s Edit, which leaves the Client ID and Client Secret fields empty to keep the previous values.

Before saving, check the Redirect URI the form shows: it must be exactly the one registered in the Entra app. If NKP shows an IP and Entra has the name, the login comes back with AADSTS50011.

The Entra ID Identity Provider registered in NKP, OIDC type and enabled

The full setup in Kommander, in 31 seconds (no audio, identifiers hidden):

Video: adding Entra ID as an OIDC Identity Provider in Kommander

The NKP dashboard lives at /dkp/kommander/dashboard. There, the login screen gets the Log in with Entra ID button, next to the local login:

BeforeAfter
NKP login screen with only the local username and passwordNKP login screen with the Log in with Entra ID button

The first test is with Eva, who isn’t in any group. She signs in with email and password in Entra and lands back in NKP with “Not Authorized”. That’s the expected result, and it proves the separation: authenticated is not authorized. This test also validates the secret, the redirect and the issuer in one go.

Eva authenticated by Entra ID and getting Not Authorized in NKP

Best practice: one user per private window

The browser’s private session lasts as long as any private window is open. Before testing another user, close them all and open a new one. On Entra’s “Stay signed in?” screen, answer No. Otherwise, the next user’s test reuses the previous user’s session.


3. The NKP groups: oidc: + Object ID

Identity Providers → Groups → Create Group. For each Entra group, one NKP group with the same name (it stays readable) and, under Identity Provider Groups, the value oidc:<group Object ID>. The oidc: prefix is what NKP adds to groups that arrive through the OIDC connector, and the screen itself points this out.

NKP group form with the guidance to prefix the group with oidc:

The three NKP groups mapped to the Entra ID groups

An NKP group on its own grants no permissions at all. Access comes from the binding, at one of three levels: global (all clusters), workspace (a team’s clusters) or project (a namespace distributed to the workspace’s clusters).


4. Platform admin: Ana

Global Access Control → Cluster Role Bindings → group nkp-plataforma-admins:

  • Cluster Admin Role: admin on every managed cluster;
  • dkp-kommander-admin: admin of the Kommander dashboard.

Global binding of the nkp-plataforma-admins group with Cluster Admin Role and dkp-kommander-admin

The two go together. With only dkp-kommander-admin, Ana got past the dashboard authorization, but the screen stayed blank: the dashboard queries the clusters with the permissions of whoever is logged in, and the role on the clusters was missing. With both, the full dashboard showed up in under 5 minutes, the time it takes NKP to federate the role to the clusters.

Only dkp-kommander-adminCluster Admin Role + dkp-kommander-admin
Ana logged in with only the dashboard role: blank screenAna logged in with both roles: full dashboard with workspaces, clusters and Administration

Ana’s sign-in through Entra ID, from the button to the full dashboard (no audio, internal names hidden):

Video: platform admin sign-in through Entra ID

5. Workspace operator: Bruno

Bruno operates the services workspace’s cluster and shouldn’t see the rest of the platform. The binding is on the workspace: workspace selector → Access Control → Cluster Role Bindings → group nkp-servicos-operadores. The first choice was the Workspace Cluster Admin Role + Kommander Workspace Admin Role:

First binding of the operators group on the workspace: Kommander Workspace Admin Role and Workspace Cluster Admin Role

The binding in the dashboard, with that first choice of roles:

Video: roles for the operators group in the services workspace

In the dashboard, the result was as expected from the very first configuration: Bruno’s workspace selector lists only the services workspace, without Global and without the others.

Bruno logged in: the selector shows only the services workspace and one cluster

Bruno’s sign-in, landing straight in his workspace:

Video: operator sign-in through Entra ID, scoped to the services workspace

In kubectl, the choice of role matters. The workspace role list has about 30 similar names. In NKP 2.19, the default Workspace Cluster Admin Role is an aggregated role: it has no rules of its own and receives those of the roles labeled aggregate-to-cluster-admin. On the cluster, that meant access to the Cluster API resources and the dashboard URLs, but not to the core Kubernetes resources: can-i create deployments returned no and get nodes returned Forbidden. Ana’s global admin role, by contrast, has explicit * rules.

With that default role, Bruno signs in through Entra, but kubectl answers no and Forbidden (recorded before the fix; the kubeconfig commands, which contain the token, were cut):

Video: operator kubectl with the default Workspace Cluster Admin Role, getting Forbidden

The solution was a custom Workspace Role, created in the dashboard (Access Control → Roles → Create Role), of type Cluster Role, with two rules: all resources and verbs, and all nonResourceURLs. I named it “Cluster Operator (full)”.

Custom Workspace Role Cluster Operator (full) with two explicit rules

The group’s final binding swapped the Workspace Cluster Admin Role for that role and kept the Kommander Workspace Admin Role (the workspace dashboard). NKP federated the new role to the cluster in under 1 minute:

Terminal window
# node names and Object ID anonymized
$ kubectl auth whoami
ATTRIBUTE VALUE
Groups [oidc:<object-id-do-grupo> system:authenticated]
$ kubectl auth can-i create deployments --all-namespaces
yes
$ kubectl get nodes
NAME STATUS ROLES AGE VERSION
cluster-servicos-control-plane-1 Ready control-plane 3d2h v1.36.2
cluster-servicos-control-plane-2 Ready control-plane 3d2h v1.36.2
cluster-servicos-control-plane-3 Ready control-plane 3d2h v1.36.2
cluster-servicos-worker-1 Ready <none> 3d2h v1.36.2
cluster-servicos-worker-2 Ready <none> 3d2h v1.36.2
cluster-servicos-worker-3 Ready <none> 3d2h v1.36.2

Best practice: test the role before handing over access

As an admin, ask the cluster while impersonating the group: kubectl auth can-i create deployments -A --as=teste --as-group='oidc:<object-id>'. That’s how the aggregated role showed up in seconds, without depending on the user’s login.


6. Project developer: Carla

Carla works only in one project’s namespace. In the services workspace: Projects → Create Project (app-demo, placed only on the services cluster). NKP creates the app-demo-mkgxc namespace on the cluster. In the project, the nkp-servicos-devs group gets App Deployer (creates and changes workloads) + Kommander Project Edit (the project in the dashboard).

Binding of the devs group on the app-demo project with App Deployer and Kommander Project Edit

For a project-only developer to use the dashboard, it takes three bindings, one at each level:

LevelRoleWhat it unlocks
Globaldkp-kommander-viewThe front door: the dashboard URL (/dkp/kommander/dashboard)
WorkspaceKommander Workspace ViewReading the workspace and the project list
ProjectApp Deployer + Kommander Project EditWorking in the project namespace

Workspace roles already include the front door; project roles don’t. That’s why, with only the project binding, Carla got “Not Authorized”; with the door but without workspace read access, the screen stayed blank. With all three, she sees the services workspace and her project:

Carla logged in, seeing the services workspace and the app-demo project

Terminal window
$ kubectl auth can-i create deployments -n app-demo-mkgxc
yes
$ kubectl auth can-i create deployments -n default
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
$ kubectl create deployment podinfo --image=ghcr.io/stefanprodan/podinfo:6.7.1 -n app-demo-mkgxc
deployment.apps/podinfo created
$ kubectl get pods -n app-demo-mkgxc
NAME READY STATUS RESTARTS AGE
podinfo-7f5848f5f6-7ztzk 1/1 Running 0 1s

Best practice: least privilege at the front door

dkp-kommander-view grants read access to /dkp/*. For a more restricted developer profile, create an NKP Role with only /dkp/kommander/dashboard* and use it instead.


7. kubectl with the Entra login

In the dashboard, the user menu has Generate Token: pick the cluster and copy the commands that build the kubeconfig. The token is issued by Dex with the Entra groups and is valid for 24 hours (in Ana’s token, exp − iat = 86,400 s). The kubeconfig points to the chosen cluster’s kube-oidc-proxy.

Claims of the token issued by Dex: groups with the Entra Object ID, the Dex issuer and a 24-hour lifetime

Ana's kubectl auth whoami and can-i with the Entra ID token

Ana’s whole flow, from Generate Token to kubectl get nodes (the kubeconfig commands, which contain the token, were cut):

Video: Generate Token and kubectl as platform admin

Best practice: one kubeconfig file per user

The Generate Token commands write to the active kubeconfig. Before pasting them, run export KUBECONFIG=~/.kube/<usuario>.conf. That way your admin ~/.kube/config stays intact.


The numbers

MeasureValue
From saving the Identity Provider to the first Entra login12 min
Entra ID license usedFree (MFA through Security Defaults)
Outbound access from management to Microsoft (test pod)20 out of 20 HTTP 200; DNS in 3–10 ms
Delegated Graph permissions on the app6, with admin consent
Workspace roles available in the dashboard~30
Federation of a global role to the clusters< 5 min
Federation of the custom Workspace Role< 1 min
Generate Token token lifetime24 h
Bindings for a project-only developer to use the dashboard3 (global, workspace, project)

What I learned

  1. Authentication and authorization live in different places. Entra says who the person is (with MFA); NKP says what their group can do. Eva proves it in the first test.
  2. Groups, always. The token carries the Object ID; NKP uses oidc:<ID>. Joining or leaving a group in Entra changes access from the next token on, without touching NKP.
  3. Dashboard roles and cluster roles go together. The dashboard queries the clusters with the permissions of whoever is logged in.
  4. Check what a role grants before using it. In NKP 2.19, the default Workspace Cluster Admin Role is aggregated; for an operator with full cluster access, use a custom Workspace Role.
  5. A project developer needs three levels. The global front door, workspace read access and work in the project.
  6. Keep emergency access. Local admin and certificate kubeconfig, stored in a vault, before touching the login.

Checklist: NKP login with Entra ID

  • Certificate kubeconfig and local admin password stored in the vault (break-glass)
  • Outbound access from management to login.microsoftonline.com and graph.microsoft.com tested from a pod
  • Entra users with the Email attribute filled in
  • Security groups in Entra, one per profile, with the Object IDs written down
  • App registration with a Redirect URI identical to the one in the NKP form
  • Groups claim (Group ID) and optional claims email and upn
  • Delegated permissions with GroupMember.Read.All and admin consent
  • Client secret Value in the vault and a reminder 30 days before expiration
  • OIDC Identity Provider with Username Key email, Insecure Enable Groups and Insecure Skip Email Verified
  • Test with an account with no group: it authenticates and gets “Not Authorized”
  • NKP groups with oidc:<Object ID>
  • Bindings tested with kubectl auth can-i --as-group before granting access
  • One kubeconfig per user (export KUBECONFIG) when using Generate Token

What I haven’t tested yet

  • kubectl-only access, without the dashboard, for an Entra user who shouldn’t get into Kommander (the k8s-servicos-kubectl group already exists in the lab’s Entra). For exceptions, access with a certificate issued by the cluster itself, bound to one namespace, is covered in NKP teams with native RBAC and quotas.
  • User offboarding: how long access remains after disabling the account in Entra, with the 24-hour token.
  • MFA on every login with Conditional Access (Entra ID P1). With Security Defaults, Entra prompts for MFA based on risk evaluation, not on every login.
  • The same profiles on a cluster with a Pro or Starter license. The recipe with native Kubernetes RBAC (namespace, RoleBinding for the group and quota), which does not depend on workspaces or projects, was tested in the lab and is covered in NKP teams with native RBAC and quotas.
  • Rotating the client secret before it expires, without interrupting logins.

References

Nutanix

Microsoft

Projects and community

Comments

Comments use GitHub Discussions; you need a GitHub account.