Intro
This page is an alternative/extension of my previous post which describes how to use the Device Authorization Grant with Kubernetes & Keycloak. With this approach the usage flow is as follows:
- The user runs a
token.shscript - The script prints a link to the Identity Provider for the user to click
- The user clicks the link and permits information to be passed to the scripts
- The script sets the identity token and refresh token in
kubectl’s configuration - Future invocations of
kubectlrenew the ID Token with the Refresh Token if possible
This has been tested with RKE2 v1.35.1, kubectl 1.34.1, curl, jq, an Keycloak (v26.5.4) but this should work for any standard compliant Identity Provider.
Kubernetes API Server
Setting up the API Server is the same as before
The relevant documentation for this is: Authentication. I will assume that the StructuredAuthenticationConfiguration Kubernetes feature gate is enabled. This has been the default since Kubernetes v1.30.
Write an AuthenticationConfiguration like the following
apiVersion: apiserver.config.k8s.io/v1
kind: AuthenticationConfiguration
jwt:
- issuer:
url: https://idp.example.com
audiences:
- k8s
audienceMatchPolicy: MatchAny
claimMappings:
username:
claim: upn
prefix: "oidc:"
groups:
claim: groups
prefix: "oidc:"
uid:
claim: sub
userValidationRules:
- expression: "!user.username.startsWith('system:')"
message: 'username cannot used reserved system: prefix'
- expression: "user.groups.all(group, !group.startsWith('system:'))"
message: 'groups cannot used reserved system: prefix'
Here make sure that:
- The OpenID Connect
.well-known/openid-configurationendpoint is immediately above the issuer URL (i.e. athttps://idp.example.com/.well-known/openid-configuration). In Keycloak’s case, I have a realm called ‘machine’ so my well-known would be at:https://idp.example.com/realms/machine/.well-known/openid-configuration. That means that my issuer URL should be:https://idp.example.com/realms/machine. - Your Identity Provider is able to issue client ID Tokens with an audience of
k8sor change it to if different audience if needed. - Your Identity Provider is able to provide a
groupsclaim with your ID Token for use as Kubernetes groups. This may vary by Identity Provider.
Write the AuthenticationConfiguration to a file somehwere, say /path/to/authconf.yaml and start the Kubernetes API Server referring to it with: --authentication-config=/path/to/authconf.yaml
Client
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 audiences items in the configuration above – I will assume that it is k8s. Keycloak will insert this as the aud claim. Finally, set up the clients scopes so that issued ID Tokens contains a sub claim, upn claim, and a groups claim. On Keycloak, this can be done by assigning the predefined basic and microprofile-jwt scopes.
Token Fetch Script
A rough script to facilitate the device authorization flow is below:
#!/bin/bash
CACHE=/run/user/$(id -u)/k8s-oauth-cache
mkdir -p "$CACHE"
device_req() {
curl -s -d client_id=k8s -d scope="openid basic microprofile-jwt" https://idp.example.com/realms/REALMNAME/protocol/openid-connect/auth/device
}
device_obt() {
jq -r .device_code "$CACHE/device" | awk '{print "device_code="$1}' | curl -s -d grant_type="urn:ietf:params:oauth:grant-type:device_code" -d client_id=k8s --data @- https://idp.example.com/realms/REALMNAME/protocol/openid-connect/token
}
apply() {
kubectl config set-credentials oidc --auth-provider oidc --auth-provider-arg=idp-issuer-url=https://idp.example.com/realms/REALMNAME --auth-provider-arg=client-id=k8s --auth-provider-arg=refresh-token="$(jq -r .refresh_token "$CACHE/token")" --auth-provider-arg=id-token="$(jq -r .id_token "$CACHE/token")"
exit "$?"
}
device_req > "$CACHE/device"
INTERVAL="$(jq -r .interval "$CACHE/device")"
echo "Click on:" > /dev/stderr
jq .verification_uri_complete "$CACHE/device" > /dev/stderr
for i in $(seq 1 60)
do
sleep "$INTERVAL"
device_obt > "$CACHE/token"
jq -e .id_token "$CACHE/token" > /dev/null && apply
jq -e '.error=="slow_down"' "$CACHE/token" > /dev/null && INTERVAL="$((INTERVAL+5))"
done
exit 1
Replace REALMANME with the name of your realm. Don’t forget to make it executable.
kubectl Configuration
Finally we write a configuration file for kubectl (normally at ~/.kube/config). This will look similar to:
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
server: https://kubernetes.default.svc.cluster.local:6443
name: default
contexts:
- context:
cluster: default
user: oidc
name: default
current-context: default
kind: Config
users:
- name: oidc
Using It
Now you can try the following
token.sh req
If everything is working, you will see a message like:
Click on:
"https://idp.example.com/realms/REALMNAME/device?user_code=XXXX-XXXX"
After clicking on the link and authenticating, your tokens will then be printed. You can then run K8S commands like kubectl auth whoami which will produce output similar to the below:
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]
This shows that you’ve inherited a username from the Identity Provider (prefixed with oidc:) and groups from the Identity Provider (prefixed with oidc:). You will need to put RBAC in place to permit your user to do things. I won’t go over that here.