Architecture

Kubernetes and AzureAD SSO

April 4, 2022

by

Marc Boorshtein

AzureAD is a popular identity system for large enterprises. They're most likely already running Active Directory, and if they're using Azure as their cloud they'll need to use AzureAD for permissions. AKS, Azure's own Kubernetes distribution, integrates directly with AzureAD and the Azure CLI, but what about your other clusters? You may have on premises clusters or clusters running in other clouds. If AzureAD is your system of record, you'll want to integrate your clusters to make sure you get the benefits. This blog post will walk you through the integration process between Kubernetes and AzureAD using OpenUnison from start to finish.

Before diving in, you might ask, "Why would I use OpenUnison with AzureAD when Kubernetes already support OpenID Connect?" There are a few important answers to that question:

  1. Provides group names, not the object id for RBAC bindings
  2. Provides SSO for both the kubectl CLI and the Kubernetes dashboard
  3. Zero configuration clients using either the OpenUnison portal or the oulogin kubectl plugin
  4. One tool to manage all the integration

For #1, AzureAD doesn't send a group's name in the id_token, it sends an ID. For instance, here's an example id_token from AzureAD:

{
  "aud": "e3edb579-187a-49b6-bf80-5418b818bbb9",
  "iss": "https://login.microsoftonline.com/61cbe426-d3ca-4ebd-8ca4-e47e354a85bb/v2.0",
  "iat": 1648235554,
  "nbf": 1648235554,
  "exp": 1648235854,
  "auth_time": 1648235852,
  "family_name": "Admin",
  "given_name": "K8s",
  "name": "K8s Admin",
  "oid": "6c2e1de2-b864-4e1b-a01f-0baa4da67c06",
  "preferred_username": "k8sadmin@marcboorshteintremolosecuri.onmicrosoft.com",
  "rh": "0.ARMAJuTLYcrTvU6MpOR-NUqFu3m17eN6GLZJv4BUGLgYu7kTAHU.",
  "roles": [
    "212702a6-f1e0-4b7e-aa12-f66a610b119c"
  ],
  "sub": "HGha_KjmLta679qnxCbZmiNrDj7IwcAxlbh5o1M3Kz4",
  "tid": "61cbe426-d3ca-4ebd-8ca4-e47e354a85bb",
  "uti": "oMNX2g3CKkquh4VI5yXxAA",
  "ver": "2.0"
}

Notice that the roles claim has a single value, "212702a6-f1e0-4b7e-aa12-f66a610b119c". This is the object id of the k8s-administrator group in AzureAD. Now if you're just going to have a single ClusterRoleBinding to give everyone cluster admin access, then using "212702a6-f1e0-4b7e-aa12-f66a610b119c" isn't likely an issue. What if you had five, ten, twenty Cluster and RoleBinding objects? Auditing that will get difficult quickly. While using an ID is great from many aspects, it doesn't work well in Kubernetes, because RBAC is just comparing strings and "212702a6-f1e0-4b7e-aa12-f66a610b119c" has no meaning to cluster operators. OpenUnison will translate that id into a group name, which is much easier to manage and track in your cluster.

The second and third reason to use OpenUnison to integrate AzureAD and Kubernetes are the standard benefits when using OpenUnison with any identity source. OpenUnison generates both windows and *nix commands for configuring your kubectl configuration without having to pre-distribute anything, including certificates. The oulogin plugin for kubectl makes this even easier, because you don't need to login to the portal first. Simply run kubectl oulogin --host=myopenunison.domain.dev and a browser window is opened for you to login and setup kubectl.

It also gives you secure access to the dashboard. I know, many of you reading this are saying "The dashboard is insecure and I don't want to use it anyways!" First, it's only insecure when deployed insecurely. OpenUnison locks down the dashboard so that you don't need to worry about someone compromising it or getting a privileged service account. It's locked down as tightly as your API server.

Finally, If you didn't want to use OpenUnison, you could get the same setup by integrating multiple systems. You could deploy an identity provider, integrate it with a tool like gangway to get SSO, then implement an oauth2 proxy or kube-oidc-proxy with the dashboard, and finally create some kind of portal to give developers easy access to everything. That's a considerable amount of work that OpenUnison does for you.

Now that you know the "why" of using OpenUnison, lets deploy it to our cluster.

What You Need in Your Cluster

Before deploying OpenUnison, you'll need three things:

  1. An Ingress Controller, like NGINX. OpenUnison supports multiple Ingress controllers and the helm charts will manage the YAML creation for you.
  2. A Load Balancer that listens on port 443. If you are doing development with a single node, you can also just patch your ingress controller to listen on the host on 443.
  3. Deploy the Kubernetes Dashboard, but just the base config. Don't create additional objects like Ingresses.

With those three things in place, let's deploy OpenUnison! Now that we're ready to deploy OpenUnison, the first question is often "how long will this take?" If you have everything together, and everything goes correctly, it should take fifteen - twenty minutes. This blog walks you through every step, but often we learn best by seeing. Here's a detailed video that walks through all the configuration steps:

Initial Configuration Steps

This guide is a streamlined version of the docs from https://openunison.github.io/. With out pre-requisites in place, the first step is to download the helm repos:

helm repo add tremolo https://nexus.tremolo.io/repository/helm/
helm repo update

Next, download the ouctl utility for your platform and rename it to ouctl.

Configure OpenUnison Host Names

This is the part where I most encounter people having issues. If your cluster is an on-premises cluster, or is deployed using an on-premises tool like kubeadm or kops, you'll want to integrate OpenUnison with your cluster directly using OpenID Connect. If you're using a cloud managed cluster, such as CIVO or EKS, you'll need to use OpenUnison in impersonation mode. The choice is important here because if you are integrating your cluster via OpenID Connect, you need two hosts. If using impersonation, you need three.

OpenUnison Networking

The above graphic shows how OpenUnison's networking comes together. The host names you choose must be unique and must all resolve to the IP address of your load balancer. Once you've decided on host names, update your values.yaml's network.openunison_host, openunison.dashboard_host, and if using impersonation openunison.api_server_host. If you're not using impersonation, set network.k8s_url to the URL you want users to interact with for your API server, usually a load balancer.

Finally, if using impersonation set enable_impersonation to true. With networking configured, the next step is to configure AzureAD.

Configuring AzureAD

The first step is to define a new App registration in your AzureAD configuration. Use a name that is descriptive. For the Redirect URI use Web and specify the URL for OpenUnison's redirect uri. It will be https://OU_HOST/auth/oidc where OU_HOST is the value from network.openunison_host in your values.yaml.

AzureAD Application Registration

Once your application is registered, the next step is to create a client secret. Click on Add a certificate or secret in the upper left hand section of the screen.

AzureAD - Create Client Secret Link

Click on + New client secret, give it a description and a life span. How long the secret is valid is based on your requirements and compliance needs. Once you click Add, the secret will become available for you to copy. Copy it, and store it to a file that the ouctl command will use when deploying OpenUnison.

With the client secret created and stored in your cluster, the next step is to setup your token. Next, click on Token configuration on the left hand side. Next, click on Add optional claim and choose email, family_name,given_name, upn, and preferred_username and click Add. When AzureAD asks if you want to add the scopes automatically, agree. Adding these claims isn't required, but will make it much easier to manage your integration.

AzureAD - Token Configuration

The last configuration step is to enable the correct APIs and provide organizational consent. Click on API permissions on the left hand side. The email, profile, and User.Read delegated permissions are already present. Click on + Add a permission, Choose Microsoft Graph, and then Application Permissions. Under User check User.Read.All. Under Group check Group.Read.All. Under GroupMember check GroupMember.Read.All. Click Add permission.

AureAD - API Configuration

Next, click on Grant admin consent for Default Directory and answer Yes. This will allow OpenUnison to lookup the user's groups.

AzureAD - Grant Permissions

Return to the Overview section to configure OpenUnison.

Configure and Deploy OpenUnison

With AzureAD and our networking configured, we can deploy OpenUnison. In your values.yaml, uncomment the oidc section and update according to the below example:

oidc:
  # get your client_id from your app registration's Application (client) ID
  client_id: e3edb579-187a-49b6-bf80-5418b818bbb9
  # replace TENNANT_ID with the value from Directory (tenant) ID from the overview screen of your app registration
  issuer: https://login.microsoftonline.com/TENNANT_ID/v2.0/
  user_in_idtoken: true
  domain: ""
  scopes: openid email profile
  claims:
    sub: upn
    email: email
    given_name: given_name
    family_name: family_name
    display_name: name
    groups: roles

In addition to setting oidc.client_id and oidc.issuer, change :

  1. oidc.user_in_idtoken to true
  2. Remove groups from oidc.scopes
  3. Change oidc.claims.sub to upn
  4. Change oidc.claims.groups to roles

With the updates to the oidc section, add

azure:
  # replace with the value from Directory (tenant) ID from the overview screen of your app registration
  tennant_id: 61cbe426-d3ca-4ebd-8ca4-e47e354a85bb

The final configuration change is to set openunison.include_auth_chain to azuread-load-groups. NOTE: openunison.include_auth_chain is NOT included in the default values yaml. This last step will tell OpenUnison to run our custom authentication mapping so that users groups will have names instead of ids.

All configuration is complete. The next step is to deploy the charts that will configure OpenUnison:

./ouctl install-auth-portal -s /path/to/azuread/secret /path/to/values.yaml
helm install orchestra-login-azuread tremolo/orchestra-login-azuread -n openunison -f /path/to/openunison-values.yaml

The first command deploys the OpenUnison authentication portal. The second command adds the Azure specific configuration options to better work with AzureAD. Once done, you should be able to go to the host you used for network.openunison_host and login. You'll see a set of "badges" with your user principal name in the upper left hand corner:

OpenUnison Portal

Click on your user principal name and you'll see your profile and groups:

OpenUnison User Profile

You'll see that your profile includes your groups by name, not by id. We'll use that for configuring RBAC. With OpenUnison and AzureAD integrated, we can now integrate OpenUnison and your cluster.

Integrating OpenUnison and Kubernetes

If you're using impersonation, you can skip this step entirely and go straight to configuring RBAC. Otherwise, the last step is to integrate OpenUnison and your cluster. Assuming you let the OpenUnison operator generate certificates for you, you'll need to retrieve it and copy it to each of your API servers:

kubectl get secret ou-tls-certificate -n openunison -o json | jq -r '.data["tls.crt"]' | base64 -d > /path/to/ou-ca.pem

How you distribute this file will depend on how you deployed Kubernetes. Next, get the API server configuration flags from a ConfigMap generated for you:

kubectl describe configmap api-server-config -n openunison

Again, how you apply these flags will be dependent on your deployment tool for your cluster. Once run, you should be able to login to OpenUnison, click on the dashboard, and see numerous error messages that say your user is not authorized to access various resources. This is because the API server recognizes who you are, but you haven't been authorized via RBAC. The next section will complete that process.

RBAC Configuration

Finally, with everything configured, the last step is to create RBAC policies and bindings. Since we've translated AzureAD's group ids into names, this becomes pretty easy. Create a group and create a binding! For instance, to make all members of the k8s-administrator group from AzureAD cluster admins, create the binding:

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: azuread-cluster-admins
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: cluster-admin
subjects:
- apiGroup: rbac.authorization.k8s.io
  kind: Group
  name: k8s-administrator

Go back to the dashboard and you'll see the error messages are gone and you can now access the entire cluster. Now you can design RBAC policies based on your needs using AzureAD group names.

What's Next?

You've completed your integration of AzureAD and Kubernetes, so now what? Beyond using your cluster, you can integrate additional applications into OpenUnison. This creates a developer portal giving your users secure access to all of their cluster applications without having to create additional AzureAD application registrations. If you're interested in learning the details of how Kubernetes authentication works, you can get the authentication chapter from Kubernetes: An Enterprise Guide (link to github) for free without any registration. You can also move on to Namespace as a Service to provide self service access to your clusters. Finally, follow me on twitter at mlbiam and tremolosecurity!

Related Posts