Cette page détaille la mise en place d’un démonstrateur pour authentifier des utilisateurs sur un cluster Kubernetes en utilisant le protocole Open ID Connect.

Open ID Connect est l’un des mécanismes permettant à un utilisateur d’être reconnu par Kubernetes. Il s’agit d’une couche d’identification basée sur le protocole OAuth 2.0, qui autorise les clients à vérifier l’identité d’un utilisateur final en se basant sur l’authentification fournie par un serveur d’autorisation.

Note : cet article s’inspire largement du projet
https://github.com/etiennedi/keycloak-nginx-https-self

Pré-requis nécessaires sur le poste local

Schéma d’architecture

schéma d’architecture

Pour plus de simplicité l’ensemble des éléments sera déployé localement dans des conteneurs docker :

  • Keycloak : le fournisseur d’identité
  • Nginx : pour servir le certificat TLS du fournisseur d’identité
  • Kind : pour créer facilement un cluster Kubernetes

Keycloak sera le fournisseur d’identité et délivrera les jetons (tokens) permettant l’identification des utilisateurs. Étant donné que toutes les informations nécessaires pour valider un utilisateur se trouvent dans le token (id_token), le cluster Kubernetes n’a pas besoin de contacter directement le fournisseur d’identité.

A noter que l’APIserver de Kubernetes n’accepte que des tokens Open ID Connect délivrés avec HTTPS.

Nous allons donc dans un premier temps devoir créer une autorité de certification avec un certficat racine et un certificat TLS pour l’accès à notre fournisseur de tokens Open ID Connect : Keycloak.

Le certificat racine sera présenté à Kubernetes de façon à ce que ce dernier puisse faire confiance à Keycloak.

Installation

L’ensemble des fichiers et scripts nécessaires à l’installation est disponible sur le dépôt git du blog.

1. Création de l’autorité de certification

Première étape, créons notre autorité de certification, en lançant le script create_ca.sh :

$ ./create_ca.sh 
Generating RSA private key, 4096 bit long modulus (2 primes)
.....++++
.........................................++++
e is 65537 (0x010001)
You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:FR
State or Province Name (full name) [Some-State]:
Locality Name (eg, city) []:
Organization Name (eg, company) [Internet Widgits Pty Ltd]:Xian Labs        
Organizational Unit Name (eg, section) []:
Common Name (e.g. server FQDN or YOUR name) []:xian.example.com
Email Address []:xian@example.com

2. Création du certificat pour Keycloak

Créer tout d’abord un fichier nommé alt_names.txt qui contient à minima l’adresse IP ou le fqdn de la machine locale.

Exemple de contenu :

subjectAltName=DNS:example.com,DNS:www.example.com, IP:192.168.1.47

Ici l’adresse 192.168.1.47 correspond à l’IP de ma machine locale.

Lancer le script issue_cert.sh :

$ ./issue_cert.sh 
Generating a RSA private key
......................................................................++++
........................................................................................................................................................................................++++
writing new private key to 'cert.key'
-----
Signature ok
subject=C = FR, ST = Bretagne, L = Rennes, O = XianLabs, CN = www.example.com
Getting CA Private Key

Vérification du contenu du certificat généré :

$ openssl x509 -in cert.crt -text -noout

Le certificat sera servi par le conteneur nginx.

3. Construction de l’image nginx

Construire l’image docker nginx à partir du Dockerfile présent dans le dépôt :

$ docker build -t nginx-keycloak:1 .
Sending build context to Docker daemon  137.7kB
Step 1/4 : FROM nginx:1.23.1-alpine
1.23.1-alpine: Pulling from library/nginx
Digest: sha256:b87c350e6c69e0dc7069093dcda226c4430f3836682af4f649f2af9e9b5f1c74
Status: Downloaded newer image for nginx:1.23.1-alpine
 ---> 568998804441
Step 2/4 : COPY default.conf /etc/nginx/conf.d/default.conf
 ---> 971bdee1ef47
Step 3/4 : COPY cert.crt /etc/nginx/cert.crt
 ---> 23ded2a44340
Step 4/4 : COPY cert.key /etc/nginx/cert.key
 ---> edd190668785
Successfully built edd190668785
Successfully tagged nginx-keycloak:1

4. Démarrage du fournisseur d’identité

Démarrer la stack docker compose :

$ docker-compose up -d
Creating network "kubernetes-openid-connect_default" with the default driver
Creating kubernetes-openid-connect_keycloak_1 ... done
Creating kubernetes-openid-connect_nginx_1    ... done

Vérifier avec la commande docker ps que les conteneurs nginx et keycloak sont bien lancés.

5. Configurer Keycloak

Dans le navigateur web, ouvrir la page https://192.168.1.47:8443/ (il s’agit de l’adresse IP de mon poste local, adresse référencée plus haut dans fichier nommé alt_names.txt).

Un message d’avertissement sur un risque probable de sécurité est affiché car notre autorité de certification est inconnue du navigateur web. Accepter le risque et poursuivre.

Cliquer sur Administration console, et se connecter avec l’utilisateur admin et le mot de passe pass.

Dans le menu de gauche, cliquer sur Clients et ajouter un nouveau client :

Créer un nouveau client

Dans le menu de gauche, cliquer sur Users et ajouter un nouvel utilisateur :

Créer un nouvel utilisateur

S’assurer que la case Email Verified est bien cochée.

Pour l’utilisateur tout juste créé, cliquer sur l’onglet Credentials et ajouter un mot de passe :

Créer un mot de passe pour le nouvel utilisateur

S’assurer que la case Temporary est bien décochée.

6. Démarrer le cluster Kubernetes

Nous utilisons l’outil kind pour générer facilement notre cluster Kubernetes dans un conteneur docker.

La configuration du cluster est déclarée dans le fichier kind-config.yaml :

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
name: open-id-connect
nodes:
- role: control-plane
  # Configure TLS certificate for keycloak
  extraMounts:
   - hostPath: /home/xian/workspace/kubernetes-openid-connect/rootCA.crt
     containerPath: /usr/share/ca-certificates/keycloak.crt
  kubeadmConfigPatches:
  # Configure Open ID Connect
  - |
    apiVersion: kubeadm.k8s.io/v1beta2
    kind: ClusterConfiguration
    apiServer:
      extraArgs:
        oidc-issuer-url: "https://192.168.1.47:8443/auth/realms/master"
        oidc-client-id: "kubernetes-cluster"
        oidc-username-claim: "email"
        oidc-ca-file: "/usr/share/ca-certificates/keycloak.crt"    
  # Install ingress controller  
  - |
    kind: InitConfiguration
    nodeRegistration:
      kubeletExtraArgs:
        node-labels: "ingress-ready=true"    
  # Configure ingress ports
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP

La partie concernant l’ingress controller est facultative.

Démarrer le cluster Kubernetes :

$ kind create cluster --config kind-config.yaml
Creating cluster "open-id-connect" ...
 ✓ Ensuring node image (kindest/node:v1.21.1) 🖼
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-open-id-connect"
You can now use your cluster with:

kubectl cluster-info --context kind-open-id-connect

Have a question, bug, or feature request? Let us know! https://kind.sigs.k8s.io/#community 🙂

Récupérer l’url d’écoute de l’API server :

$ kubectl cluster-info --context kind-open-id-connect
Kubernetes control plane is running at https://127.0.0.1:35081
CoreDNS is running at https://127.0.0.1:35081/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

En cas de problème, supprimer le cluster avec la commande :

$ kind delete cluster --name open-id-connect

6. Test de connection au cluster

La commande suivante permet de demander à Keycloak de générer un token temporaire :

$ curl -s -k -X POST https://192.168.1.47:8443/auth/realms/master/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=kubernetes-cluster \
  -d username=xian \
  -d password=pass \
  -d scope=openid \
  -d response_type=id_token \
  | jq -r .id_token

La commande doit afficher un token au format JWT.

A noter que l’URL du fournisseur d’identité est embarquée dans le token, et que celle-ci doit impérativement être indiquée avec le protocole HTTPS pour pouvoir être acceptée par l’API server de Kubernetes.

On peut voir le contenu détaillé d’un token JWT en allant sur le site https://jwt.io/, exemple avec le token tout juste récupéré :

contenu de l’id_token

L’URL indiquée dans le champ iss est en HTTPS, on est bon ;-)

Demandons un nouveau token et pour plus de facilté enregistrons le dans le fichier token :

$ curl -s -k -X POST https://192.168.1.47:8443/auth/realms/master/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=kubernetes-cluster \
  -d username=xian \
  -d password=pass \
  -d scope=openid \
  -d response_type=id_token \
  | jq -r .id_token > token

Ce token peut ensuite être utilisé pour faire une requête sur l’API Server de Kubernetes :

$ curl -H "Authorization: Bearer $(< token)" -k https://127.0.0.1:35081/api/v1/namespaces
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {
    
  },
  "status": "Failure",
  "message": "namespaces is forbidden: User \"xian@example.com\" cannot list resource \"namespaces\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
  "details": {
    "kind": "namespaces"
  },
  "code": 403
}

La requête est refusée, mais l’utilisateur xian@example.com est correctement identifié !

Il ne reste plus qu’à donner des droits d’accès à l’utilisateur. Ceci s’effectue en créant des règles RBAC sur le cluster. Pour l’exemple on va donner les pleins pouvoir à l’utilisateur xian@example.com :

$ kubectl create clusterrolebinding --clusterrole cluster-admin --user xian@example.com xian-admin
clusterrolebinding.rbac.authorization.k8s.io/xian-admin created

Tentons maintenant une nouvelle requête :

$ curl -H "Authorization: Bearer $(< token)" -k https://127.0.0.1:35081/api/v1/namespaces
{
  "kind": "NamespaceList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "3186"
  },
  "items": [
    {
      "metadata": {
        "name": "default",
        "uid": "df455d6b-4b03-46d2-bf44-0687e972cf8d",
        "resourceVersion": "199",
        "creationTimestamp": "2022-10-14T19:58:32Z",
        "labels": {
          "kubernetes.io/metadata.name": "default"
        },
...

Victoire ! La liste des namespaces Kubernetes est affichée !

7. Configurer Kubectl

Ca fonctionne avec curl, mais il serait plus pratique de pouvoir utiliser directement la CLI kubectl.

Plusieurs méthodes sont possibles pour faire cela.

Méthode avec –token

L’option --token permet de spécifier le token à utiliser :

kubectl –token=$(< token) get ns NAME STATUS AGE default Active 4d11h kube-node-lease Active 4d11h kube-public Active 4d11h kube-system Active 4d11h local-path-storage Active 4d11h

On peut automatiser la chose avec le script kubectl-oidc.sh ci-dessous :

#!/bin/bash

# Get the Open ID token
token=$(
curl -s -k -X POST https://192.168.1.47:8443/auth/realms/master/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=kubernetes-cluster \
  -d username=xian \
  -d password=pass \
  -d scope=openid \
  -d response_type=id_token \
  | jq -r .id_token
)

kubectl --token $token "$@"

Utilisation :

./kubectl-oidc.sh get ns
NAME                 STATUS   AGE
default              Active   4d11h
kube-node-lease      Active   4d11h
kube-public          Active   4d11h
kube-system          Active   4d11h
local-path-storage   Active   4d11h

Méthode en créant un nouvel utilisateur et un contexte kubeconfig

L’intérêt de cette méthode est que lorsque le token_id expire kubectl peut utiliser le refresh_token pour obtenir un nouveau token_id.

schéma d&rsquo;architecture

Pour pouvoir utiliser cette méthode il faut ajouter un peu de configuration additionnelle dans Keycloak.

Au niveau du client, sélectionner l’Access Type confidential :

kubectl-oidc

Indiquer les Valid Redirect URIs http://* et https://* :

kubectl-oidc

Et cliquer sur Save.

Aller ensuite sur l’onglet Credentials, sélectionner le Client Authenticator Client Id and Secret, cliquer sur Regenerate Secret et noter le Secret généré :

kubectl-oidc

Récupérer un nouveau token :

$ curl -s -k -X POST https://192.168.1.47:8443/auth/realms/master/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=kubernetes-cluster \
  -d username=xian \
  -d password=pass \
  -d scope=openid \
  -d response_type=id_token \
  -d client_secret=r60pzHf9dQt4TgcnM0dA6FUUEwqoTKzj \
  | jq .

Noter que pour pouvoir le faire nous devons maintenant ajouter le header client_secret dans la requête.

Créer ensuite un nouvel utilisateur et configurer l’authentification en utilisant le modèle suivant :

kubectl config set-credentials USER_NAME \
   --auth-provider=oidc \
   --auth-provider-arg=idp-issuer-url=( issuer url ) \
   --auth-provider-arg=client-id=( your client id ) \
   --auth-provider-arg=client-secret=( your client secret ) \
   --auth-provider-arg=refresh-token=( your refresh token ) \
   --auth-provider-arg=idp-certificate-authority=( path to your ca certificate ) \
   --auth-provider-arg=id-token=( your id_token )

exemple :

kubectl config set-credentials xian \
   --auth-provider=oidc \
   --auth-provider-arg=idp-issuer-url=https://192.168.1.47:8443/auth/realms/master \
   --auth-provider-arg=client-id=kubernetes-cluster \
   --auth-provider-arg=client-secret=r60pzHf9dQt4TgcnM0dA6FUUEwqoTKzj \
   --auth-provider-arg=refresh-token=eyJhbGciOiJIUzI1NiIsInR5cCIgO... \
   --auth-provider-arg=idp-certificate-authority=/home/xian/workspace/kubernetes-openid-connect/rootCA.crt \
   --auth-provider-arg=id-token=eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSl...

Créer un nouveau contexte de connexion pour cet utilisteur :

kubectl config set-context --cluster kind-open-id-connect --user xian xian-kind-open-id-connect

Utiliser le nouveau contexte :

kubectl config use-context xian-kind-open-id-connect

Tester l’accès :

$ kubectl get ns
NAME                 STATUS   AGE
default              Active   4d13h
kube-node-lease      Active   4d13h
kube-public          Active   4d13h
kube-system          Active   4d13h
local-path-storage   Active   4d13h

Ca marche \o/

Le script refresh-kubectl-credentials.sh permet d’automatiser le raffraichissement des tokens dans le contexte Kubeconfig :

#!/bin/bash                      

# Set credentials for kubernetes user 

idp_issuer_url=https://192.168.1.47:8443/auth/realms/master
idp_ca=/home/xian/workspace/kubernetes-openid-connect/rootCA.crt
client_id=kubernetes-cluster                                                                       
client_secret=r60pzHf9dQt4TgcnM0dA6FUUEwqoTKzj
user_name=xian
user_password=pass
debug=True

echo "Get the Open ID token..."
token=$(
curl -s -k -X POST $idp_issuer_url/protocol/openid-connect/token \
  -d grant_type=password \
  -d client_id=$client_id \
  -d username=$user_name \
  -d password=$user_password \
  -d scope=openid \
  -d response_type=id_token \
  -d client_secret=$client_secret
)

[ $debug == True ] && echo $token | jq .

id_token=$(echo $token | jq -r .id_token)
refresh_token=$(echo $token | jq -r .refresh_token)

kubectl config set-credentials $user_name \
   --auth-provider=oidc \
   --auth-provider-arg=idp-issuer-url=$idp_issuer_url \
   --auth-provider-arg=client-id=$client_id \
   --auth-provider-arg=client-secret=$client_secret \
   --auth-provider-arg=refresh-token=$refresh_token \
   --auth-provider-arg=idp-certificate-authority=$idp_ca \
   --auth-provider-arg=id-token=$id_token

Sources :