The following are steps to set up Azure AD as an Identity Provider for an on-prem Kubernetes cluster.
- Azure Subscription
- On-prem Kubernetes cluster (in my case RKE2 v1.27.12+rke2r1)
I want to connect from my Workstation (Windows/Linux/Mac) to an on-prem Kubernetes cluster and authenticate to is using Azure AD.
I could not find many useful resources for this scenario except the dbi blog, so i decided to expand on it a little.
- Create an App Registration in Azure
- Use kubelogin to authenticate to Azure AD through the App Registration and aquire a token
- Use that token to authenticate to Kubernetes API Server
This is the basic flow from the kubelogin repo:
- Create an App Registration and set its Redirect URI to http://localhost:8000 (Azure will only accept localhost with http, every other URI has to be https)
- The App registration needs to have User.Read permissions to be able to sign users in and read their info.
- Add a group claim for the tokens so that you can use group membership for Kubernetes RBAC. Keep in mind that there are limits to the number of group memberships a single token can hold, more on that: MSFT Docs.
NOTE: If you have a large AD with many groups you can assign specific groups to the App Registration so that only those groups are emitted in the token. MSFT Docs. In that case you should only select the 4th option:
- Create a client secret to use when requesting tokens.
- Add the following arguments to your Kubernetes API server.
--oidc-client-id=<azure-app-registration-application-id>
--oidc-issuer-url=https://sts.windows.net/<azure-tenant-id>/
--oidc-username-claim=upn
--oidc-username-prefix=oidc:
--oidc-groups-claim=groups
--oidc-groups-prefix=oidc:
These arguments are very well explained in the K8S Docs, i will just explain shortly what they mean to us practically in this example.
--oidc-username-claim - points the Kubernetes API to the fields in the JWT to use for the username. If you use upn you will be using the user principal name of the user for K8S role assignment. Other fields from the JWT can be used, for example oid - object id of the user.
--oidc-username-prefix - prefix prepended to the user claim. This is an arbitrary string.
--oidc-groups-claim - points the Kubernetes API to the fields in the JWT to use for the users group membership.
--oidc-groups-prefix - prefix prepended to group claims. This is also an arbitrary string.
- Install kubelogin link
# Krew (macOS, Linux, Windows and ARM)
kubectl krew install oidc-login
- Add user to your kubeconfig file, for example:
using kubectl:
```
kubectl config set-credentials <user_name> \
--exec-api-version=client.authentication.k8s.io/v1beta1 \
--exec-command=kubectl \
--exec-arg=oidc-login \
--exec-arg=get-token \
--exec-arg=--oidc-issuer-url=https://sts.windows.net/<azure-tenant-id>/ \
--exec-arg=--oidc-client-id=<azure-app-registration-application-id> \
--exec-arg=--oidc-client-secret=<azure-app-registration-client-secret> \
--kubeconfig <path-to-kubeconfig>
```
resulting in something like:
```
users:
- name: <user_name>
user:
exec:
apiVersion: client.authentication.k8s.io/v1beta1
args:
- oidc-login
- get-token
- --oidc-issuer-url=https://sts.windows.net/<azure-tenant-id>/
- --oidc-client-id=<azure-app-registration-application-id>
- --oidc-client-secret=<azure-app-registration-client-secret>
command: kubectl
env: null
interactiveMode: IfAvailable
provideClusterInfo: false
```
NOTE: Don't forget to set the appropriate context (in the kubeconfig file) using the newly added user.
- When connecting to the Kubernetes cluster (for example using kubectl) a browser will open for you to authenticate to Azure AD. After successful authentication a token will be aquired and cached and used to communicate further with the Kubernetes API Server. You can examine the cached token if necessary for troubleshooting (~/.kube/cache/oidc-login).
Now that the authentication is done, we need to assign RBAC roles to our user.
- Define an example role:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: test-clusterrole
rules:
- apiGroups: [""]
resources: ["nodes", "pods"]
verbs: ["get", "watch", "list"]
- Assigning role to a user:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: test-clusterrolebinding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: test-clusterrole
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: User
name: "oidc:<user_principal_name>" # here our --oidc-username-prefix argument value comes into play
- Assigning role to a group:
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: test-clusterrolebinding-group
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: test-clusterrole
subjects:
- apiGroup: rbac.authorization.k8s.io
kind: Group
name: "oidc:<group_object_id>" # here our --oidc-groups-prefix argument value comes into play
@vaspoz
Getting error :
kubectl --context=ev get --raw /.well-known/openid-configuration --v=10
I0306 20:21:06.128145 67247 loader.go:395] Config loaded from file: /Users/prathap.dasari/.kube/config
I0306 20:21:06.128377 67247 round_trippers.go:466] curl -v -XGET -H "Accept: application/json, /" -H "User-Agent: kubectl/v1.30.3 (darwin/arm64) kubernetes/6fc0a69" 'https://:6443/.well-known/openid-configuration'
I0306 20:21:06.173922 67247 round_trippers.go:510] HTTP Trace: Dial to tcp::6443 succeed
I0306 20:21:06.257599 67247 round_trippers.go:553] GET https://***********:6443/.well-known/openid-configuration 401 Unauthorized in 129 milliseconds
I0306 20:21:06.257632 67247 round_trippers.go:570] HTTP Statistics: DNSLookup 0 ms Dial 32 ms TLSHandshake 28 ms ServerProcessing 39 ms Duration 129 ms
I0306 20:21:06.257636 67247 round_trippers.go:577] Response Headers:
I0306 20:21:06.257642 67247 round_trippers.go:580] Audit-Id: 13f732be-2254-403e-bb06-c52be64341aa
I0306 20:21:06.257645 67247 round_trippers.go:580] Cache-Control: no-cache, private
I0306 20:21:06.257648 67247 round_trippers.go:580] Content-Type: application/json
I0306 20:21:06.257649 67247 round_trippers.go:580] Content-Length: 129
I0306 20:21:06.257651 67247 round_trippers.go:580] Date: Thu, 06 Mar 2025 19:21:06 GMT
I0306 20:21:06.257682 67247 request.go:1212] Response Body: {"kind":"Status","apiVersion":"v1","metadata":{},"status":"Failure","message":"Unauthorized","reason":"Unauthorized","code":401}
I0306 20:21:06.257901 67247 helpers.go:246] server response object: [{
"metadata": {},
"status": "Failure",
"message": "Unauthorized",
"reason": "Unauthorized",
"code": 401
}]
error: You must be logged in to the server (Unauthorized)