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
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 :
Dans le menu de gauche, cliquer sur Users
et ajouter 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 :
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é :
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
.
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
:
Indiquer les Valid Redirect URIs http://*
et https://*
:
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é :
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 :
- https://hasinthaindrajee.medium.com/authenticate-to-kubernetes-api-server-with-an-external-identity-provider-4fbb7a73080e
- https://kubernetes.io/docs/reference/access-authn-authz/authentication/
- https://hasinthaindrajee.medium.com/authenticate-to-kubernetes-api-server-with-an-external-identity-provider-4fbb7a73080e