Aller au contenu
Infrastructure as Code medium

Gestion des secrets avec Salt : le renderer GPG

11 min de lecture

Logo Salt Project

Les pillars sortent les secrets des states, mais un pillar en clair reste lisible sur le master et dans Git. Ce module protège vraiment les données sensibles avec le renderer GPG : les valeurs sont chiffrées sur disque, donc versionnables, et déchiffrées à la volée par le master au moment de servir le pillar. Vous verrez aussi quand déléguer à Vault. Public visé : avancés maîtrisant les pillars.

  • Comprendre pourquoi un pillar clair ne suffit pas.
  • Chiffrer des valeurs de pillar avec le renderer GPG.
  • Consommer un secret chiffré dans un state sans le fuiter.
  • Savoir quand déléguer à Vault.

Un pillar déplace le secret hors du state, mais ne le chiffre pas. Si vous versionnez l'arbre des pillars, le mot de passe se retrouve en clair dans Git ; sur le master, il est lisible par qui a les droits sur /srv/pillar. Il faut donc une couche de chiffrement ou une source externe.

Avant de chiffrer quoi que ce soit, une règle non négociable : un secret vit exclusivement en pillar, jamais en grains ni dans un state versionné. Les grains sont modifiables localement par tout utilisateur ayant accès au minion, donc non sûrs pour une donnée sensible : un minion pourrait forger un grain pour se faire servir une valeur qui ne le concerne pas. Un state, lui, part dans Git en clair. Le pillar reste la seule source légitime, servie par le master aux seuls minions ciblés.

Le renderer GPG chiffre les valeurs d'un pillar avec une clé GPG dont la clé privée vit sur le master. Les valeurs sont stockées chiffrées dans le dépôt et déchiffrées à la volée au moment de servir le pillar. C'est l'option native, sans dépendance externe.

Salt cherche son trousseau dans /etc/salt/gpgkeys. On l'y crée, puis on génère une paire de clés dédiée :

Fenêtre de terminal
sudo install -d -m 0700 /etc/salt/gpgkeys
sudo gpg --homedir /etc/salt/gpgkeys --batch --gen-key <<'EOF'
%no-protection
Key-Type: RSA
Key-Length: 3072
Name-Real: Salt Master
Name-Email: salt@lab.local
Expire-Date: 0
%commit
EOF

La clé apparaît dans le trousseau :

Fenêtre de terminal
sudo gpg --homedir /etc/salt/gpgkeys --list-secret-keys
sec rsa3072/5B92B68E 2026-07-14 [SCEAR]
uid [ultimate] Salt Master <salt@lab.local>

On chiffre le secret pour la clé du master, en sortie ASCII armor :

Fenêtre de terminal
echo -n 'S3cr3t-PostgreSQL-2026' \
| sudo gpg --homedir /etc/salt/gpgkeys --armor --batch \
--trust-model always --encrypt -r salt@lab.local
-----BEGIN PGP MESSAGE-----
hQGMA9YZatJbkraOAQwAl1EEFnfnN1WqkRgsjQET+Bsk0XiDqpviUfartwq2nOpl
...
-----END PGP MESSAGE-----

Le fichier pillar s'ouvre par le shebang #!yaml|gpg : il indique à Salt de rendre le YAML, puis de faire passer le résultat dans le renderer GPG, qui déchiffre tout bloc PGP MESSAGE rencontré. Le bloc chiffré est indenté sous la clé, dans un scalaire littéral | :

/srv/pillar/db.sls
#!yaml|gpg
db:
password: |
-----BEGIN PGP MESSAGE-----
hQGMA9YZatJbkraOAQwAl1EEFnfnN1WqkRgsjQET+Bsk0XiDqpviUfartwq2nOpl
...
-----END PGP MESSAGE-----

Le top file des pillars ne change pas : il cible db1 par identifiant, et non par grain, précisément parce qu'un grain se forge.

/srv/pillar/top.sls
base:
'role:web':
- match: grain
- web
'db1':
- db

Après modification, on recharge les pillars sur les minions :

Fenêtre de terminal
sudo salt '*' saltutil.refresh_pillar

Vérifions la promesse à chaque étage de la chaîne. Sur disque d'abord : le fichier pillar que vous versionnez ne contient aucune valeur en clair, seul le bloc chiffré.

Fenêtre de terminal
sudo grep -c 'S3cr3t-PostgreSQL-2026' /srv/pillar/db.sls
0

En sortie de commande ensuite. Depuis Salt 3008, la valeur est masquée par défaut, ce qui évite de l'exposer dans un historique de shell ou un log :

Fenêtre de terminal
sudo salt db1 pillar.items
db1:
----------
db:
----------
password:
**********

Le déchiffrement fonctionne pourtant bien : unmask=True le prouve, en affichant la valeur que le master a déchiffrée pour ce minion.

Fenêtre de terminal
sudo salt db1 pillar.items unmask=True
db1:
----------
db:
----------
password:
S3cr3t-PostgreSQL-2026

L'isolation tient enfin : web1 n'est pas ciblé par le pillar db, il ne reçoit donc que ses propres données, même en demandant explicitement les valeurs en clair.

Fenêtre de terminal
sudo salt web1 pillar.items unmask=True
web1:
----------
app:
----------
admin_email:
ops@exemple.fr
paquets_web:
- nginx
- certbot

Le state lit le pillar comme n'importe quelle autre donnée, avec la syntaxe mapping et une valeur de repli. Le fichier produit est en 0600, lisible du seul root :

/srv/salt/dbconf.sls
/etc/appdb/db.conf:
file.managed:
- makedirs: true
- user: root
- group: root
- mode: "0600"
- contents: |
host=localhost
password={{ salt['pillar.get']('db:password', '') }}
Fenêtre de terminal
sudo salt db1 state.apply dbconf
ID: /etc/appdb/db.conf
Comment: File /etc/appdb/db.conf updated
Succeeded: 1 (changed=1)
Failed: 0

Le second passage ne change rien, et le fichier porte bien le secret avec des permissions strictes :

Fenêtre de terminal
sudo salt db1 cmd.run 'stat -c "%a %U:%G" /etc/appdb/db.conf'
db1:
600 root:root

La boucle est bouclée : le secret est chiffré dans le dépôt, déchiffré par le master au moment de servir, masqué en sortie CLI, et n'atterrit en clair que dans un fichier 0600 sur la machine qui en a besoin.

Le renderer GPG fige une valeur chiffrée dans Git. C'est parfait pour des secrets stables et un parc modeste, mais cela montre ses limites dès que le nombre de secrets grandit : changer un mot de passe impose de rechiffrer et de recommiter, et rien n'expire tout seul.

HashiCorp Vault répond à ces limites : les states récupèrent le secret au moment de l'exécution, sans jamais le stocker dans le dépôt. Surtout, Vault génère des identifiants à durée de vie courte et sait les révoquer sans redéployer un pillar. Cette rotation automatique et ces secrets dynamiques (identifiants de base créés à la demande, puis expirés) réduisent fortement la fenêtre d'exposition en cas de fuite.

Le critère de bascule est simple : tant que vos secrets sont peu nombreux et rarement changés, le GPG suffit et coûte moins cher en exploitation. Dès que vous avez besoin de rotation, de révocation ou d'un audit centralisé des accès, passez à Vault. Les guides HashiCorp Vault et les secrets dynamiques de base de données détaillent cette mise en place.

Un secret correctement chiffré peut quand même fuir en clair au moment de l'exécution. Salt, comme tout outil, peut journaliser une valeur en mode verbeux ou l'afficher dans un message d'erreur. Le masquage CLI introduit en 3008 aide, mais il ne couvre pas tout : un cmd.run qui affiche un fichier de configuration, un mode debug, un retour d'erreur verbeux suffisent à recracher la valeur. Ces traces atterrissent dans les logs du master, dans un historique de shell ou dans un pipeline CI/CD, hors de tout contrôle.

SymptômeCause probableSolution
Pillar render error: Rendering SLS 'db' failedLe master ne peut pas lire le trousseauchown -R salt:salt /etc/salt/gpgkeys (le master tourne en salt, pas root)
gpg: WARNING: unsafe ownership on homedirTrousseau appartenant à rootMême correction, puis redémarrer salt-master
gpg: decryption failed: No secret keyClé privée absente du trousseau du masterVérifier gpg --homedir /etc/salt/gpgkeys --list-secret-keys
Le pillar reste vide sur le minionPillars non rechargéssalt '<minion>' saltutil.refresh_pillar
La valeur s'affiche **********Comportement normal depuis 3008Ajouter unmask=True pour vérifier, jamais en usage courant
  1. Un secret vit exclusivement en pillar, jamais en grains (modifiables localement) ni dans un state versionné.
  2. Un pillar clair n'est pas chiffré : lisible sur le master et dans Git.
  3. Le renderer GPG (#!yaml|gpg) chiffre les valeurs : le dépôt reste versionnable, le master déchiffre à la volée.
  4. Le trousseau doit appartenir à l'utilisateur salt du master, sinon le rendu du pillar échoue.
  5. La clé privée du master déchiffre tout le parc : c'est l'actif à protéger en priorité.
  6. Vault prend le relais quand il faut de la rotation, de la révocation ou des secrets dynamiques.
  7. Ne jamais journaliser un secret : vu une fois dans un log, il est compromis.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracking. Un soutien, même symbolique, m'aide à couvrir l'hébergement et à garder ces ressources gratuites. Merci pour votre appui.

Le formulaire ne s'affiche pas ? Ouvrir Ko-fi dans un onglet.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn