Introduction

Dans mon article sur le Secret Manager d’OVHcloud, j’expliquais que j’évitais OpenBao sur les petits clusters parce que l’héberger est pénible : il faut gérer les volumes (on est pas en stateless) et surtout le unseal. À chaque redémarrage de pod, OpenBao repart scellé et attend qu’on lui présente ses clés. Sur un cluster qu’on recrée le temps d’un live, c’est infernale.

Cette étape peut se faire manuellement (avec une ou plusieurs clés) via une puce TSM (sur du hardware spécifique) ou via un autre KMS (pouvant être un Vault/Bao également).

Et dans mon précédent article, je montrais le combo External-Secret x OVH KMS pour stocker des secrets. Dans celui-ci, on va voir comment l’utiliser pour unseal automatiquement notre Bao (ce qui est cool aussi, c’est qu’OVH supporte l’API Vault nativement).

Commençons déjà par détailler ce que c’est Unseal.

C’est quoi l’unseal ?

OpenBao chiffre tout ce qu’il stocke avec une root key. Cette root key est elle-même chiffrée, et c’est là que ça se joue :

  • Seal Shamir (le défaut) : la root key est découpée en parts qu’on distribue à des humains (ou a des scripts). Au démarrage, il faut en ressaisir 3 sur 5 (configurable). Fiable, pénible.
  • Auto-unseal : la root key est chiffrée par une clé externe détenue par un KMS/TSM. Au démarrage, OpenBao appelle le serveur KMS ou la puce TSM, récupère sa root key en clair et se déverrouille tout seul.

Deux voies vers l’OKMS

OpenBao propose deux façons de parler à un KMS OVHcloud, et le choix n’est pas anodin.

La première est le seal ovhcloud, un plugin externe (kms-ovhcloud), listé par OVHcloud comme l’intégration officielle. Il parle à l’API REST de l’OKMS. La seconde est le seal kmip, natif.

Le plugin ovhcloud n’est pas dans l’image : il faut le télécharger depuis ghcr.io avant que le serveur puisse se déverrouiller. Comme je suis pas fan de dépendre sur une autre image, je vais privilégier l’intégration déjà présente.

Préparer le KMS

Je pars du principe que vous avez déjà un OKMS provisionné (ça se fait depuis la WebUI, comme pour le Secret Manager). Il nous faut deux choses : un certificat client, et une clé qu’on va générer nous-même

Le certificat client

L’endpoint KMIP n’accepte qu’une seule authentification : le mTLS. Pas de token, pas de PAT. On génère donc une paire de clés localement et on ne fait signer que le CSR — la clé privée ne sort jamais de la machine :

openssl ecparam -genkey -name prime256v1 -noout -out okms-client.key
openssl req -new -key okms-client.key -subj "/CN=openbao-unseal" -out okms-client.csr

Puis on envoie le CSR à l’API OVH, en le rattachant à une identité (ici je fais crado avec l’utilisateur principal mais un utilisateur IAM dédié est plus propre serait propre, je détaille sa création dans l’autre article) :

POST /v2/okms/resource/d37ae355-8e45-419a-8da2-663e2c7bec12/credential
{
  "name": "openbao-unseal",
  "identityURNs": ["urn:v1:eu:identity:account:jq4381-ovh"],
  "csr": "-----BEGIN CERTIFICATE REQUEST-----\n...",
  "validity": 365
}

Le certificat signé se récupère ensuite sur GET /v2/okms/resource/d37ae355-8e45-419a-8da2-663e2c7bec12/credential/def89cc7-d353-4709-b8a5-85706c3b28ec via l’API OVH.

Astuce

à retenir quand même la valeur du champ validity qui plafonne à 365 jours. Notez la date quelque part : le jour où ce certificat expire, OpenBao ne peut plus se déverrouiller et il faudra faire une rotation.

La clé de seal

Pour créer une clé KMIP, il faut parler KMIP. La CLI d’OVH s’en charge (installée évidemment avec Mise :D)

mise use -g "github:ovh/okms-cli[exe=okms]"

okms kmip create symmetric --alg AES --size 256 --usage Encrypt,Decrypt \
  --name openbao-seal \
  --endpoint eu-west-gra.okms.ovh.net:5696 --okmsId d37ae355-8e45-419a-8da2-663e2c7bec12 \
  --auth-method mtls --cert okms-client.crt --key okms-client.key

# {"ObjectType": "SymmetricKey", "UniqueIdentifier": "3d384932-4706-4227-a61f-ae939cd329c1"}

On doit ensuite l’activer, une clé KMIP qui vient d’être créée est en état pre-active et OpenBao refusera de s’en servir.

okms kmip activate 3d384932-4706-4227-a61f-ae939cd329c1

Déployer OpenBao

Le certificat client part dans un Secret de type TLS, que l’on montera dans le pod :

kubectl create namespace openbao
kubectl -n openbao create secret tls okms-client-cert \
  --cert=okms-client.crt --key=okms-client.key

Puis les values du chart Helm officiel pointant vers ce même secret

server:
  volumes:
    - name: okms-client
      secret:
        secretName: okms-client-cert
        defaultMode: 0400
  volumeMounts:
    - name: okms-client
      mountPath: /openbao/okms
      readOnly: true

  standalone:
    enabled: true
    config: |
      ui = true

      listener "tcp" {
        tls_disable     = 1
        address         = "[::]:8200"
        cluster_address = "[::]:8201"
      }

      storage "pebbledb" {
        path = "/openbao/data/pebbledb"
      }

      seal "kmip" {
        endpoint    = "eu-west-gra.okms.ovh.net:5696"
        kms_key_id  = "3d384932-4706-4227-a61f-ae939cd329c1"
        client_cert = "/openbao/okms/tls.crt"
        client_key  = "/openbao/okms/tls.key"
        encrypt_alg = "AES_GCM"
      }      

Puis on peut installer tranquilou notre OpenBao :

helm repo add openbao https://openbao.github.io/openbao-helm
helm upgrade --install openbao openbao/openbao -n openbao -f values.yaml

Dans les logs, on voit qu’il récupère bien la clé via le KMS.

Auto Seal: kmip (builtin: true, encrypt_alg: "AES_GCM",
                 endpoint: "eu-west-gra.okms.ovh.net:5696",
                 kms_key_id: "3d384932-4706-4227-a61f-ae939cd329c1",
                 kmip_version: "1.4")

Il ne reste qu’à l’initialiser et c’est la seule fois où on aura affaire à des clés :

kubectl -n openbao exec openbao-0 -- bao operator init -format=json > init.json
{
  "unseal_keys_b64": [],
  "unseal_keys_hex": [],
  "unseal_shares": 1,
  "unseal_threshold": 1,
  "recovery_keys_b64": [
    "3GlHd6ZK1E4tYb0vM9rKapgyv3heYCtf4HQnMHQwHhzn",
    "MXeUi9d/22zYBeJCt4/THm/h3EuR5WrkDWNNku84LJtM",
    "cZTtPUY6ikQWfWl/VaTTm8GO1l0sTb7Usgbc8QT2Aw35",
    "xSU0Qd8rhRG5niz+Q/NNfVjKG+m6OQDZNz/H47/Iu2pc",
    "KrAQ18Ij+Vl+vqRokdx9LuACVRlGjIKiH6sSQw2bumK0"
  ],
  "recovery_keys_hex": [
    "dc694777a64ad44e2d61bd2f33daca6a9832bf785e602b5fe074273074301e1ce7",
    "3177948bd77fdb6cd805e242b78fd31e6fe1dc4b91e56ae40d634d92ef382c9b4c",
    "7194ed3d463a8a44167d697f55a4d39bc18ed65d2c4dbed4b206dcf104f6030df9",
    "c5253441df2b8511b99e2cfe43f34d7d58ca1be9ba3900d9373fc7e3bfc8bb6a5c",
    "2ab010d7c223f9597ebea46891dc7d2ee0025519468c82a21fab12430d9bba62b4"
  ],
  "recovery_keys_shares": 5,
  "recovery_keys_threshold": 3,
  "root_token": "s.em7knHmEUECQkNFMevS1HxEo"
}

Les unseal_keys sont vides : ce qui veut dire qu’il les récupère bien depuis le KMS. Ce qui reste en local, ce sont les recovery_keys (pour unseal si on perd l’accès au KMS) et le root token (premier token admin) à garder précieusement.

On peut faire le test final en supprimant un pod OpenBao pour voir s’il arrive à se unseal de manière autonome seul.

kubectl -n openbao delete pod openbao-0

Dans les logs du nouveau pod :

[INFO] core: stored unseal keys supported, attempting fetch
[INFO] core: post-unseal setup starting
[INFO] core: post-unseal setup complete
[INFO] core: vault is unsealed
[INFO] core: unsealed with stored key
curl -s http://127.0.0.1:8200/v1/sys/seal-status | jq -c
# {"type":"kmip","initialized":true,"sealed":false,"recovery_seal":true}

Une petite seconde pour l’unseal et ça sans intervention.