Microsoft Azure/Entra SSO + AWS EKS + Oauth2-Proxy with Kubernetes-Dashboard

My goal was to deploy the Kubernetes Dashboard in a managed Kubernetes cluster with AWS EKS. The dashboard is secured via single sign-on via Microsoft Entra in combination with the OAuth2 Proxy and NGINX Ingress controller.

In the following I will show the those steps:

  • Microsoft Entra OAuth2 Application with terraform
  • OAuth2 Proxy setup with Microsoft Entra
  • Kubernetes Dashboard configured with authorization header for authenticating users
  • AWS EKS with Entra as OIDC provider because the Kubernetes Dashboard uses the Kubernetes API for authorization
  • Troubleshooting

Microsoft Entra OAuth 2.0 Application

The application will have two roles, one for Support users that can only manage deployments in some namespaces via the Kubernetes Dashboard and Admins who can access all deployments/namespaces. Only users that have one of those roles are allowed to access the dashboard via Oauth2 Proxy, we will configure that later in the Oauth2 Proxy config.

The application roles are assigned to existing entra groups to control which entra users are allowed to access the dashboard.

How to setup an Entra application for OAuth2 Proxy via the GUI is very well documented.

I will show how those steps look like as Infrastructure as code (IaC) in terraform code.

locals {
  # permission ids taken from: https://learn.microsoft.com/en-us/graph/permissions-reference
  msgraph_user_read_permission_id      = "e1fe6dd8-ba31-4d61-89e7-88639da4683d" 
  msgraph_openid_profile_permission_id = "14dad69e-099b-42c9-810b-d002981feec1"
  msgraph_openid_permission_id         = "37f7f235-527c-4136-accd-4a02d197296e"
  msgraph_openid_email_permission_id   = "64a6cdd6-aab1-4aaf-94b8-3cc8405e90d0"
}

# Used to get ID of predefined Graph API permissions
resource "azuread_service_principal" "msgraph" {
    client_id    = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph
    use_existing = true
}


resource "random_uuid" "aws_oauth_proxy_support_role" {
}

resource "random_uuid" "aws_oauth_proxy_admin_role" {
}

resource "azuread_application" "aws_oauth2_proxy" {
  display_name = "Oauth2 Proxy"

# required permissions for Oauth2-Proxy 
  required_resource_access {
    resource_app_id = data.azuread_application_published_app_ids.well_known.result.MicrosoftGraph

    resource_access {
      id   = local.msgraph_openid_email_permission_id
      type = "Scope"
    }
    resource_access {
      id   = local.msgraph_openid_permission_id
      type = "Scope"
    }
    resource_access {
      id   = local.msgraph_openid_profile_permission_id
      type = "Scope"
    }

    resource_access {
      id   = local.msgraph_user_read_permission_id 
      type = "Scope"
    }

    resource_access {
      id   = azuread_service_principal.msgraph.app_role_ids["Group.Read.All"]
      type = "Scope"
    }

  }

# fill oauth2 groups claim with ids of security and microsoft 365 groups
  group_membership_claims = ["All"]

  optional_claims {
    id_token {
      additional_properties = []
      essential             = false
      name                  = "groups"
    }
  }

# application roles, will be in the oauth2 roles claim and will be assigned to entra groups to control which users can access the dashboard
  app_role {
    allowed_member_types = ["User"]
    description          = "Can access the kubernetes-dashboard"
    display_name         = "Support"
    id                   = random_uuid.aws_oauth_proxy_cloud_support_role.id
    enabled              = true
    value                = "Support"
  }

  app_role {
    allowed_member_types = ["User"]
    description          = "Can access the kubernetes-dashboard"
    display_name         = "Admin"
    id                   = random_uuid.aws_oauth_proxy_admin_role.id
    enabled              = true
    value                = "Admin"
  }

# temporary used oauthdebugger.com for troubleshooting
  web {
    redirect_uris = [
      "https://your-oauth2-domain/oauth2/callback",
      # "https://oauthdebugger.com/debug"
    ]
  }

# To assign entra users/groups via the GUI
  feature_tags {
    enterprise = true
  }

# Not needed for Oauth2-Proxy but for the AWS EKS Identity Provider we will connect later
  fallback_public_client_enabled = true

}

# Applicaton role to entra group mapping
resource "azuread_app_role_assignment" "oauth2_proxy_admin" {
  app_role_id         = random_uuid.aws_oauth_proxy_admin_role.id
  principal_object_id = azuread_group.admin.object_id
  resource_object_id  = azuread_service_principal.oauth2_proxy.object_id
}

# Applicaton role to entra group mapping
resource "azuread_app_role_assignment" "oauth2_proxy_support" {
  app_role_id         = random_uuid.aws_oauth_proxy_support_role.id
  principal_object_id = azuread_group.support.object_id
  resource_object_id  = azuread_service_principal.oauth2_proxy.object_id
}

#
resource "azuread_service_principal" "oauth2_proxy" {
client_id                     = azuread_application.aws_oauth2_proxy.client_id
preferred_single_sign_on_mode = "oidc"
owners                        = [data.azuread_client_config.current.object_id]
}

# to grant admin consent on the necessary permissions we declared in the resources azuread_application.aws_oauth2_proxy required_permissions_block
resource "azuread_service_principal_delegated_permission_grant" "oauth2_proxy_admin_consent" {
  service_principal_object_id          = azuread_service_principal.oauth2_proxy.object_id
  resource_service_principal_object_id = azuread_service_principal.msgraph.object_id
  claim_values                         = ["openid", "profile", "email", "Group.Read.All", "User.Read"]
}



# client secret
resource "azuread_application_password" "aws_oauth2_proxy_client_secret" {
  application_id = azuread_application.aws_oauth2_proxy.id

  display_name = "OAuth2 Proxy"
  end_date     = "2026-03-28T06:00:00Z"
}


# For convenience to extract the secret via terraform output
output "aws_oauth_proxy_client_secret" {
  value     = azuread_application_password.aws_oauth2_proxy_client_secret.value
  sensitive = true
}

output "aws_oauth_proxy_client_id" {
  value = azuread_application.aws_oauth2_proxy.client_id
}

Having configured the entra side, we continue to configure the Oauth2 Proxy to use this application.

Troubleshooting

If you want to see the OAuth2 response of your entra application you can use https://oauthdebugger.com/ To make it work, add https://oauthdebugger.com/debug as redirect url to your application.

After sending the request a POST Request template will be shown to you. Just replace the clientSecret placeholder with your real secret of the entra application (terraform output aws_oauth_proxy_client_secret)

The Post request will look like this:

POST https://login.microsoftonline.com/xxx/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&
code=XXX&
client_id=XXX&
client_secret={clientSecret}&
redirect_uri=https%3A%2F%2Foauthdebugger.com%2Fdebug

You can copy this code and execute it directly e.g. in a http File with IntelliJ

The result is a reponse with the access_token / id_token. To decode those two tokens und can use the jwt debugger. In your id token you should see the configured roles/groups claim.

OAuth2 Proxy

I used OAuth2 Proxy in Version 7.5.1, deployed as Helm Chart in Kubernetes.

Here is my OAuth2 Proxy configuration file for the Chart:

extraArgs:
  provider: azure
  set-authorization-header: true
  provider-display-name: SSO Login
  client-id: {{ .Values.entra.oauth2Proxy.clientId }} # Entra Application (Client) ID
  client-secret: {{ .Values.entra.oauth2Proxy.clientSecret }} # Entra app registration client secret from terraform output 
  oidc-issuer-url: "https://login.microsoftonline.com/{{ .Values.entra.tenantId }}/v2.0"
  user-id-claim: oid
  oidc-groups-claim: roles # i used roles instead of groups because the groups are just IDs
  allowed-group: "Support,Admin" # mapped as app roles in entra, see Step [Microsoft Entra OAuth 2.0 Application](#microsoft-entra-application)
  azure-tenant: {{ .Values.entra.tenantId }} # Entra Tenant ID
  whitelist-domain: "{{ .Values.entra.oauth2Proxy.cookieDomain }},login.microsoftonline.com"
  cookie-domain: {{ .Values.entra.oauth2Proxy.cookieDomain  }}
  scope: "openid profile email"

ingress:
  enabled: true
  path: /
  hosts:
  - {{ .Values.entra.oauth2Proxy.host }}

Any user/group that has assigned one of the groups in allowed-group should be able to login. Users that are not part of one of those groups will receive a 403 forbidden status.

Important: The flag set-authorization-header (Docs: set Authorization Bearer response header (useful in Nginx auth_request mode)) is necessary to pass the oauth2 token via header to the Kubernetes Dashboard.

Troubleshooting

To seet the information returned by OAuth2 Proxy you can use the /oauth2/auth endpoint. Open your Browser developer tools and go to https://your-oauth2-proxy/oauth2/auth - In the Network-Tab you will see the returned token which you can decode with jwt debugger.

Kubernetes Dashboard

After setting up OAuth2 Proxy, we can provide authentication for the Kubernetes-Dashboard and we will use Header Authorization. The authorization token is passed to the Dashboard. The Dashboard was installed via Helm Chart in version v2.7.0.

This is my configuration file for the Chart, the important part is the ingress configuration

ingress:
  enabled: true
  hosts:
    - {{ .Values.monitoring.kubernetesDashboardIngressHost }}
  className: nginx
  annotations:
    nginx.ingress.kubernetes.io/proxy-buffering: "on"
    nginx.ingress.kubernetes.io/proxy-buffer-size: "1M"
    nginx.ingress.kubernetes.io/auth-signin: "https://{{ .Values.entra.oauth2Proxy.host }}/oauth2/start"
    nginx.ingress.kubernetes.io/auth-url: "https://{{ .Values.entra.oauth2Proxy.host }}/oauth2/auth"
    nginx.ingress.kubernetes.io/configuration-snippet: |
      auth_request_set $token $upstream_http_authorization;
      proxy_set_header Authorization $token;
      # debug "Header: $token"; not necessary, could be used to show the token in your browsers developer tools       

extraArgs:
  - --enable-insecure-login=true  {{/*SSL Termination at ingress*/}}

Important: The annotation configuration-snippet makes the magic happen and sets the authorization header. auth-signin / auth-url are necessary to protect the dashboard with OAuth2 Proxy.

If you open the dashboard now, you would not see any information about your cluster. We still have to implement the following two points:

  • Roles and permissions with Kubernetes RBAC
  • The dashboard just passes the authorization token to the Kubernetes API server and we have not properly configured our cluster yet to accept these tokens.

The following example shows the use case: All users who are authenticated can see all namespaces so that they are available for selection in the dashboard. In addition, users of a specific Entra group can manage deployments in a namespace.

Authenticated users can see all Namespaces, therefore we use the Cluster wide resources ClusterRole / ClusterRoleBinding

 kind: ClusterRole
  apiVersion: rbac.authorization.k8s.io/v1
  metadata:
  name: read-namespaces
  rules:
    - apiGroups:
        - ""
          resources:
        - namespaces
          verbs:
        - get
        - list
---
  kind: ClusterRoleBinding
  apiVersion: rbac.authorization.k8s.io/v1
  metadata:
  name: dasbhoard-read-namespaces
  roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: read-namespaces
  subjects:
    - kind: Group
      name: "system:authenticated"
      apiGroup: rbac.authorization.k8s.io
    

And to limit the management of deployments/pods to a Namespace, we use a RoleBinding

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: kubernetes-dashboard-manage-deployments
rules:
  - apiGroups:
      - ""
    resources:
      - pods
      - pods/log
    verbs:
      - get
      - list
  - apiGroups:
      - ""
    resources:
      - events
    verbs:
      - get
      - list
  - apiGroups:
      - "apps"
    resources:
      - deployments
    verbs:
      - get
      - list
      - update
---
kind: RoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: dasbhoard-manage-deployments
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: kubernetes-dashboard-manage-deployments
subjects:
  - kind: Group
    name: "entra-group:id-of-your-entra-group-which-should-see-all-deployments"
    apiGroup: rbac.authorization.k8s.io

Important: In RoleBinding dasbhoard-manage-deployments i refer to an entra group (name: "entra-group:id-of-your-entra-group-which-should-see-all) with the prefix entra-group. The prefix is attached by the EKS OIDC provider we will set up in the next part. You could also refer to single users:

subjects:
- kind: User
  name: "entra-email:entra-user-principal-name"
  apiGroup: rbac.authorization.k8s.io

EKS OIDC Provider

The configuration of the EKS OIDC Provider is very simple:

resource "aws_eks_identity_provider_config" "provider" {
  cluster_name = "your-eks-cluster-name"

  oidc {
    client_id                     = "xxxx" # Entra application ID
    identity_provider_config_name = "Entra SSO"
    issuer_url                    = "https://login.microsoftonline.com/<your-tenant-id>/v2.0"
    username_claim                = "email"
    username_prefix               = "entra-email:"
    groups_claim                  = "roles"
    groups_prefix                 = "entra-role:"
  }
}

After applying the terraform code, everything is ready. You can now log into the Kubernetes-Dashboard via SSO and restrict access of who can login via OAuth2-Proxy and what each users can see in the Dashboard via RBAC.