Sécuriser l’accès à vos clusters Kubernetes en utilisant Keycloak comme OIDC provider

Rédigé par Nathanaël Hannebert le 26/02/2024
Temps de lecture: 10 minutes

Lorsque de nombreuses personnes sont autorisées à se connecter à un cluster Kubernetes, il devient nécessaire de bien suivre qui est autorisé à s’y connecter et avec quels niveaux d’autorisation. Avoir une gestion fine des accès et des autorisations est un véritable défi ! Dans un contexte de production, l’usage des fichiers kubeconfig est un chemin semé d’embûches, par exemple :

  • Certains fichiers kubeconfig ont des autorisations complètes d’administration,
  • Les tokens ou certificats ont parfois une durée de validité longue ou illimitée,
  • Un fichier kubeconfig peut suffire à s’authentifier sur un cluster Kubernetes,
  • Parfois, plusieurs utilisateurs partagent un même fichier kubeconfig, ce qui rend compliquée (voire impossible) la revue des accès.

Une solution commune en entreprise est d’utiliser un portail SSO (Single Sign On, authentification unique), une solution permettant aux utilisateurs et utilisatrices de ne se connecter qu’une seule fois pour accéder à toutes les applications de l’entreprise.

Bien que peu utilisée, la connexion par SSO à un cluster Kubernetes est possible et est même recommandée. Celle-ci permet d’attribuer des droits différents selon quel utilisateur se connecte. Voyons cela ensemble !

1. Déployer et configurer le SSO avec Keycloak

Dans le cadre de cet article, nous utiliserons un cluster Kubernetes sur lequel est déjà déployé cert-manager et un serveur Keycloak, accessible par un nom de domaine avec un certificat TLS valide. L’objectif sera de montrer comment s’authentifier via Keycloak sur un autre cluster Kubernetes, que nous installerons en local avec kind.

Nous utiliserons deux utilisateurs différents, associés à des groupes différents dans Keycloak, et à qui nous donnerons des droits d’accès différents sur le cluster Kubernetes. À l’un nous accorderons des droits d’administrateur et à l’autre des droits de lecture-seule.

Keycloak pourrait être remplacé par d’autres solutions de SSO telles qu’Okta, Dex, Azure Active Directory, LemonLDAP::NG., qui peuvent être déployées de différentes manières, pas nécessairement sur Kubernetes.

a. Déploiement de Keycloak

Voici un exemple de déploiement de Keycloak pour illustrer cet article. Nous utiliserons un cluster Kubernetes accessible publiquement sur internet, et cert-manager pour obtenir un certificat valide automatiquement avec Let’s Encrypt :

Créons tout d’abord un fichier values.yaml comme ceci (pensez à remplacer le hostname par le vôtre, et le nom du cluster-issuer à utiliser dans votre contexte) :

 1# fichier values.yaml
 2  ingress:
 3	enabled: true
 4	ingressClassName: nginx
 5	tls: true
 6	hostname: keycloak.myhostname.example
 7	annotations:
 8  	cert-manager.io/cluster-issuer: letsencrypt-prod
 9  production: true
10  proxy: edge

Installons Keycloak :

1helm install keycloak oci://registry-1.docker.io/bitnamicharts/keycloak -f values.yaml

Pensez à mettre à jour l’enregistrement DNS avec l’adresse IP attribuée au service LoadBalancer de votre cluster, afin que cert-manager puisse obtenir un certificat TLS valide auprès de Let’s Encrypt.

b. Utilisateurs et groupes

Accédons à notre serveur Keycloak. Nous devons d’abord créer deux utilisateurs kube-admin et kube-readonly et nous les mettons respectivement dans deux groupes distincts, kube-admin-group et kube-readonly-group.

c. Client OIDC

Ensuite, nous ajoutons un nouveau client de type Open ID Connect, que nous nommons kubernetes. Nous définissons les paramètres suivants :

  • Valid redirect URLs :
    • http://localhost:18000
    • http://localhost:8000
  • Web origins :
    • +
  • Client authentication : off
  • Authorization : off
  • Direct access grants : disabled

Ensuite, dans l’onglet Client Scope, modifions le Assigned Client Scope kubernetes-dedicated en ajoutant un mapper By Configuration de type User Client Role. Voici la configuration nécessaire :

  • Name : roles
  • Client ID : kubernetes #C’est le nom du client créé précédemment
  • Client Role prefix : #Laisser vide
  • Multivalued : enabled
  • Token Claim Name : roles #Cette valeur devrait être ajoutée à la configuration de l’API Server
  • Claim JSON Type : String
  • Add to ID token : enabled
  • Add to access token : disabled
  • Add to userinfo : disabled
  • Add to token introspection : disabled

d. Rôles

Maintenant, nous devons créer les rôles nécessaires, allons dans Clients, sélectionnons kubernetes (le client que nous venons de créer), allons dans l’onglet Roles et créons-en deux avec ces noms-là :

  • kube-admin-role
  • kube-readonly-role

Enfin, il faut que nous associions ces rôles avec les groupes. Allons dans Groups, sélectionnons kube-admin-group créé précédemment, allons dans l’onglet Role mapping, cliquons sur Assign role. Dans la fenêtre qui s’ouvre, basculons sur Filter by clients et sélectionnons le rôle kube-admin-role. Répétons la même opération pour associer le groupe kube-readonly-group avec le rôle kube-readonly-role.

2. Configurer l’API server

L’API Server doit être configuré pour utiliser notre SSO. Vous trouverez dans la documentation officielle tous les paramètres disponibles pour cela : https://kubernetes.io/docs/reference/access-authn-authz/authentication/#configuring-the-api-server

Pour illustrer cet article, nous utilisons un cluster local basé sur kind (qui utilise lui-même kubeadm) et nous le créons en appliquant ce patch. Notez que les valeurs indiquées ci-dessous pour les paramètres oidc-* doivent correspondre à celles définies plus haut.

NB : La valeur pour oidc-issuer-url peut être récupérée dans Keycloak, en allant dans Realm settings, Endpoints, OpenID Endpoint Configuration et en copiant le lien issuer.

 1# Contenu du fichier ./kind-patch.yaml
 2kind: Cluster
 3apiVersion: kind.x-k8s.io/v1alpha4
 4nodes:
 5- role: control-plane
 6 kubeadmConfigPatches:
 7 - |
 8   kind: ClusterConfiguration
 9   apiServer:
10       extraArgs:
11         oidc-issuer-url: https://keycloak.myhostname.example/realms/cockpitio
12         oidc-client-id: kubernetes
13         oidc-username-claim: sub
14         oidc-username-prefix: "-"
15         oidc-groups-claim: roles
16         oidc-groups-prefix: "oidc:"   

Créons le cluster Kubernetes avec kind :

 1❯ kind create cluster --name poc-oidc-kubernetes  --config ./kind-patch.yaml
 2
 3Creating cluster "poc-oidc-kubernetes" ...
 4 ✓ Ensuring node image (kindest/node:v1.27.3) 🖼
 5 ✓ Preparing nodes 📦  
 6 ✓ Writing configuration 📜
 7 ✓ Starting control-plane 🕹️
 8 ✓ Installing CNI 🔌
 9 ✓ Installing StorageClass 💾
10Set kubectl context to "kind-poc-oidc-kubernetes"
11You can now use your cluster with:
12kubectl cluster-info --context kind-poc-oidc-kubernetes

Nous avons donc indiqué à l’API server de notre cluster Kubernetes quel est notre identity provider et comment “dialoguer” avec celui-ci.

3. ClusterRolesBindings

Nous disions dans l’introduction que nous pouvions attribuer des droits différents selon l’utilisateur qui se connecte, via OIDC. D’après la documentation de Kubernetes sur l’authentification, nous pouvons faire correspondre des roles ou des clusterroles à des utilisateurs ou à des groupes. En utilisant une authentification OIDC, l’API server reçoit pour chaque utilisateur authentifié plusieurs informations, parmi lesquelles il y a l’identifiant unique de l’utilisateur, définit dans notre exemple sur le claim sub et les groupes, définis sur le claim roles, et préfixés par oidc:

Pour notre exemple, associons le clusterRole cluster-admin à utilisateurs qui ont dans le rôle kube-admin-role dans Keycloak ; et le clusterRole view pour ceux qui ont le rôle kube-readonly-role :

1❯ kubectl create clusterrolebinding oidc-cluster-admin --clusterrole=cluster-admin --group="oidc:kube-admin-role"
2
3clusterrolebinding.rbac.authorization.k8s.io/oidc-cluster-admin created
4
5
6❯ kubectl create clusterrolebinding oidc-cluster-view --clusterrole=view --group="oidc:kube-read-role"
7
8clusterrolebinding.rbac.authorization.k8s.io/oidc-cluster-view created

⚠️ Nous devons ajouter le préfixe oidc: au nom du groupe, conformément au paramètre oidc-groups-prefix: "oidc:" de l’API Server.

4. Kubectl avec kubelogin ⚙️

La documentation de kubectl nous indique que nous pouvons utiliser OIDC pour nous authentifier auprès de notre cluster kubectl, mais que cela n’est pas très pratique car il n’y a pas de moyen automatique d’obtenir le token nécessaire, et que ce dernier devant avoir une durée de vie courte (de l’ordre de quelques minutes) il faut le renouveler fréquemment, ce qui peut être fastidieux. En voici un extrait :

  1. Kubernetes has no “web interface” to trigger the authentication process. There is no browser or interface to collect credentials which is why you need to authenticate to your identity provider first.
  2. The id_token can’t be revoked, it’s like a certificate so it should be short-lived (only a few minutes) so it can be very annoying to have to get a new token every few minutes.

Pour fluidifier tout ce processus, il existe le module kubelogin. Celui-ci est notamment capable de lancer le processus d’authentification auprès du SSO dans le navigateur par défaut, puis de récupérer le token grâce à un serveur web intégré, qui écoute en local sur les ports 8000 ou 18000 (par défaut). C’est pour cela que plus haut, dans la configuration du client OIDC nous avons indiqué des URL pointant vers localhost.

a. Installation

Installation du module kubectl kubelogin avec krew : https://github.com/int128/kubelogin

kubectl krew install oidc-login

Voyez le dépôt pour d’autres façons de l’installer, avec homebrew ou chocolatey par exemple.

b. Vérifier l’authentification

1❯ kubectl oidc-login setup --oidc-issuer-url=https://keycloak.myhostname.example/realms/cockpitio --oidc-client-id=kubernetes --oidc-use-pkce
2
3authentication in progress...

keycloak_login_page
Connexion via keycloak

keycloak_authenticated
L'utilisateur est bien authentifié

Nous obtenons une réponse avec tous les claims de notre utilisateur :

 1{
 2  "exp": 1706000000,
 3  "iat": 1706000000,
 4  "auth_time": 1700000000,
 5  "jti": "7bdaf221-ad53-454e-8669-0ddabbbbbbbb",
 6  "iss": "https://keycloak.myhostname.example/realms/cockpitio",
 7  "aud": "kubernetes",
 8  "sub": "399cb1d6-e099-4228-942d-74703bxxxxxx",
 9  "typ": "ID",
10  "azp": "kubernetes",
11  "nonce": "KZg8X1J9nBmQ4Pj7T_HSJ0IxxxxxxzQICj2CUMiSD-s",
12  "session_state": "59f67957-zzz-aaa-bbb-407969497b8c",
13  "at_hash": "RP4d9OXM5tzzzzzzzzzzzz",
14  "acr": "1",
15  "sid": "59f67957-xxxx-yyyy-zzz-407969497b8c",
16  "email_verified": false,
17  "roles": [
18	"kube-admin-role"
19  ],
20  "preferred_username": "kube-admin"
21}

Nous poursuivons en ajoutant un utilisateur à notre fichier kubeconfig…

 1❯ kubectl config set-credentials oidc \
 2  --exec-api-version=client.authentication.k8s.io/v1beta1 \
 3  --exec-command=kubectl \
 4  --exec-arg=oidc-login \
 5  --exec-arg=get-token \
 6  --exec-arg=--oidc-issuer-url=https://keycloak.myhostname.example/realms/cockpitio \
 7  --exec-arg=--oidc-client-id=kubernetes \
 8  --exec-arg=--oidc-use-pkce
 9
10User "oidc" set.

… et en l’utilisant une première fois :

1❯ kubectl --user=oidc get nodes
2
3NAME                                STATUS   ROLES           AGE   VERSION
4poc-oidc-kubernetes-control-plane   Ready    control-plane   32m   v1.27.3

La connexion à notre cluster Kubernetes via OIDC fonctionne, nous pouvons donc mettre à jour notre contexte :

1❯ kubectl config set-context --current --user=oidc
2
3Context "kind-poc-oidc-kubernetes" modified.

Dans cet exemple l’utilisateur qui s’est connecté via OIDC est kube-admin, lors de la connexion le SSO a fourni un token qui a été passé à l’API Server qui a analysé le claim "roles": ["kube-admin-role" ] et l’a associé au clusterRoleBinding oidc-cluster-admin. Si nous nous étions connectés avec l’utilisateur kube-readonly le claim roles aurait contenu la valeur ["kube-read-role"] donc le clusterRoleBinding associé aurait été oidc-cluster-view et l’utilisateur aurait eu beaucoup moins de privilèges sur notre cluster (ceux du clusterRole view). C’est ainsi que nous pouvons finement gérer les droits associés à chaque utilisateur, groupe ou rôle dans le cadre de l’utilisation d’OIDC pour se connecter à Kubernetes.

c. Fichier kubeconfig

Voici le fichier kubeconfig correspondant à notre installation :

 1apiVersion: v1
 2clusters:
 3- cluster:
 4   certificate-authority-data: [..] #redacted
 5   server: https://127.0.0.1:40753
 6 name: kind-poc-oidc-kubernetes
 7contexts:
 8- context:
 9   cluster: kind-poc-oidc-kubernetes
10   user: oidc
11 name: kind-poc-oidc-kubernetes
12current-context: kind-poc-oidc-kubernetes
13kind: Config
14preferences: {}
15users:
16- name: oidc
17 user:
18   exec:
19     apiVersion: client.authentication.k8s.io/v1beta1
20     args:
21     - oidc-login
22     - get-token
23     - --oidc-issuer-url=https://keycloak.myhostname.example/realms/cockpitio
24     - --oidc-client-id=kubernetes
25     - --oidc-use-pkce
26     command: kubectl
27     env: null
28     interactiveMode: IfAvailable
29     provideClusterInfo: false

5. Usage quotidien en équipe

Pour le quotidien, il suffit de partager le fichier kubeconfig ci-dessus avec toute l’équipe, et de faire installer à chacun le module kubelogin pour kubectl. La première commande kubectl ouvrira le navigateur pour demander à l’utilisateur de se connecter au SSO, et seulement lorsque le token obtenu sera expiré, il faudra que l’utilisateur s’authentifie de nouveau. Cette durée d’expiration est un réglage propre à chaque SSO.

Au fil du temps, cette méthode permettra aux responsables des clusters Kubernetes de ne pas avoir de craintes de voir des fichiers kubeconfig “admin” échangés à tout-va, et de s’affranchir d’opérations manuelles lors de l’arrivée ou du départ d’une personne dans l’entreprise.

Comme dit précédemment, les utilisateurs connectés via OIDC pourront avoir des niveaux de privilèges différents sur le cluster, selon ce qui a été configuré pour leur propre compte utilisateur ou pour les groupes auxquels ils appartiennent.

6. Conclusion

Ainsi, en utilisant la méthode habituelle de l’entreprise (le SSO) pour l’authentification et la gestion des privilèges, nous diminuons les interventions manuelles et le risque d’erreur afférent, nous évitons le partage de moyens de connexion (fichiers kubeconfig) non nominatifs, et nous inscrivons l’accès au cluster Kubernetes dans les processus de sécurité (revue de comptes utilisateurs, revue des privilèges d’accès, gestion centralisée et éprouvée du cycle de vie des accès, possibilité d’ajouter une authentification à multiples facteurs). Enfin, nous diminuons le risque en cas de “vol” de fichiers kubeconfig, car seuls, ceux-ci ne suffisent plus pour se connecter aux clusters Kubernetes, il faut aussi disposer de moyens de connexion au SSO de l’entreprise.

En conclusion, nous recommandons à toute entité disposant d’un SSO de l’utiliser pour gérer les accès à ses clusters Kubernetes.