Kubernetes OpenID Connect Integration with Resource Owner Flow

Hello πŸ˜„,

In this article, I will demonstrate how to configure Kubernetes (minikube) to use OpenID Connect as an authentication strategy.

We will cover the Resource Owner Password flow. Feel free chose the right authentication flow depending on your application’s needs.

Please refer to this diagram in order to choose the flow:

Note that the Client Credentials flow is not supported by Kubernetes. According to the official docs:

“To identify the user, the authenticator uses the id_token (not the access_token) from the OAuth2 token response as a bearer token. “

Since the Client Credentials flow only returns an access_token, we won’t be able to use this flow with Kubernetes.

Setup the OAuth client

To play around with these concepts we need to create an OAuth application. I prefer to use Auth0 so that everyone can follow along for free.

  1. Create an application of type Regular Web Application

2. Open the newly created application, go to Settings, scroll down and click advanced settings. On the Grant Types tab click the Password item and hit Save.

3. Goto Authentication -> Database and create a new database Connection. I named my connection Username-Password-Authentication.

4. Goto Settings -> Tenant and in the API Authorization Settings section set the Default Directory connection name to Username-Password-Authentication

5. Goto User Management -> Users and create a new user. I named my user: nuculabs-kube@nuculabs.dev and gave it the following password: Pa27rgN9KneN

Next, click on the user and set it’s email address as verified.

Setup Minikube

We can run Kubernetes locally on our computers using Minikube.

If it is your first time playing with Minikube then follow the installation instructions from: https://minikube.sigs.k8s.io/docs/start/ and start your minikube cluster with minikube start.

After your cluster is started stop it with minikube stop. This step is necessary otherwise you will encounter errors when running the next command.

Now, let’s start it with the following command:

 
minikube start --extra-config=apiserver.authorization-mode=RBAC --extra-config=apiserver.oidc-issuer-url="https://xxx.auth0.com/" --extra-config=apiserver.oidc-client-id=1RaJmjhjaapNLGXQjcYViQ15ZYzZoZdL --extra-config=apiserver.oidc-username-claim=email

The --extra-config=apiserver.oidc-issuer-url= must be equal to your Auth0 domain, it must start with https:// and end with a /.

The --extra-config=apiserver.oidc-client-id= will contain the Client ID of the OAuth client.

The --extra-config=apiserver.oidc-username-claim=email will be set to email because we want to map the email of our Auth0 user nuculabs-kube@nuculabs.dev to a user that we will create within Kubernetes.

Auth0 will return an id_token of the form Header.Payload.Signature. In our case, it will have a payload with a field email that will be equal to the user’s email address.

The following token doesn’t include the email claim, but it includes the name claim, in that case, we can use name as our apiserver.oidc-username-claim.

To play with JWTs visit: https://jwt.io/#debugger-io

If your Minikube cluster has started you should see the following output:

πŸ˜„ minikube v1.20.0 on Microsoft Windows 10 Home 10.0.19042 Build 19042
✨ Using the docker driver based on existing profile
πŸ‘ Starting control plane node minikube in cluster minikube
🚜 Pulling base image …
πŸ”„ Restarting existing docker container for "minikube" …
🐳 Preparing Kubernetes v1.20.2 on Docker 20.10.6 …
β–ͺ apiserver.authorization-mode=RBAC
β–ͺ apiserver.oidc-issuer-url=https://xxx.auth0.com/
β–ͺ apiserver.oidc-client-id=1RaJmjhjaapNLGXQjcYViQ15ZYzZoZdL
β–ͺ apiserver.oidc-username-claim=email
πŸ”Ž Verifying Kubernetes components…
β–ͺ Using image gcr.io/k8s-minikube/storage-provisioner:v5
🌟 Enabled addons: storage-provisioner, default-storageclass
πŸ„ Done! kubectl is now configured to use "minikube" cluster and "default" namespace by default

Congrats for making it this far! πŸŽ‰βœ¨

The next step is to map our nuculabs-kube@nuculabs.dev user to a admin role inside the Kubernetes cluster.

Open your favorite code editor, create clusterrole.yaml and paste the following contents in it:

kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1
metadata:
name: admin-role
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["*"]

Next, create clusterrolebinding.yaml and paste:

kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
metadata:
  name: admin-binding
subjects:
- kind: User
  name: nuculabs-kube@nuculabs.dev
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: ClusterRole
  name: admin-role

Apply the role and the role binding in minikube:

kubectl apply -f clusterrole.yaml
kubectl apply -f clusterrolebinding.yaml

Great, now we’ve interacted with the minikube cluster using a client certificate and key and the minikube user.

PS C:\Users\denis\Downloads> cat C:\Users\denis\.kube\config
apiVersion: v1
clusters:
...
users:
- name: minikube
  user:
    client-certificate: C:\Users\denis\.minikube\profiles\minikube\client.crt
    client-key: C:\Users\denis\.minikube\profiles\minikube\client.key

Next, we’ll update kubeconfig to switch to our nuculabs-kube@nuculabs.dev user.

Updating kubeconfig

The kubeconfig file is usually found in the ~/.kube/config directory and it contains information about clusters and users.

To send requests to minikube with the nuculabs-kube@nuculabs.dev user, first we need to grab the id_token:

curl --request POST \
  --url https://xxx.auth0.com/oauth/token \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data grant_type=password \
  --data client_id=1RaJmjhjaapNLGXQjcYViQ15ZYzZoZdL \
  --data client_secret=ZzxjjnOfxIj-__pUu7HCRNC9qknS5jYxOsTxd5Koz1uH7AmeXxtRClwlEW6WJLav \
  --data username=nuculabs-kube@nuculabs.dev \
  --data 'password=Pa27rgN9KneN'

{
  "access_token": "yepJxATWNN2hCZUmP4H4BzBaLycsUsJw",
  "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1FSkVNRVE1TWtFMlFrUTRNVEZFTWpZNE5qSkRORFkxTURBMk16bENORVJET1VNelJUSTBPUSJ9.eyJuaWNrbmFtZSI6Im51Y3VsYWJzLWt1YmUiLCJuYW1lIjoibnVjdWxhYnMta3ViZUBudWN1bGFicy5kZXYiLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvMzQ1NjMxNzQxNjU0ODViYWE1NDBjOTBkODc2MzAyN2I_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZudS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wNS0xM1QxOTo1Nzo0Mi42ODZaIiwiZW1haWwiOiJudWN1bGFicy1rdWJlQG51Y3VsYWJzLmRldiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiaXNzIjoiaHR0cHM6Ly9kbnV0aXUtZGV2LmV1LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2MDlkNzlkMjk2OWU3MjAwNjhhNmFhNjciLCJhdWQiOiIxUmFKbWpoamFhcE5MR1hRamNZVmlRMTVaWXpab1pkTCIsImlhdCI6MTYyMDkzNTg2MiwiZXhwIjoxNjIwOTcxODYyfQ.avnUJv2aM0vpTkcoGiO54N4y765BxpXHV2alYAVXCpFeaNI2ISW-lW0sFUFrU3w35oM4p2Xh2tlMmVoQplSvJL0mJ9qZObMDvdqijFGdUbYN3XnD2F0kI5CCwshGhP59cbS_gdXIwwz3SOwDKKjGvacvEPcOofmhcBxNVW16qP7GS2JaAnrbGygCpj6AOyRcCkAL-jz0rxQCPrwZ5i1E4ofsbH0H8cVprYYazBpNRKPWcadcMqvGAXrtkrkSzHvrmTbQNaXi2rUiDzepDeJvciYJsNefLGmW2iStpB4KuN9M0wpXdV2PAd6lMYAd3sYSn_4NYLQKEbtmL5Sp1lkxow",
  "scope": "openid profile email address phone",
  "expires_in": 86400,
  "token_type": "Bearer"
}

Note that the token expires in 86400 seconds, 24 hours, we can only use it in that time interval. Feel free to decode the token on jwt.io to inspect it’s contents. πŸ˜€

To tell kubectl to use the id_token we’ve just retrieved, we need to update it as follows:

apiVersion: v1
clusters:
- cluster:
    certificate-authority: C:\Users\denis\.minikube\ca.crt
    server: https://127.0.0.1:56840
  name: minikube
contexts:
- context:
    cluster: minikube
    namespace: default
    user: nuculabs-kube@nuculabs.dev
  name: minikube
current-context: minikube
kind: Config
preferences: {}
users:
- name: minikube2
  user:
    client-certificate: C:\Users\denis\.minikube\profiles\minikube\client.crt
    client-key: C:\Users\denis\.minikube\profiles\minikube\client.key
- name: nuculabs-kube@nuculabs.dev
  user:
    auth-provider:
      name: oidc
      config:
        client-id: 1RaJmjhjaapNLGXQjcYViQ15ZYzZoZdL 
        client-secret: ZzxjjnOfxIj-__pUu7HCRNC9qknS5jYxOsTxd5Koz1uH7AmeXxtRClwlEW6WJLav
        idp-issuer-url: https://xxx.auth0.com/
        id-token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Ik1FSkVNRVE1TWtFMlFrUTRNVEZFTWpZNE5qSkRORFkxTURBMk16bENORVJET1VNelJUSTBPUSJ9.eyJuaWNrbmFtZSI6Im51Y3VsYWJzLWt1YmUiLCJuYW1lIjoibnVjdWxhYnMta3ViZUBudWN1bGFicy5kZXYiLCJwaWN0dXJlIjoiaHR0cHM6Ly9zLmdyYXZhdGFyLmNvbS9hdmF0YXIvMzQ1NjMxNzQxNjU0ODViYWE1NDBjOTBkODc2MzAyN2I_cz00ODAmcj1wZyZkPWh0dHBzJTNBJTJGJTJGY2RuLmF1dGgwLmNvbSUyRmF2YXRhcnMlMkZudS5wbmciLCJ1cGRhdGVkX2F0IjoiMjAyMS0wNS0xM1QxOTo1Nzo0Mi42ODZaIiwiZW1haWwiOiJudWN1bGFicy1rdWJlQG51Y3VsYWJzLmRldiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwiaXNzIjoiaHR0cHM6Ly9kbnV0aXUtZGV2LmV1LmF1dGgwLmNvbS8iLCJzdWIiOiJhdXRoMHw2MDlkNzlkMjk2OWU3MjAwNjhhNmFhNjciLCJhdWQiOiIxUmFKbWpoamFhcE5MR1hRamNZVmlRMTVaWXpab1pkTCIsImlhdCI6MTYyMDkzNTg2MiwiZXhwIjoxNjIwOTcxODYyfQ.avnUJv2aM0vpTkcoGiO54N4y765BxpXHV2alYAVXCpFeaNI2ISW-lW0sFUFrU3w35oM4p2Xh2tlMmVoQplSvJL0mJ9qZObMDvdqijFGdUbYN3XnD2F0kI5CCwshGhP59cbS_gdXIwwz3SOwDKKjGvacvEPcOofmhcBxNVW16qP7GS2JaAnrbGygCpj6AOyRcCkAL-jz0rxQCPrwZ5i1E4ofsbH0H8cVprYYazBpNRKPWcadcMqvGAXrtkrkSzHvrmTbQNaXi2rUiDzepDeJvciYJsNefLGmW2iStpB4KuN9M0wpXdV2PAd6lMYAd3sYSn_4NYLQKEbtmL5Sp1lkxow

Note: The authentication won’t work if the user doesn’t has it’s email verified: E0513 20:03:32.089659 1 authentication.go:53] Unable to authenticate the request due to an error: [invalid bearer token, oidc: email not verified] you will need to create another user or set the user’s email as verified from the Auth0 interface.

To verify that the authentication works save the kubeconfig file and run:

PS C:\Users\denis\Downloads> kubectl get services
NAME         TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1    <none>        443/TCP   33m

If you change a single byte in the id-token, then the authentication won’t work anymore.

✨ We’ve successfully interacted with Minikube with our Auth0 user. ✨

Refreshing the token

Since the Resource Owner flow doesn’t return a refresh_token, the oidc authorization provider plugin for kubectl won’t be able to refresh the token, thus a manual refresh is needed.

The Kubernetes documentation offers a solution for this: ExecCredentials. You can use an existing go plugin or write yourself a program that gets executed and prints an ExecCredential object to stdout, which looks like this:

{
  "apiVersion": "client.authentication.k8s.io/v1beta1",
  "kind": "ExecCredential",
  "status": {
    "token": "my-bearer-token"
  }
}

If an expirationTimestamp is provided along with the token then kubectl will cache the token until the token expires otherwise if the expirationTimestamp is missing then kubectl will use the token until the server responds with 401.

A kubectl file made for an ExecCredentials scenario would look like this:

users:
- name: nuculabs-kube@nuculabs.dev
  user:
    exec:
      apiVersion: "client.authentication.k8s.io/v1beta1"
      command: "my-program-that-prints-an-credential"
      args:
      - "print-token"

Thank you for reading πŸ“š!

References:

Article cover photo by θ΄θŽ‰ε„Ώ DANIST.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.