Skip to content

Instantly share code, notes, and snippets.

@lukapetrovic-git
Last active March 7, 2025 19:07
Show Gist options
  • Save lukapetrovic-git/5ea8a8d15cafa02b2095dcd0873e08f2 to your computer and use it in GitHub Desktop.
Save lukapetrovic-git/5ea8a8d15cafa02b2095dcd0873e08f2 to your computer and use it in GitHub Desktop.

The following are steps to set up Azure AD as an Identity Provider for an on-prem Kubernetes cluster.

Starting point:

  • Azure Subscription
  • On-prem Kubernetes cluster (in my case RKE2 v1.27.12+rke2r1)

The scenario is pretty straightforward:

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:

credential-plugin-diagram

Steps:

- On the Azure side

  1. 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)

redirect-uri

  1. The App registration needs to have User.Read permissions to be able to sign users in and read their info.

api-permissions

  1. 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.

group-claim

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:

group-claim-large-ad

  1. Create a client secret to use when requesting tokens.

client-secret

- On the Kubernetes Cluster side:

  1. 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.

- On the client (Workstation) side

  1. Install kubelogin link
# Krew (macOS, Linux, Windows and ARM)
kubectl krew install oidc-login
  1. 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.
  1. 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).

- Assigning RBAC roles

Now that the authentication is done, we need to assign RBAC roles to our user.

  1. 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"]
  1. 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 
  1. 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

@PrathapDasari
Copy link

@lukapetrovic-git , Thank you for sharing this article. It will be helpful for anyone looking to implement this in their environment. I am planning to implement this solution on my on-premise cluster. This document will be beneficial for my setup.

@lukapetrovic-git
Copy link
Author

lukapetrovic-git commented Jul 26, 2024

@PrathapDasari
Hey, i'm just glad if it helps some people out there!

@vaspoz
Copy link

vaspoz commented Jan 7, 2025

@PrathapDasari , what is the group_object_id in the last point (even last line)? and should it be somehow linked to app registration you created earlier?

@lukapetrovic-git
Copy link
Author

@vaspoz
It is the object id of a group in Azure AD (Entra ID) that you wish to assign a Role to.

Azure Portal > Entra Id > Groups > click on a group and there you go

Screenshot 2025-01-09 115956

@vaspoz
Copy link

vaspoz commented Jan 9, 2025

@lukapetrovic-git , thanks!
Then how does the app registration know that this group is allowed to login?

@lukapetrovic-git
Copy link
Author

lukapetrovic-git commented Jan 11, 2025

@vaspoz
The flow is the following (after you have completed the steps above):

  1. You send a command over to the Kubernetes API like:
    kubectl get pods --kubeconfig <path-to-your-kubeconfig>

  2. Your kubeconfig file should look something like this:

users:
- name: luka
  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

The name field is in my case luka but it does not really matter.

The above means that kubectl will execute the command

kubectl 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>

You can try to execute this once you set everything up (from the terminal)
A browser opens and you log into Azure and if the authentication is successful you get a response like this:

{
  "kind": "ExecCredential",
  "apiVersion": "client.authentication.k8s.io/v1beta1",
  "spec": {
    "interactive": false
  },
  "status": {
    "expirationTimestamp": "2025-01-11T18:54:42Z",
    "token": "<encoded-token>"
  }
}

  1. You can take the encoded token from above (JWT) and decode it using something like https://jwt.io/
    Once you do that you can see output similair to this:

image

It contains all the Azure AD groups i am a member of.

  1. Your request to the Kubernetes API from step 1. (kubectl get pods) is now sent along with the token.
    Here the authorization is done - Kubernetes api decodes the token and looks at it - it looks at the group object ids from the token and tries to match it to RoleBinding/ClusterRoleBinding that authorizes the group to execute the request - list pods in this example kubectl get pods

So the authorization is done on the Kubernetes side, Azure is just used to authenticate you, and pass the token along to Kubernetes.

@pr07pr07
Copy link

pr07pr07 commented Mar 6, 2025

@lukapetrovic-git Finally, I got a chance to implement this, but somehow, I am unable to reach it due to the following errors.
The configurations seem to be correct:

configurations in apiserver:

  • --oidc-client-id=***************************************************
  • --oidc-issuer-url=**************************************************
  • --oidc-groups-claim=groups
  • --oidc-username-claim=unique_name

and also created a clusterrolebinding with clusterrole along with group objectID which is created in entraID.

Error:
2025-03-06T17:43:20.448622284+00:00 stderr F E0306 17:43:20.448576 1 authentication.go:74] "Unable to authenticate the request" err="invalid bearer token" 2025-03-06T17:43:46.204429535+00:00 stderr F E0306 17:43:46.204385 1 authentication.go:74] "Unable to authenticate the request" err="invalid bearer token"

any suggestions here ?

@vaspoz
Copy link

vaspoz commented Mar 6, 2025

@pr07pr07 , did you check the token? like that you have the group in the list.

@pr07pr07
Copy link

pr07pr07 commented Mar 6, 2025

@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)

@pr07pr07
Copy link

pr07pr07 commented Mar 6, 2025

@vaspoz Getting correct token when retrieving the token with all details but when checking with the same token it does not work as expected ..

do we need to give any permissions here ?

@vaspoz
Copy link

vaspoz commented Mar 6, 2025

@pr07pr07 , I had exactly the same issue couple of months ago lol. And it was an eeeeeasy fix, something that i just overlooked.
But i really don't remember, sorry man
maybe tomorrow with a fresh mind i would recall

@pr07pr07
Copy link

pr07pr07 commented Mar 6, 2025

@vaspoz That would be great if you could remember.

My suspicion is that something is missing in the K8s cluster itself. Anyhow, I can wait for some time; meanwhile, I will try to debug the issue.

Thank you in advance 👍

@pr07pr07
Copy link

pr07pr07 commented Mar 7, 2025

@vaspoz Also, may I know which version of K8s you implemented this on?

@lukapetrovic-git
Copy link
Author

lukapetrovic-git commented Mar 7, 2025

@pr07pr07
For me, it works on my current version of K8S - 1.30.8
Recheck you kubernetes api arguments maybe?

For example by getting the process on a master node
ps aux | grep kube-apiserver

or describing the kubernetes-api pod:
kubectl describe pod kube-apiserver-<node-name> -n kube-system

For me its like this:

		--oidc-groups-claim=groups 
		--oidc-groups-prefix=azuread: 
		--oidc-issuer-url=https://sts.windows.net/<tennant-id>/ 
		--oidc-username-claim=upn 
		--oidc-username-prefix=azuread: 

then because of the prefix i chose azuread: the role binding is as follows:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: azuread:<azure-group-object-id>

Maybe the prefixs dont match? By default it should be oidc:. If that's the case you should be seeing 401 unauthorized when trying to execute a command.

Also maybe check the content of token you are getting by executing:
kubectl 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>

@pr07pr07
Copy link

pr07pr07 commented Mar 7, 2025

@lukapetrovic-git

Yes, I am unable to see those arguments when describing kube-apiserver-. This could be the problem.

I have added them to /etc/kubernetes/manifests/kube-apiserver.yaml, but I still cannot see them when describing it.

@pr07pr07
Copy link

pr07pr07 commented Mar 7, 2025

@lukapetrovic-git

The location /etc/kubernetes/manifests/ is specifically designated for static pods. Any YAML file placed in this directory is automatically picked up by Kubernetes, which then creates the corresponding static pods.
During our backup process, a copy of the kube-apiserver.yaml file was placed in this directory.
As a result, Kubernetes automatically created an additional API server instance, leading to our changes not being reflected as expected.

image

Thank you !!

@lukapetrovic-git
Copy link
Author

@pr07pr07 glad you figured it out! :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment