<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Kubernetes on Abhinav Praveen's Blog</title><link>https://blog.praveen.org.uk/tags/kubernetes/</link><description>Recent content in Kubernetes on Abhinav Praveen's Blog</description><generator>Hugo -- gohugo.io</generator><language>en-gb</language><lastBuildDate>Sat, 14 Mar 2026 16:53:37 +0000</lastBuildDate><atom:link href="https://blog.praveen.org.uk/tags/kubernetes/index.xml" rel="self" type="application/rss+xml"/><item><title>Using OIDC Authentication with Kubernetes, Keycloak and the OAuth 2.0 Device Authorization Grant</title><link>https://blog.praveen.org.uk/posts/kubernetes-keycloak-device-authorization/</link><pubDate>Sat, 14 Mar 2026 16:53:37 +0000</pubDate><guid>https://blog.praveen.org.uk/posts/kubernetes-keycloak-device-authorization/</guid><description>&lt;h1 id="intro">Intro
&lt;/h1>&lt;p>This page is an alternative/extension of &lt;a class="link" href="https://blog.praveen.org.uk/posts/kubernetes-oidc/" >my previous post&lt;/a> which describes how to use the Device Authorization Grant with Kubernetes &amp;amp; Keycloak. With this approach the usage flow is as follows:&lt;/p>
&lt;ul>
&lt;li>The user runs a &lt;code>token.sh&lt;/code> script&lt;/li>
&lt;li>The script prints a link to the Identity Provider for the user to click&lt;/li>
&lt;li>The user clicks the link and permits information to be passed to the scripts&lt;/li>
&lt;li>The script sets the identity token and refresh token in &lt;code>kubectl&lt;/code>&amp;rsquo;s configuration&lt;/li>
&lt;li>Future invocations of &lt;code>kubectl&lt;/code> renew the ID Token with the Refresh Token if possible&lt;/li>
&lt;/ul>
&lt;p>This has been tested with RKE2 v1.35.1, &lt;code>kubectl&lt;/code> 1.34.1, &lt;code>curl&lt;/code>, &lt;code>jq&lt;/code>, an Keycloak (v26.5.4) but this should work for any standard compliant Identity Provider.&lt;/p>
&lt;h1 id="kubernetes-api-server">Kubernetes API Server
&lt;/h1>&lt;p>Setting up the API Server is the same as before&lt;/p>
&lt;p>The relevant documentation for this is: &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/" target="_blank" rel="noopener"
>Authentication&lt;/a>. I will assume that the StructuredAuthenticationConfiguration Kubernetes feature gate is enabled. This has been the default &lt;a class="link" href="https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/" target="_blank" rel="noopener"
>since Kubernetes v1.30&lt;/a>.&lt;/p>
&lt;p>Write an &lt;code>AuthenticationConfiguration&lt;/code> like the following&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">apiVersion&lt;/span>: &lt;span style="color:#ae81ff">apiserver.config.k8s.io/v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">kind&lt;/span>: &lt;span style="color:#ae81ff">AuthenticationConfiguration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">jwt&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">issuer&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">url&lt;/span>: &lt;span style="color:#ae81ff">https://idp.example.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">audiences&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">k8s&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">audienceMatchPolicy&lt;/span>: &lt;span style="color:#ae81ff">MatchAny&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claimMappings&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">username&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claim&lt;/span>: &lt;span style="color:#ae81ff">upn&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">prefix&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;oidc:&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">groups&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claim&lt;/span>: &lt;span style="color:#ae81ff">groups&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">prefix&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;oidc:&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">uid&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claim&lt;/span>: &lt;span style="color:#ae81ff">sub&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">userValidationRules&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">expression&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;!user.username.startsWith(&amp;#39;system:&amp;#39;)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">message: &amp;#39;username cannot used reserved system&lt;/span>: &lt;span style="color:#ae81ff">prefix&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">expression&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;user.groups.all(group, !group.startsWith(&amp;#39;system:&amp;#39;))&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">message: &amp;#39;groups cannot used reserved system&lt;/span>: &lt;span style="color:#ae81ff">prefix&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here make sure that:&lt;/p>
&lt;ul>
&lt;li>The OpenID Connect &lt;code>.well-known/openid-configuration&lt;/code> endpoint is immediately above the issuer URL (i.e. at &lt;code>https://idp.example.com/.well-known/openid-configuration&lt;/code>). In Keycloak&amp;rsquo;s case, I have a realm called &amp;lsquo;machine&amp;rsquo; so my well-known would be at: &lt;code>https://idp.example.com/realms/machine/.well-known/openid-configuration&lt;/code>. That means that my issuer URL should be: &lt;code>https://idp.example.com/realms/machine&lt;/code>.&lt;/li>
&lt;li>Your Identity Provider is able to issue client ID Tokens with an audience of &lt;code>k8s&lt;/code> or change it to if different audience if needed.&lt;/li>
&lt;li>Your Identity Provider is able to provide a &lt;code>groups&lt;/code> claim with your ID Token for use as Kubernetes groups. This may vary by Identity Provider.&lt;/li>
&lt;/ul>
&lt;p>Write the &lt;code>AuthenticationConfiguration&lt;/code> to a file somehwere, say &lt;code>/path/to/authconf.yaml&lt;/code> and start the Kubernetes API Server referring to it with: &lt;code>--authentication-config=/path/to/authconf.yaml&lt;/code>&lt;/p>
&lt;h1 id="client">Client
&lt;/h1>&lt;p>Here, unlike the previous post, create a public client (i.e. one that does not have a client secret). The only authentication flow enabled should be the OAuth 2.0 Device Authorization Grant. The client ID should normally be the same as one of the &lt;code>audiences&lt;/code> items in the configuration above &amp;ndash; I will assume that it is &lt;code>k8s&lt;/code>. Keycloak will insert this as the &lt;code>aud&lt;/code> claim. Finally, set up the clients scopes so that issued ID Tokens contains a &lt;code>sub&lt;/code> claim, &lt;code>upn&lt;/code> claim, and a &lt;code>groups&lt;/code> claim. On Keycloak, this can be done by assigning the predefined &lt;code>basic&lt;/code> and &lt;code>microprofile-jwt&lt;/code> scopes.&lt;/p>
&lt;h1 id="token-fetch-script">Token Fetch Script
&lt;/h1>&lt;p>A rough script to facilitate the device authorization flow is below:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>CACHE&lt;span style="color:#f92672">=&lt;/span>/run/user/&lt;span style="color:#66d9ef">$(&lt;/span>id -u&lt;span style="color:#66d9ef">)&lt;/span>/k8s-oauth-cache
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>mkdir -p &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>device_req&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> curl -s -d client_id&lt;span style="color:#f92672">=&lt;/span>k8s -d scope&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;openid basic microprofile-jwt&amp;#34;&lt;/span> https://idp.example.com/realms/REALMNAME/protocol/openid-connect/auth/device
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>device_obt&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jq -r .device_code &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/device&amp;#34;&lt;/span> | awk &lt;span style="color:#e6db74">&amp;#39;{print &amp;#34;device_code=&amp;#34;$1}&amp;#39;&lt;/span> | curl -s -d grant_type&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;urn:ietf:params:oauth:grant-type:device_code&amp;#34;&lt;/span> -d client_id&lt;span style="color:#f92672">=&lt;/span>k8s --data @- https://idp.example.com/realms/REALMNAME/protocol/openid-connect/token
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>apply&lt;span style="color:#f92672">()&lt;/span> &lt;span style="color:#f92672">{&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> kubectl config set-credentials oidc --auth-provider oidc --auth-provider-arg&lt;span style="color:#f92672">=&lt;/span>idp-issuer-url&lt;span style="color:#f92672">=&lt;/span>https://idp.example.com/realms/REALMNAME --auth-provider-arg&lt;span style="color:#f92672">=&lt;/span>client-id&lt;span style="color:#f92672">=&lt;/span>k8s --auth-provider-arg&lt;span style="color:#f92672">=&lt;/span>refresh-token&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>jq -r .refresh_token &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/token&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span> --auth-provider-arg&lt;span style="color:#f92672">=&lt;/span>id-token&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>jq -r .id_token &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/token&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> exit &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$?&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">}&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>device_req &amp;gt; &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/device&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>INTERVAL&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$(&lt;/span>jq -r .interval &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/device&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">)&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>echo &lt;span style="color:#e6db74">&amp;#34;Click on:&amp;#34;&lt;/span> &amp;gt; /dev/stderr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>jq .verification_uri_complete &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/device&amp;#34;&lt;/span> &amp;gt; /dev/stderr
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">for&lt;/span> i in &lt;span style="color:#66d9ef">$(&lt;/span>seq &lt;span style="color:#ae81ff">1&lt;/span> 60&lt;span style="color:#66d9ef">)&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">do&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> sleep &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$INTERVAL&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> device_obt &amp;gt; &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/token&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jq -e .id_token &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/token&amp;#34;&lt;/span> &amp;gt; /dev/null &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> apply
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> jq -e &lt;span style="color:#e6db74">&amp;#39;.error==&amp;#34;slow_down&amp;#34;&amp;#39;&lt;/span> &lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CACHE&lt;span style="color:#e6db74">/token&amp;#34;&lt;/span> &amp;gt; /dev/null &lt;span style="color:#f92672">&amp;amp;&amp;amp;&lt;/span> INTERVAL&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>&lt;span style="color:#66d9ef">$((&lt;/span>INTERVAL+5&lt;span style="color:#66d9ef">))&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#66d9ef">done&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>exit &lt;span style="color:#ae81ff">1&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Replace &lt;code>REALMANME&lt;/code> with the name of your realm. Don&amp;rsquo;t forget to make it executable.&lt;/p>
&lt;h1 id="kubectl-configuration">&lt;code>kubectl&lt;/code> Configuration
&lt;/h1>&lt;p>Finally we write a configuration file for &lt;code>kubectl&lt;/code> (normally at &lt;code>~/.kube/config&lt;/code>). This will look similar to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">apiVersion&lt;/span>: &lt;span style="color:#ae81ff">v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">clusters&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">cluster&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">certificate-authority-data&lt;/span>: &lt;span style="color:#ae81ff">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server&lt;/span>: &lt;span style="color:#ae81ff">https://kubernetes.default.svc.cluster.local:6443&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">contexts&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">context&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cluster&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">user&lt;/span>: &lt;span style="color:#ae81ff">oidc&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">current-context&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">kind&lt;/span>: &lt;span style="color:#ae81ff">Config&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">users&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">oidc&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="using-it">Using It
&lt;/h1>&lt;p>Now you can try the following&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>token.sh req
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If everything is working, you will see a message like:&lt;/p>
&lt;pre tabindex="0">&lt;code>Click on:
&amp;#34;https://idp.example.com/realms/REALMNAME/device?user_code=XXXX-XXXX&amp;#34;
&lt;/code>&lt;/pre>&lt;p>After clicking on the link and authenticating, your tokens will then be printed. You can then run K8S commands like &lt;code>kubectl auth whoami&lt;/code> which will produce output similar to the below:&lt;/p>
&lt;pre tabindex="0">&lt;code>ATTRIBUTE VALUE
Username oidc:xxxxxxs
UID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Groups [oidc:xxxxxxxxxxxxxx oidc:xxxxxxxxxxxxxxxxx oidc:xxxxxxxxxxxxxxxxxxxxx system:authenticated]
Extra: authentication.kubernetes.io/credential-id [JTI=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx]
&lt;/code>&lt;/pre>&lt;p>This shows that you&amp;rsquo;ve inherited a username from the Identity Provider (prefixed with &lt;code>oidc:&lt;/code>) and groups from the Identity Provider (prefixed with &lt;code>oidc:&lt;/code>). You will need to put RBAC in place to permit your user to do things. I won&amp;rsquo;t go over that here.&lt;/p></description></item><item><title>Using OIDC Authentication with Kubernetes</title><link>https://blog.praveen.org.uk/posts/kubernetes-oidc/</link><pubDate>Sun, 08 Mar 2026 01:32:37 +0000</pubDate><guid>https://blog.praveen.org.uk/posts/kubernetes-oidc/</guid><description>&lt;h1 id="intro">Intro
&lt;/h1>&lt;p>This is a quick post to go over how you can configure Kubernetes to authenticate clients with OpenID Connect authentication. This has some advantages over alternatives like using X509 Client Certificate verification like:&lt;/p>
&lt;ul>
&lt;li>Revoking old or leaked credentials is easier; just update the client&amp;rsquo;s client secret&lt;/li>
&lt;li>Clients can be centrally managed at your Identity Provider including by providing groups for use in RBAC&lt;/li>
&lt;li>You don&amp;rsquo;t need to modify the CA authority that&amp;rsquo;s probably already been set up by your Kubernetes distribution&lt;/li>
&lt;/ul>
&lt;p>In this post, we&amp;rsquo;ll do this without using &amp;rsquo;non-standard&amp;rsquo; tools. We&amp;rsquo;ll do this by:&lt;/p>
&lt;ul>
&lt;li>Configuring the Kubernetes API Server to communicate with the Identity Provider&lt;/li>
&lt;li>Setting up a client with the Identity Provider and storing the client secret locally&lt;/li>
&lt;li>Writing a short script to obtain an ID Token from the Identity Provider&lt;/li>
&lt;li>Configuring &lt;code>kubectl&lt;/code> to call the script to obtain the ID Token&lt;/li>
&lt;/ul>
&lt;p>All we&amp;rsquo;ll need for this is Kubernetes (tested on RKE2 v1.35.1), &lt;code>kubectl&lt;/code> (tested with 1.34.1), &lt;code>curl&lt;/code>, &lt;code>jq&lt;/code>, and an OpenID Connect compliant Identity Provider. This post will be tailored towards Keycloak (v26.5.4) but this should work for any Identity Provider.&lt;/p>
&lt;p>Note that if you&amp;rsquo;re willing to use &lt;a class="link" href="https://github.com/int128/kubelogin" target="_blank" rel="noopener"
>kubelogin&lt;/a> as a &lt;code>kubectl&lt;/code> plugin, you would not need to store the client secret locally; instead you would authenticate directly with your Identity Provider in a standard browser flow. You wouldn&amp;rsquo;t need to write a script to obtain an ID Token either.&lt;/p>
&lt;h1 id="kubernetes-api-server">Kubernetes API Server
&lt;/h1>&lt;p>The relevant documentation for this is: &lt;a class="link" href="https://kubernetes.io/docs/reference/access-authn-authz/authentication/" target="_blank" rel="noopener"
>Authentication&lt;/a>. I will assume that the StructuredAuthenticationConfiguration Kubernetes feature gate is enabled. This has been the default &lt;a class="link" href="https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/" target="_blank" rel="noopener"
>since Kubernetes v1.30&lt;/a>.&lt;/p>
&lt;p>Write an &lt;code>AuthenticationConfiguration&lt;/code> like the following&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">apiVersion&lt;/span>: &lt;span style="color:#ae81ff">apiserver.config.k8s.io/v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">kind&lt;/span>: &lt;span style="color:#ae81ff">AuthenticationConfiguration&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">jwt&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">issuer&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">url&lt;/span>: &lt;span style="color:#ae81ff">https://idp.example.com&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">audiences&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#ae81ff">k8s&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">audienceMatchPolicy&lt;/span>: &lt;span style="color:#ae81ff">MatchAny&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claimMappings&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">username&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claim&lt;/span>: &lt;span style="color:#ae81ff">sub&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">prefix&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;oidc:&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">groups&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claim&lt;/span>: &lt;span style="color:#ae81ff">groups&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">prefix&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;oidc:&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">uid&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">claim&lt;/span>: &lt;span style="color:#ae81ff">sub&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">userValidationRules&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">expression&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;!user.username.startsWith(&amp;#39;system:&amp;#39;)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">message: &amp;#39;username cannot used reserved system&lt;/span>: &lt;span style="color:#ae81ff">prefix&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> - &lt;span style="color:#f92672">expression&lt;/span>: &lt;span style="color:#e6db74">&amp;#34;user.groups.all(group, !group.startsWith(&amp;#39;system:&amp;#39;))&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">message: &amp;#39;groups cannot used reserved system&lt;/span>: &lt;span style="color:#ae81ff">prefix&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Here make sure that:&lt;/p>
&lt;ul>
&lt;li>The OpenID Connect &lt;code>.well-known/openid-configuration&lt;/code> endpoint is immediately above the issuer URL (i.e. at &lt;code>https://idp.example.com/.well-known/openid-configuration&lt;/code>). In Keycloak&amp;rsquo;s case, I have a realm called &amp;lsquo;machine&amp;rsquo; so my well-known would be at: &lt;code>https://idp.example.com/realms/machine/.well-known/openid-configuration&lt;/code>. That means that my issuer URL should be: &lt;code>https://idp.example.com/realms/machine&lt;/code>.&lt;/li>
&lt;li>Your Identity Provider is able to issue client ID Tokens with an audience of &lt;code>k8s&lt;/code> or change it to if different audience if needed.&lt;/li>
&lt;li>Your Identity Provider is able to provide a &lt;code>groups&lt;/code> claim with your ID Token for use as Kubernetes groups. This may vary by Identity Provider.&lt;/li>
&lt;/ul>
&lt;p>Write the &lt;code>AuthenticationConfiguration&lt;/code> to a file somehwere, say &lt;code>/path/to/authconf.yaml&lt;/code> and start the Kubernetes API Server referring with: &lt;code>--authentication-config=/path/to/authconf.yaml&lt;/code>&lt;/p>
&lt;h1 id="client">Client
&lt;/h1>&lt;p>Note that here we are using a client and not a regular user account on the Identity Provider. This better represents the authentication flow of programmatic authentication (AKA M2M) instead of a human entering a username and password. &lt;a class="link" href="https://github.com/int128/kubelogin" target="_blank" rel="noopener"
>kubelogin&lt;/a> would be a better choice if you wanted to authenticate actual user accounts.&lt;/p>
&lt;p>Create a client on your Identity Provider. &amp;lsquo;Client authentication&amp;rsquo; or &amp;lsquo;confidential access&amp;rsquo; should be turned on. Additionally &amp;lsquo;Service account roles&amp;rsquo; or &amp;lsquo;Client Credentials Grant&amp;rsquo; should be turned on. Finally, set up the clients scopes so that issued ID Tokens contain a &lt;code>groups&lt;/code> claim, a &lt;code>sub&lt;/code> claim and an &lt;code>aud&lt;/code> (audience) claim with the issuer audiences value from the &lt;code>AuthenticationConfiguration&lt;/code> above.&lt;/p>
&lt;p>You can do this on Keycloak by assosciating the client with the pre-existing &lt;code>microprofile-jwt&lt;/code> scope (providing &lt;code>groups&lt;/code> through client service account roles), &lt;code>basic&lt;/code> scope, and by creating a custom client scope (named, say, &lt;code>k8s&lt;/code>), configuring a new mapper with the &amp;ldquo;Audience&amp;rdquo; name and including a custom audience with, in the above example, the value &lt;code>k8s&lt;/code>. Assign the &lt;code>k8s&lt;/code> client scope to the client you created.&lt;/p>
&lt;p>Save the client id and secret for the client; we&amp;rsquo;ll use them in the next section. I will assume they are respectively &lt;code>$CLIENT_ID&lt;/code> and &lt;code>$CLIENT_SECRET&lt;/code>. Also note the claims your client needs to get the &lt;code>groups&lt;/code> claim and the correct audience. This should also include the standard &lt;code>openid&lt;/code> scope. That means that for Keycloak, we have &lt;code>SCOPES=openid microprofile-jwt k8s&lt;/code>&lt;/p>
&lt;h1 id="token-fetch-script">Token Fetch Script
&lt;/h1>&lt;p>The script for this is quite simple. First, go to the Well Known endpoint of your Identity Provider (&lt;a class="link" href="https://idp.example.com/.well-known/openid-configuration" target="_blank" rel="noopener"
>https://idp.example.com/.well-known/openid-configuration&lt;/a>) and find the value for the &lt;code>token_endpoint&lt;/code> key. I will assume that it is &lt;code>https://idp.example.com/protocol/openid-connect/token&lt;/code>. Now we can write the script:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">#!/bin/bash
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#75715e">&lt;/span>curl -s -d grant_type&lt;span style="color:#f92672">=&lt;/span>client_credentials -d client_id&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CLIENT_ID&lt;span style="color:#e6db74">&amp;#34;&lt;/span> -d client_secret&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$CLIENT_SECRET&lt;span style="color:#e6db74">&amp;#34;&lt;/span> -d scope&lt;span style="color:#f92672">=&lt;/span>&lt;span style="color:#e6db74">&amp;#34;&lt;/span>$SCOPES&lt;span style="color:#e6db74">&amp;#34;&lt;/span> https://idp.example.com/protocol/openid-connect/token |&lt;span style="color:#ae81ff">\
&lt;/span>&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#ae81ff">&lt;/span>jq &lt;span style="color:#e6db74">&amp;#39;{&amp;#34;status&amp;#34;: {&amp;#34;token&amp;#34;: .id_token, &amp;#34;expirationTimestamp&amp;#34;: (now + .expires_in) | todate}, &amp;#34;apiVersion&amp;#34;: &amp;#34;client.authentication.k8s.io/v1&amp;#34;, &amp;#34;kind&amp;#34;: &amp;#34;ExecCredential&amp;#34;}&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>I will assume this script is at &lt;code>/path/to/token.sh&lt;/code>. Don&amp;rsquo;t forget to make it executable.&lt;/p>
&lt;p>This script uses a generated ID token to authenticate the Kubernetes API. It it isn&amp;rsquo;t required by the specification for the Identity Provider (&lt;a class="link" href="https://datatracker.ietf.org/doc/html/rfc6749#section-4.4" target="_blank" rel="noopener"
>https://datatracker.ietf.org/doc/html/rfc6749#section-4.4&lt;/a>) to return an ID token for a Client Credentials flow. KeyCloak does provide an ID token for the above request. If your Identity Provider provides a JWT access token with the correct claims and audience, you can use that instead.&lt;/p>
&lt;p>All &lt;code>jq&lt;/code> does here is format the ID Token into an &lt;a class="link" href="https://kubernetes.io/docs/reference/config-api/client-authentication.v1/#client-authentication-k8s-io-v1-ExecCredential" target="_blank" rel="noopener"
>ExecCredential&lt;/a> object. Using it is nice because it prevents any malicious or accidental JSON syntax breakage but you could remove it if you wanted to. Also, here, we&amp;rsquo;re writing the client secret to a file. You might prefer to use a different approach to store or cache it (e.g. GPG).&lt;/p>
&lt;h1 id="kubectl-configuration">&lt;code>kubectl&lt;/code> Configuration
&lt;/h1>&lt;p>Finally we write a configuration file for &lt;code>kubectl&lt;/code> (normally at &lt;code>~/.kube/config&lt;/code>). This will look similar to:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-yaml" data-lang="yaml">&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">apiVersion&lt;/span>: &lt;span style="color:#ae81ff">v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">clusters&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">cluster&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">certificate-authority-data&lt;/span>: &lt;span style="color:#ae81ff">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">server&lt;/span>: &lt;span style="color:#ae81ff">https://kubernetes.default.svc.cluster.local:6443&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">contexts&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">context&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">cluster&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">user&lt;/span>: &lt;span style="color:#ae81ff">oidc&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">current-context&lt;/span>: &lt;span style="color:#ae81ff">default&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">kind&lt;/span>: &lt;span style="color:#ae81ff">Config&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>&lt;span style="color:#f92672">users&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span>- &lt;span style="color:#f92672">name&lt;/span>: &lt;span style="color:#ae81ff">oidc&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">user&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">exec&lt;/span>:
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">apiVersion&lt;/span>: &lt;span style="color:#ae81ff">client.authentication.k8s.io/v1&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">command&lt;/span>: &lt;span style="color:#ae81ff">/path/to/token.sh&lt;/span>
&lt;/span>&lt;/span>&lt;span style="display:flex;">&lt;span> &lt;span style="color:#f92672">interactiveMode&lt;/span>: &lt;span style="color:#ae81ff">Never&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h1 id="using-it">Using It
&lt;/h1>&lt;p>Now you can try the following&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">&lt;code class="language-bash" data-lang="bash">&lt;span style="display:flex;">&lt;span>kubectl auth whoami
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>If everything is working, you will see output like:&lt;/p>
&lt;pre tabindex="0">&lt;code>ATTRIBUTE VALUE
Username oidc:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
UID xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Groups [oidc:xxxxxxxxxxxxxx oidc:xxxxxxxxxxxxxxxxx oidc:xxxxxxxxxxxxxxxxxxxxx system:authenticated]
Extra: authentication.kubernetes.io/credential-id [JTI=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx]
&lt;/code>&lt;/pre>&lt;p>This shows that you&amp;rsquo;ve inherited a username from the Identity Provider (prefixed with &lt;code>oidc:&lt;/code>) and groups from the Identity Provider (prefixed with &lt;code>oidc:&lt;/code>). You will need to put RBAC in place to permit your user to do things. I won&amp;rsquo;t go over that here.&lt;/p></description></item></channel></rss>