Contexte

Cet article décrit la mise en place de HashiCorp Vault sur Debian 13 dans une configuration durcie : stockage Raft intégré, API en TLS, audit activé. Vault sert ensuite deux usages concrets : le chiffrement au repos (TDE) de MariaDB via le plugin hashicorp_key_management, et la fourniture d’une clé de chiffrement à une application PHP qui chiffre les fichiers avant stockage.

Topologie retenue : un seul noeud Vault (simple à opérer, suffisant pour un test ou une petite infra), dépôt APT officiel HashiCorp, certificat auto-signé interne pour le labo. Adaptez les adresses IP à votre réseau.

Architecture

Vault écoute en HTTPS sur le port 8200. MariaDB récupère ses clés de chiffrement via l’API Vault (moteur KV v2). L’application PHP récupère une clé dédiée via la même API, chiffre le fichier en AES-256-GCM avec OpenSSL, puis stocke le résultat. Vault ne voit jamais les fichiers : il ne distribue que les clés.

1. Installation de Vault depuis le dépôt HashiCorp

On installe la version officielle (plus récente que le paquet Debian figé) avec vérification de signature :

sudo apt update
sudo apt install -y gpg wget lsb-release
wget -qO - https://apt.releases.hashicorp.com/gpg | sudo gpg --dearmor -o /usr/share/keyrings/hashicorp-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] https://apt.releases.hashicorp.com trixie main" | sudo tee /etc/apt/sources.list.d/hashicorp.list
sudo apt update
sudo apt install -y vault jq openssl
vault --version

2. Certificat TLS auto-signé pour le labo

Vault exige du TLS dès qu’on l’expose au-delà du loopback. Pour un labo, un certificat auto-signé suffit (en production, préférez votre AC interne). Remplacez l’IP par celle de votre serveur :

sudo mkdir -p /etc/vault/tls
sudo openssl req -x509 -newkey rsa:4096 -nodes \
  -keyout /etc/vault/tls/vault.key \
  -out /etc/vault/tls/vault.crt \
  -days 825 -subj "/CN=vault" \
  -addext "subjectAltName=IP:192.0.2.10,DNS:vault"
sudo chown -R vault:vault /etc/vault/tls
sudo chmod 600 /etc/vault/tls/vault.key
sudo chmod 644 /etc/vault/tls/vault.crt
openssl x509 -in /etc/vault/tls/vault.crt -noout -subject -dates

3. Configuration Vault (Raft mono-noeud, TLS, UI)

Le fichier HCL active le stockage Raft intégré, le listener TLS (TLS 1.2 minimum) et l’interface web. On l’écrit avec printf plutôt qu’un heredoc, pour garder un contenu exact :

sudo mkdir -p /etc/vault.d /var/lib/vault/raft
sudo printf '%s\n' 'storage "raft" {' '  path = "/var/lib/vault/raft"' '  node_id = "vault1"' '}' '' 'listener "tcp" {' '  address = "0.0.0.0:8200"' '  tls_cert_file = "/etc/vault/tls/vault.crt"' '  tls_key_file = "/etc/vault/tls/vault.key"' '  tls_min_version = "tls12"' '}' '' 'api_addr = "https://192.0.2.10:8200"' 'cluster_addr = "https://192.0.2.10:8201"' 'ui = true' | sudo tee /etc/vault.d/vault.hcl
sudo chown -R vault:vault /etc/vault.d /var/lib/vault
sudo chmod 640 /etc/vault.d/vault.hcl

4. Durcissement : mlock, permissions, pare-feu, audit

Vault verrouille la mémoire (mlock) pour que les clés ne partent jamais en swap : il faut la capacité IPC_LOCK. On restreint aussi le port 8200 au sous-réseau des bases et de l’applicatif, puis on active le journal d’audit :

sudo setcap cap_ipc_lock=+ep /usr/bin/vault
sudo systemctl enable --now vault
systemctl is-active vault
sudo ufw allow from 192.0.2.0/24 to any port 8200 proto tcp
sudo ufw allow OpenSSH
sudo ufw --force enable
sudo ufw status numbered
export VAULT_ADDR="https://192.0.2.10:8200"
export VAULT_SKIP_VERIFY=true
vault status || true
vault audit enable file file_path=/var/log/vault/audit.log

VAULT_SKIP_VERIFY=true n’est acceptable qu’en labo avec un certificat auto-signé. En production, déposez la CA dans le magasin système et laissez la vérification active.

5. Initialisation et descellage (Shamir)

L’initialisation génère 5 parts de clé dont 3 suffisent à desceller. Stockez le JSON hors du serveur (gestionnaire de secrets, papier en coffre). Chaque unseal est à refaire après un redémarrage :

export VAULT_ADDR="https://192.0.2.10:8200"
export VAULT_SKIP_VERIFY=true
vault operator init -key-shares=5 -key-threshold=3 -format=json | tee /root/vault-init.json
chmod 600 /root/vault-init.json
vault operator unseal PART_DE_CLE_1
vault operator unseal PART_DE_CLE_2
vault operator unseal PART_DE_CLE_3
vault login TOKEN_ROOT
vault status

6. Moteur KV pour MariaDB et création des clés TDE

Le plugin MariaDB exige un moteur KV version 2 et des clés nommées par leur identifiant numérique, chacune avec un champ data de 32 octets (AES-256). Ici deux clés, générées en hexadécimal :

export VAULT_ADDR="https://192.0.2.10:8200"
export VAULT_SKIP_VERIFY=true
vault login TOKEN_ROOT
vault secrets enable -path=mariadb-kv -version=2 kv
CLE1="$(openssl rand -hex 32)"
CLE2="$(openssl rand -hex 32)"
vault kv put mariadb-kv/1 data="$CLE1"
vault kv put mariadb-kv/2 data="$CLE2"
vault kv get mariadb-kv/1
unset CLE1 CLE2

7. Politique et token dédiés à MariaDB (moindre privilège)

MariaDB ne reçoit qu’un token lecture seule : lecture des clés sous mariadb-kv/data plus lecture du point de réglage pour la vérification de version KV. Le token est périodique (renouvelé automatiquement tant que MariaDB tourne) :

export VAULT_ADDR="https://192.0.2.10:8200"
export VAULT_SKIP_VERIFY=true
vault login TOKEN_ROOT
printf '%s\n' 'path "mariadb-kv/data/*" {' '  capabilities = ["read"]' '}' '' 'path "sys/mounts/mariadb-kv/tune" {' '  capabilities = ["read"]' '}' | vault policy write mariadb-tde -
vault policy read mariadb-tde
vault token create -policy=mariadb-tde -period=768h -display-name="mariadb-tde" -format=json | jq -r .auth.client_token

Notez le token retourné : c’est TOKEN_MARIADB_TDE dans la suite. Si Vault est injoignable au démarrage, le plugin utilise son cache local (comportement depuis MariaDB 10.6.24) : prévoyez malgré tout une supervision du port 8200.

8. Côté MariaDB : plugin, CA et chiffrement

Sur le serveur MariaDB (paquet Debian 13 incluant le plugin), on installe le plugin, on dépose le certificat Vault comme CA, puis on déclare le chiffrement. La section [mariadb] ci-dessous va dans un fichier dédié, pas dans un bloc de commandes :

sudo apt install -y mariadb-server mariadb-plugin-hashicorp-key-management
sudo mkdir -p /etc/mysql/vault-ca
sudo cp /etc/vault/tls/vault.crt /etc/mysql/vault-ca/vault-ca.crt
sudo chown -R mysql:mysql /etc/mysql/vault-ca
sudo chmod 644 /etc/mysql/vault-ca/vault-ca.crt
[mariadb]
plugin_load_add = hashicorp_key_management
loose_hashicorp_key_management_vault_url = https://192.0.2.10:8200/v1/mariadb-kv
loose_hashicorp_key_management_token = TOKEN_MARIADB_TDE
loose_hashicorp_key_management_vault_ca = /etc/mysql/vault-ca/vault-ca.crt
innodb_encrypt_tables = ON
innodb_encrypt_temporary_tables = ON
encrypt_binlog = ON

Le fichier s’appelle par exemple /etc/mysql/mariadb.conf.d/99-tde.cnf (propriétaire root, droits 640, MariaDB doit pouvoir le lire via son groupe). Puis :

sudo chmod 640 /etc/mysql/mariadb.conf.d/99-tde.cnf
sudo systemctl restart mariadb
sudo mysql -e "SHOW PLUGINS SONAME LIKE '%hashicorp%';"
sudo mysql -e "SHOW GLOBAL VARIABLES LIKE '%encrypt%';"

9. Activer le TDE sur une table et vérifier

Avec innodb_encrypt_tables = ON, les nouvelles tables chiffrent par défaut ; on peut aussi chiffrer explicitement table par table et contrôler le résultat dans les tables de performance :

sudo mysql -e "CREATE DATABASE IF NOT EXISTS app; USE app; CREATE TABLE IF NOT EXISTS documents (id INT AUTO_INCREMENT PRIMARY KEY, nom VARCHAR(255), contenu LONGBLOB) ENCRYPTED=YES;"
sudo mysql -e "SELECT NAME, ENCRYPTION_SCHEME, MIN_KEY_VERSION FROM INFORMATION_SCHEMA.INNODB_TABLESPACES_ENCRYPTION WHERE NAME LIKE 'app/%';"
sudo mysql -e "ALTER TABLE app.documents ENCRYPTION_KEY_ID=2;"
sudo mysql -e "SELECT NAME, ENCRYPTION_SCHEME, MIN_KEY_VERSION FROM INFORMATION_SCHEMA.INNODB_TABLESPACES_ENCRYPTION WHERE NAME LIKE 'app/%';"

La rotation consiste à écrire une nouvelle version de la clé dans Vault puis à basculer ENCRYPTION_KEY_ID. Sur MariaDB 12.3 et plus, un FLUSH HASHICORP_KEY_MANAGEMENT_CACHE recharge les clés sans redémarrage ; sur les versions antérieures, prévoyez un redémarrage planifié.

10. Clé applicative : stocker une clé de chiffrement pour les fichiers

Second usage : l’application stocke une clé symmetrique dans Vault (montage séparé app-keys), la récupère à la volée via l’API et chiffre les fichiers avant de les déposer sur disque ou objet. Création côté Vault :

export VAULT_ADDR="https://192.0.2.10:8200"
export VAULT_SKIP_VERIFY=true
vault login TOKEN_ROOT
vault secrets enable -path=app-keys -version=2 kv
CLE_FICHIER="$(openssl rand -base64 32)"
vault kv put app-keys/upload-cle valeur="$CLE_FICHIER"
vault kv get app-keys/upload-cle
unset CLE_FICHIER
printf '%s\n' 'path "app-keys/data/upload-cle" {' '  capabilities = ["read"]' '}' | vault policy write app-fichiers -
vault token create -policy=app-fichiers -period=768h -display-name="app-fichiers" -format=json | jq -r .auth.client_token

Variante sans gestion de clé côté applicatif : le moteur Transit chiffre directement (Vault ne rend que le chiffré). À évaluer si vous préférez ne jamais manipuler la clé :

export VAULT_ADDR="https://192.0.2.10:8200"
export VAULT_SKIP_VERIFY=true
vault login TOKEN_ROOT
vault secrets enable transit
vault write -f transit/keys/fichiers
echo -n "contenu de test" | base64 -w0
vault write transit/encrypt/fichiers plaintext="CONTENU_BASE64"

11. Exemple PHP : récupérer la clé et chiffrer un upload

Exemple minimal en PHP avec cURL et OpenSSL (AES-256-GCM). Collez ce code dans upload.php après avoir ajouté la balise d’ouverture PHP en première ligne, non reproduite ici pour la coloration. Le token applicatif passe par variable d’environnement, jamais en dur :

// upload.php -- ajouter la balise d'ouverture PHP en ligne 1
const VAULT_ADDR = "https://192.0.2.10:8200";
const MOUNT = "app-keys";
const NOM_CLE = "upload-cle";
function vaultLireValeur(string $token, string $champ): string {
  $url = VAULT_ADDR . "/v1/" . MOUNT . "/data/" . NOM_CLE;
  $ch = curl_init($url);
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-Vault-Token: " . $token]);
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
  curl_setopt($ch, CURLOPT_CAINFO, "/etc/ssl/certs/vault-ca.crt");
  $rep = curl_exec($ch);
  $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
  curl_close($ch);
  if ($rep === false) {
    throw new RuntimeException("appel Vault impossible");
  }
  if ($code !== 200) {
    throw new RuntimeException("Vault a repondu code " . $code);
  }
  $json = json_decode($rep, true);
  $valeur = $json["data"]["data"][$champ] ?? "";
  if ($valeur === "") {
    throw new RuntimeException("champ absent dans Vault");
  }
  return $valeur;
}
function chiffrerFichier(string $clair, string $cleBase64, string $dest): void {
  $cle = base64_decode($cleBase64, true);
  if ($cle === false) {
    throw new RuntimeException("cle base64 invalide");
  }
  if (strlen($cle) !== 32) {
    throw new RuntimeException("cle de taille inattendue");
  }
  $iv = random_bytes(12);
  $tag = "";
  $chiffre = openssl_encrypt($clair, "aes-256-gcm", $cle, OPENSSL_RAW_DATA, $iv, $tag);
  if ($chiffre === false) {
    throw new RuntimeException("chiffrement impossible");
  }
  file_put_contents($dest, $iv . $tag . $chiffre);
}
$token = getenv("VAULT_TOKEN_APP");
if ($token === false) {
  throw new RuntimeException("VAULT_TOKEN_APP manquant");
}
$cleBase64 = vaultLireValeur($token, "valeur");
$contenu = file_get_contents($_FILES["fichier"]["tmp_name"]);
if ($contenu === false) {
  throw new RuntimeException("lecture upload impossible");
}
chiffrerFichier($contenu, $cleBase64, "/srv/stockage/doc1.bin.enc");
echo "OK : fichier chiffre stocke";

En miroir, le déchiffrement relit les 12 premiers octets (IV), les 16 suivants (tag GCM), puis appelle openssl_decrypt avec la même clé récupérée dans Vault. Seul le compte applicatif détenteur du token peut lire la clé.

Points de vigilance

Le descellage est manuel après chaque redémarrage (c’est le prix de Shamir) : documentez qui détient les parts et testez la procédure. Sauvegardez le stockage Raft (/var/lib/vault/raft) et surtout les parts et le token root initial, sans quoi les données sont perdues. Le token MariaDB est périodique : s’il expire sans renouvellement (Vault arrêté trop longtemps), MariaDB bascule sur son cache puis échoue au redémarrage, prévoyez une alerte. Enfin, ne mélangez pas les montages : mariadb-kv pour le TDE (clés numériques), app-keys pour l’applicatif, transit si vous externalisez le chiffrement.

Récapitulatif des commandes

vault status
vault kv list mariadb-kv/
vault kv get mariadb-kv/1
vault policy read mariadb-tde
vault token lookup -format=json | jq -r .data.policies
sudo mysql -e "SHOW PLUGINS SONAME LIKE '%hashicorp%';"
sudo mysql -e "SELECT NAME, ENCRYPTION_SCHEME FROM INFORMATION_SCHEMA.INNODB_TABLESPACES_ENCRYPTION WHERE NAME LIKE 'app/%';"
vault kv get app-keys/upload-cle

Conclusion

Avec un seul noeud Vault bien durci (TLS, mlock, audit, moindre privilège), on centralise les deux besoins : TDE MariaDB via le plugin officiel en KV v2, et clés applicatives pour chiffrer les fichiers avant stockage, avec exemple PHP à l’appui. Prochaine étape logique pour la production : passer à trois noeuds Raft, adosser le TLS à votre AC interne et superviser scellements et renouvellements de tokens.