Rook chiffré et kubelet conteneurisé, kamoulox ?
Rook chiffré et kubelet conteneurisé, kamoulox ?
Contexte : ça fait maintenant presque un an que je bosse avec du Rook. J’adore cette solution autant qu’elle me fait peur. Ceph est vraiment une techno puissante mais très complexe. Aujourd’hui, je vais vous parler de la mise en place de Rook avec des OSDs chiffrés sur Talos.
Déjà, petite piqûre de rappel sur Rook : c’est un opérateur Kubernetes qui permet de déployer des solutions de stockage distribué (avec Ceph). Il va utiliser des disques additionnels sur les nœuds pour y stocker les données. Ces disques sont appelés des OSDs (Object Storage Daemon). Pour lui donner accès à ces disques, deux méthodes sont possibles :
- Device Based OSD : il va directement utiliser les disques du nœud en les formatant lui-même.
- PVC Based OSD : Il va utiliser des PVs de type block pour créer des OSDs, c’est la méthode que j’utilise.
L’autre “gros” composant de Rook est le MON (Monitor). Il va surveiller l’état du cluster et permettre de le maintenir en vie avec un Qorum dont les membres sont répartis sur les nœuds. Le MON a aussi besoin de persistence (mais lui, il se contente de PVC en mode FS, pas besoin de block).
En plus de ces deux composants, Rook va également stocker des certificats et diverses informations dans le filesystem du nœud.
talosctl -e 172.16.2.98 -n worker-1 ls -r /var/lib/rook
NODE NAME
worker-1 .
worker-1 exporter
worker-1 exporter/ceph-client.ceph-exporter.asok
worker-1 exporter/ceph-mds.rook-pool-b.asok
worker-1 exporter/ceph-mgr.a.asok
worker-1 exporter/ceph-mgr.b.asok
worker-1 exporter/ceph-mon.f.asok
worker-1 exporter/ceph-mon.h.asok
worker-1 exporter/ceph-osd.5.asok
worker-1 exporter/ceph-osd.6.asok
worker-1 exporter/ceph-osd.7.asok
worker-1 rook-ceph
worker-1 rook-ceph/client.admin.keyring
worker-1 rook-ceph/crash
worker-1 rook-ceph/crash/postedworker-1 rook-ceph/log
worker-1 rook-ceph/log/ceph-client.ceph-exporter.logworker-1 rook-ceph/rook-ceph.config
worker-1 rook-ceph.cephfs.csi.ceph.com
worker-1 rook-ceph.cephfs.csi.ceph.com/log
worker-1 rook-ceph.cephfs.csi.ceph.com/log/controller-plugin
worker-1 rook-ceph.cephfs.csi.ceph.com/log/node-plugin
worker-1 rook-ceph.cephfs.csi.ceph.com/log/node-plugin/csi-cephfsplugin.log
worker-1 rook-ceph.cephfs.csi.ceph.com/log/node-plugin/csi-cephfsplugin.log.1.gz
worker-1 rook-ceph.rbd.csi.ceph.com
worker-1 rook-ceph.rbd.csi.ceph.com/log
worker-1 rook-ceph.rbd.csi.ceph.com/log/controller-plugin
worker-1 rook-ceph.rbd.csi.ceph.com/log/node-plugin
worker-1 rook-ceph.rbd.csi.ceph.com/log/node-plugin/csi-rbdplugin.log
worker-1 rook-ceph.rbd.csi.ceph.com/log/node-plugin/csi-rbdplugin.log.1.gz
Oui j’utilise Talos, lui aussi je l’adore, mais c’est une autre histoire.
En dehors des fichiers dont le nom est explicite, je ne sais pas trop à quoi ils servent, néanmoins je peux vous garantir qu’ils sont importants pour le bon fonctionnement de Rook (source: mon cluster Ceph qui meurt si je lui retire ces fichiers).
Bref, ça c’était pour le récap de Rook. Maintenant passons au vif du sujet : J’ai activé le chiffrement des OSDs (oui on va mettre un peu de temps à arriver au vif du sujet).
Chiffrement des OSDs
Une nouvelle preuve que Rook est incroyable : il peut chiffrer lui-même les OSDs et stocker les clés dans le secret store de Kubernetes, et ce, sans aucun KMS ou Vault nécessaire. Pas sur que ça soit le meilleur moyen de gérer les clés de chiffrement, mais c’est déjà un excellent début.
Pour activer le chiffrement, il suffit de rajouter le champ .spec.storageClassDeviceSets[].encrypted: true
dans le fichier de configuration de Rook.
storage:
flappingRestartIntervalHours: 0
storageClassDeviceSets:
- count: 3
+ encrypted: true
name: data-pool
placement: {}
resources: {}
volumeClaimTemplates:
- metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 1Gi
storageClassName: rook-osd
volumeMode: Block
Les nouveaux OSDs dans ce DeviceSet seront chiffrés, quel bonheur ! 😌
J’imagine que les performances vont en prendre un coup (j’ai pas encore testé), mais le seul trade-off que je vois pour le moment est le temps de démarrage des OSDs qui est plus long (notamment car les initContainers sont bien plus nombreux).
Sans chiffrement :
$ kubectl get pods -n rook-ceph rook-ceph-osd-0-6cb7b4ff56-s5t2d -o jsonpath='{.spec.initContainers[*].name}'
blkdevmapper activate expand-bluefs chown-container-data-dir
Avec chiffrement :
$ kubectl get pods -n rook-ceph rook-ceph-osd-0-85d5797b9d-84hsh -o jsonpath='{.spec.initContainers[*].name}'
blkdevmapper encryption-open blkdevmapper-encryption encrypted-block-status expand-encrypted-bluefs activate expand-bluefs chown-container-data-dir
Effectivement, il y a un peu plus de boulot pour démarrer un OSD chiffré 🫣, mais c’est pas bien grave, la sécurité avant tout !
En plus, le chiffrement se base sur des technos maitrisées (dm-crypt et LUKS), donc je suis plutôt confiant sur la robustesse de la solution. Rook propose même de créer des CronJobs qui vont faire la rotation des clés de chiffrement, c’est vraiment bien pensé.
Pour les activer, il suffit de modifier le CephCluster comme ceci :
security:
keyRotation:
enabled: true
schedule: '@hourly' # Fréquence de rotation des clés
kms: {}
Si on regarde le contenu des cronJobs de rotation de clés, on peut voir qu’il va taper directement sur le filesystem du nœud pour faire la rotation des clés.
# kubectl get cronjob -n rook-ceph rook-ceph-osd-key-rotation-2 -o yaml
volumeMounts:
- mountPath: /var/lib/ceph/osd/
name: bridge
dnsPolicy: ClusterFirst
volumes:
- hostPath:
path: /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
type: Directory
name: bridge
De ce que je comprends : le chemin /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
pointe vers des fichiers block pour communiquer avec l’OSD (à la fois pour récupérer des infos et pour faire la rotation des clés).
$ ls -hal /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
drwxr-xr-x 2 167 167 146 Mar 24 16:38 .
drwxr-xr-x 3 root root 20 Mar 24 16:37 ..
… Vide ? 🤔 Bon d’accord.
M’enfin c’est pas grave, je viens de vérifier que les pods OSDs tournent bien et arrivent bien à trouver avec les fichiers qu’on vient d’évoquer. Donc tout va bien, je suis rassuré.
$ kubectl exec -n rook-ceph rook-ceph-osd-2-5944c4d4b6-6kr77 -c osd -- ls /var/lib/ceph/osd/ceph-2
block
block-tmp
ceph_fsid
fsid
keyring
ready
require_osd_release
type
whoami
$ kubectl get pods -n rook-ceph rook-ceph-osd-2-5944c4d4b6-6kr77 -o yaml
# ...
volumeMounts:
- mountPath: /var/lib/ceph/osd/ceph-2
name: rook-pool-data-1hmgl6-bridge
subPath: ceph-2
# ....
volumes:
- hostPath:
path: /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6
type: DirectoryOrCreate
name: rook-pool-data-1hmgl6-bridge
Bref, je peux continuer mon éloge de Rook, je vous ai déjà dit que c’était une super solution ? 😇 Pour le prouver encore plus, on va lancer une rotation de clé !
Déjà on va récupérer la clé d’un OSD :
$ kubectl -n rook-ceph get secrets rook-ceph-osd-encryption-key-rook-pool-data-0f4lgj -o jsonpath="{.data.dmcrypt-key}" | base64 -d
3RcczSJMh5LPiklOfBsdNfermHlhOWpBb8ygAEO8kQjle13zowsj89HAHnYHZgkow52xVeNoHNMb5qGy4a9H1mvp2WWNKi3rZXzWWFA96xjR2GZqpP2hjkonKPdptbf5ib7MBNCkt5KjXDe3n6znBoJgikwPjC/nE7MTZSU9E0I=
Il s’agit de la clé de l’OSD 1, on va maintenant déclencher nous-même le job de rotation (manuellement) :
kubectl create job lets-rotate-this --from=cronjob/rook-ceph-osd-key-rotation-0 -n rook-ceph
job.batch/lets-rotate-this created
Hop, c’est parti ! On check les logs :
2025/03/24 10:14:34 maxprocs: Leaving GOMAXPROCS=2: CPU quota undefined
2025-03-24 10:14:34.641384 I | cephosd: fetching the current key
2025-03-24 10:14:34.643021 I | cephosd: adding the current key to slot "1" of the device "/var/lib/ceph/osd/block-tmp"
2025-03-24 10:14:34.643700 D | exec: Running command: cryptsetup --verbose --key-file=/tmp/2547857724 --key-slot=1 luksAddKey /var/lib/ceph/osd/block-tmp /tmp/3811962899
2025-03-24 10:14:34.646941 C | rookcmd: failed to rotate secret "rook-pool-data-0bfv4t": failed to add the current key to slot "1" of the device: failed to add new passphrase to encrypted device "/var/lib/ceph/osd/block-tmp": "Device /var/lib/ceph/osd/block-tmp does not exist or access denied.\nWARNING: The --key-slot parameter is used for new keyslot number.\nCommand failed with code -4 (wrong device or file specified).": exit status 4
Ok… Ça a pas l’air de fonctionner. 🤔 Il veut vraiment me pourrir mon ode à Rook !
Bon, c’est quoi le souci à la fin ?!
Pour rappel, voici ce qu’on a dans le spec du CronJob :
# kubectl get cronjob -n rook-ceph rook-ceph-osd-key-rotation-2 -o yaml
volumeMounts:
- mountPath: /var/lib/ceph/osd/
name: bridge
dnsPolicy: ClusterFirst
volumes:
- hostPath:
path: /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
type: Directory
name: bridge
Ça me parait être un HostPath on ne peut plus classique. Je peux modifier l’image et la commande pour vérifier ce que le pod voit.
# kubectl get cronjob -n rook-ceph rook-ceph-osd-key-rotation-2 -o yaml
# ...
spec:
containers:
- command:
- /bin/sh
- -c
- ls /var/lib/ceph/osd/
image: rook/ceph:v1.8.0
name: rook-ceph-osd-key-rotation
volumeMounts:
- mountPath: /var/lib/ceph/osd/
name: bridge
volumes:
- hostPath:
path: /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
type: Directory
name: bridge
Verdict :
$ kubectl logs -n rook-ceph rook-ceph-osd-key-rotation-2-1616574000-5j7zv
total 0
drwxr-xr-x 2 167 167 146 Mar 24 16:38 .
drwxr-xr-x 3 root root 20 Mar 24 16:37 ..
Ah… C’est vide. 🤔 (et j’ai comme une impression de déjà-vu). Mais ce n’est pas la faute du job de rotation, lui il ne fait que consommer les données que l’OSD laisse dans ce même répertoire !
Bon, je vais déjà check si l’OSD voit des fichiers dans ce répertoire :
$ kubectl exec -n rook-ceph rook-ceph-osd-2-7f7cbc8749-strdg -c osd -- ls -hal /var/lib/ceph/osd/ceph-2
total 28K
drwxr-xr-x 2 ceph ceph 146 Mar 24 16:37 .
drwxr-x--- 1 ceph ceph 20 Mar 24 16:48 ..
brw-rw---- 1 ceph ceph 251, 2 Mar 24 16:49 block
brw------- 1 ceph ceph 253, 18 Mar 24 17:00 block-tmp
-rw------- 1 ceph ceph 37 Mar 24 16:48 ceph_fsid
-rw------- 1 ceph ceph 37 Mar 24 16:48 fsid
-rw------- 1 ceph ceph 55 Mar 24 16:48 keyring
-rw------- 1 ceph ceph 6 Mar 24 16:48 ready
-rw------- 1 ceph ceph 3 Mar 24 16:37 require_osd_release
-rw------- 1 ceph ceph 10 Mar 24 16:48 type
-rw------- 1 ceph ceph 2 Mar 24 16:48 whoami
Ah beh enfin un truc qui marche !
Donc, si je comprends bien, l’OSD va écrire dans l’HostPath /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
… et le CronJob va lire dans le HostPath /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
… Mais l’un voit des fichiers et l’autre non.
Spoiler : c’est pas un bug, c’est une feature ! 😅
Bon, on continue ?
Déjà assurons-nous que l’OSD écrive bien dans un volume :
$ kubectl exec -n rook-ceph pods/rook-ceph-osd-2-5cb69bc595-xkcqf -c osd -- mount | grep /var/lib/ceph/osd/ceph-2
overlay on /var/lib/ceph/osd/ceph-2 type overlay (rw,relatime,lowerdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/4/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/3/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/2/fs:/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/1/fs,upperdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/38/fs,workdir=/var/lib/containerd/io.containerd.snapshotter.v1.overlayfs/snapshots/38/work,uuid=on)
C’est bien le cas, j’ai également vérifié que ce volume était persistant (il l’est, j’ai rollout et redémarré le node plusieurs fois, les données sont toujours là)….
Mon état d’esprit actuel :

Là, vous voyez déjà une bonne journée de tentatives vaudouesques pour comprendre ce qui se passe ( je vous passe les tentatives infructueuses). Avec les informations que je vous ai données, vous pouvez techniquement trouver la solution !
Je vous laisse réfléchir un peu, je vais me prendre un café et je reviens pour vous donner la solution. 😇
Déjà, je vais vous donner un indice : ce problème est valable sur RancherOS, FlatCar et Talos.
Deuxième indice : avec une bonne paire d’yeux, vous avez peut-être remarqué un truc “différent” dans le mount du HostPath entre le pod OSD et le CronJob.
Vous l’avez ? 😏
La solution
Voici comment le HostPath est monté dans le pod OSD :
# ---- OSD ----
volumeMounts:
- mountPath: /var/lib/ceph/osd/ceph-2
name: rook-pool-data-1hmgl6-bridge
subPath: ceph-2
----
volumes:
- hostPath:
path: /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6
type: DirectoryOrCreate
name: rook-pool-data-1hmgl6-bridge
Et voici comment le HostPath est monté dans le CronJob :
# ---- Job ----
volumeMounts:
- mountPath: /var/lib/ceph/osd/
name: bridge
----
volumes:
- hostPath:
path: /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
type: Directory
name: bridge
Les deux pointent vers /var/lib/rook/rook-ceph/rook-pool-data-1hmgl6/ceph-2
, l’un via un subPath et l’autre directement (mais oui, les chemins sont les bons). Comme les deux pods pointent bien vers le même répertoire, aucun souci de ce côté là.

Sauf qu’en réalité, IL Y A BIEN UN SOUCI ! 🤯
Lorsqu’on monte un volume en mode HostPath, c’est le kubelet qui va faire une requête à l’interface CRI (containerd dans notre cas) pour que le pod ait un accès direct au filesystem du nœud. C’est l’exacte équivalent de créer un conteneur Docker avec un volume bind-mount.
MAIS, quand on passe par un SubPath, ça change tout ! De base, quand on utilise un SubPath, c’est pour réutiliser plusieurs fois le même volume dans un même pod en rajoutant un suffixe au chemin du volume, par exemple :
volumeMounts:
- mountPath: /logs
name: logs
subPath: logs # va écrire dans /data/logs
- mountPath: /backup
name: backup
subPath: backup
# ...
volumes:
- hostPath:
path: /data
type: Directory
name: bridge
Sauf que pour arriver à ce résultat, il est obligé de passer par un premier montage dans le kubelet qui va ensuite monter ce volume dans le pod. Et c’est là que le bât blesse : le kubelet étant un conteneur, il n’a pas accès au filesystem du nœud !!
Me demandez pas pourquoi, c’est une question de namespace Linux de ce que j’ai compris. Voici une issue de 2018 qui en parle
DONC POUR RÉSUMER :
- Si on utilise un HostPath sans SubPath, le pod va taper directement sur le filesystem du nœud.
- Si on utilise un HostPath avec SubPath, le pod va taper sur le filesystem du kubelet (qui n’a pas accès au filesystem du nœud s’il est conteneurisé).
Et beh bordel, c’est pas simple à comprendre tout ça ! 😅 Ça tombe bien, j’avais été dans un cas similaire dans un précédent article .
Ce qui est ironique, c’est que la solution est la même que dans l’article précédent : il suffit de monter le répertoire parent du HostPath dans le kubelet pour que le pod puisse y accéder. Voici mon Patch de configuration talos :
machine:
kubelet:
extraMounts:
- destination: /var/lib/rook/rook-ceph
type: bind
source: /var/lib/rook/rook-ceph
options:
- bind
- rshared
- rw
Maintenant, le déclenchement du job de rotation de clé fonctionne parfaitement ! 🎉
kubectl -n rook-ceph get secrets rook-ceph-osd-encryption-key-rook-pool-data-0f4lgj -o jsonpath="{.data.dmcrypt-key}"
WGs2Y3ZESVF5cWoxekU5SUhSM2F2WGplM0kzMUF3bDZCY21MWnowTkZFeUpxQlNxaXVhdEtxemhjMlQ0UlBaZWE4WFo5V2xEZEZsZ04vbDI1Ymx2T0N4aWJYTmZiOTZGa3hXUlUvb1MvYkN1My9MRGcyVTRSc3BpQjVoK0FlT2N1N0VTNUZEREhvLytrT0JzVFMraXVSekcxdWVkL0VBc1lBaXhBcG00clRjPQ==
Bref, une bonne journée de perdue pour un problème que j’ai déjà eu dans un autre contexte. 😅