Aller au contenu
Infrastructure as Code medium

Salt en production : durcir le master, ACL et salt-api

12 min de lecture

Logo Salt Project

Un master Salt peut exécuter n'importe quelle commande en root sur tout le parc. C'est l'actif le plus sensible de votre infrastructure, et il n'est pas sécurisé par défaut. Ce module le durcit concrètement : restreindre le bus, cadrer les droits avec publisher_acl et eauth, exposer une API contrôlée pour la CI, et connaître les limites réelles du durcissement des minions. Public visé : avancés exploitant Salt sur un vrai parc.

  • Restreindre l'exposition réseau du bus Salt.
  • Cadrer les droits avec publisher_acl et eauth.
  • Exposer une API contrôlée avec salt-api.
  • Comprendre les limites du durcissement des minions.

Commençons par le fait qui explique la plupart des surprises en production : le paquet Salt fait tourner le salt-master sous l'utilisateur salt, pas sous root (user: salt dans /etc/salt/master). C'est une bonne nouvelle pour la sécurité : une compromission du service ne donne pas immédiatement root sur la machine.

Mais cela change tout le reste. Le master ne peut lire que ce qui appartient à salt ou lui est accessible. Trois échecs classiques en découlent, avec le même remède :

Ce que vous voulez faireCe qui échoue si vous l'oubliezRemède
Chiffrer les pillars en GPGPillar render error, trousseau illisiblechown -R salt:salt /etc/salt/gpgkeys
Servir les states par GitFSdetected dubious ownership, GitFS videchown -R salt:salt sur le dépôt
Authentifier par PAM (eauth)HTTP 401, /etc/shadow illisibleusermod -aG shadow salt

Gardez ce réflexe : quand quelque chose ne marche pas côté master, demandez-vous si l'utilisateur salt a le droit de le lire.

Le master écoute sur deux ports : le 4505 (bus de publication, il diffuse les ordres) et le 4506 (canal de retour, les minions y renvoient résultats et fichiers). Regardez sur quelle interface ils écoutent par défaut :

Fenêtre de terminal
sudo ss -tlnp | grep -E '4505|4506'
LISTEN 0 1000 0.0.0.0:4506 0.0.0.0:*
LISTEN 0 1000 0.0.0.0:4505 0.0.0.0:*

0.0.0.0 : le bus écoute sur toutes les interfaces. Sur un serveur ayant une patte publique, votre master est joignable depuis Internet. C'est le premier point à corriger, au pare-feu : n'autorisez 4505 et 4506 que depuis les sous-réseaux des minions, et faites passer l'administration par un bastion ou un VPN.

Par défaut, quiconque peut publier une commande depuis le master le fait avec les pleins pouvoirs. Le publisher_acl change cela : il déclare, pour un utilisateur système du master, quelles fonctions il peut appeler et sur quelles cibles.

Ici, l'utilisateur deploy ne peut qu'appliquer des states et interroger des services, uniquement sur les minions web :

/etc/salt/master.d/acl.conf
publisher_acl:
deploy:
- 'web*':
- state.apply
- service.status

Le résultat est net. La commande autorisée passe :

Fenêtre de terminal
sudo -u deploy salt 'web1' service.status nginx
web1:
True

Mais le shell arbitraire est refusé, tout comme la cible hors périmètre :

Fenêtre de terminal
sudo -u deploy salt 'web1' cmd.run 'id'
sudo -u deploy salt 'db1' state.apply motd
Authorization error occurred.

C'est le moindre privilège rendu concret : un opérateur ou une CI n'obtient plus root sur tout le parc, mais exactement les verbes dont ils ont besoin, sur les cibles qui les concernent.

Une CI ne devrait pas se connecter en SSH au master pour lancer des commandes. Le bon canal est salt-api, qui expose Salt en HTTP, adossé à l'eauth (authentification externe) pour rattacher les droits à un annuaire (PAM, LDAP) plutôt qu'à un compte partagé.

La configuration déclare l'API et les permissions par utilisateur, avec la même logique que publisher_acl :

/etc/salt/master.d/api.conf
rest_cherrypy:
port: 8000
disable_ssl: true # lab uniquement : en production, TLS obligatoire
external_auth:
pam:
deploy:
- 'web*':
- state.apply
- service.status
netapi_enable_clients:
- local

L'authentification renvoie un jeton à durée de vie limitée, et la réponse rappelle explicitement le périmètre accordé :

Fenêtre de terminal
curl -sS http://localhost:8000/login -H 'Accept: application/json' \
-d username=deploy -d password='...' -d eauth=pam
{
"return": [{
"token": "e5da811b046216188afa74af4dbde03d838a213e",
"user": "deploy",
"eauth": "pam",
"perms": [{"web*": ["state.apply", "service.status"]}]
}]
}

Muni de ce jeton, la CI exécute une commande dans son périmètre :

Fenêtre de terminal
curl -sS http://localhost:8000/ -H 'Accept: application/json' \
-H "X-Auth-Token: $TOKEN" \
-d client=local -d tgt='web1' -d fun=service.status -d arg=nginx
{"return": [{"web1": true}]}

Hors de son périmètre, l'API refuse, que ce soit la fonction ou la cible qui dépasse :

cmd.run sur web1 : HTTP 401
service.status sur db1 : HTTP 401
service.status sur web1 (permis) : HTTP 200

Vous obtenez ainsi une intégration CI/CD propre : un jeton, un périmètre, une trace, et aucun accès shell au master.

On lit souvent que disable_modules permet de retirer le module cmd d'un minion sensible, pour empêcher toute commande arbitraire. La réalité est plus nuancée, et mieux vaut la connaître avant de s'appuyer dessus.

L'option fonctionne pour retirer un module. En désactivant disk sur un minion, la fonction devient bien indisponible :

/etc/salt/minion.d/hardening.conf
disable_modules:
- disk
db1:
'disk' __virtual__ returned False

Mais avec cmd, le module résiste : cmd.run continue de répondre, car le système de states dépend de cmd en interne et Salt le maintient disponible. Autrement dit, disable_modules ne vous protège pas du shell arbitraire.

Pour les machines les plus critiques, la vraie réponse reste le mode masterless : le minion applique ses states en local avec salt-call --local, sans jamais se connecter à un master. Vous supprimez la dépendance au bus et la surface d'attaque associée, au prix d'une distribution des states à organiser autrement (Git, image, artefact).

Un master unique est un point de défaillance. Salt permet de faire pointer les minions vers plusieurs masters : chacun accepte les clés, et le parc reste pilotable si l'un tombe. Les masters doivent rester synchronisés (mêmes states, pillars et clés), ce que GitFS simplifie beaucoup puisque les states viennent d'un dépôt commun.

Au-delà de quelques milliers de minions, un master sature. Le syndic répond à ce besoin : un master intermédiaire relaie les ordres d'un master supérieur vers ses propres minions, formant une hiérarchie qui segmente le parc par zone ou par datacenter.

La rotation limite l'impact d'une fuite de matériel cryptographique. Côté minion, on régénère la paire, puis on retire l'ancienne clé du master (salt-key -d) avant d'accepter la nouvelle (salt-key -a) :

Fenêtre de terminal
salt-call saltutil.regen_keys

Côté master, la manœuvre est bien plus lourde : arrêter le service, retirer master.pem et master.pub du répertoire pki, redémarrer (une nouvelle paire est générée), puis rétablir la confiance de tous les minions.

SymptômeCause probableSolution
Authorization error occurred.La fonction ou la cible dépasse le publisher_aclComportement attendu : élargir l'ACL seulement si le besoin est légitime
Minion did not return. [No response] alors que le job a réussiL'utilisateur n'est pas dans le groupe saltusermod -aG salt <utilisateur>
HTTP 401 sur /login de salt-apiLe master (utilisateur salt) ne peut pas lire /etc/shadowusermod -aG shadow salt
400 Client disabled: 'local'Les clients de l'API sont désactivés par défautDéclarer netapi_enable_clients: [local]
cmd.run répond malgré disable_modulescmd n'est pas retirable ainsiDurcir au master (publisher_acl), pas au minion
  1. Le salt-master tourne sous l'utilisateur salt, pas root : c'est la cause numéro un des échecs de trousseau GPG, de dépôt GitFS et d'authentification PAM.
  2. Le bus 4505/4506 écoute sur toutes les interfaces par défaut : le filtrer au pare-feu est le premier durcissement.
  3. publisher_acl cadre qui exécute quoi, sur quelles cibles, et refuse le reste avec Authorization error occurred.
  4. L'utilisateur doit appartenir au groupe salt, sinon sa commande s'exécute mais il ne reçoit pas le retour.
  5. salt-api et l'eauth donnent à la CI un jeton cadré, sans accès shell au master. Les clients sont désactivés par défaut.
  6. disable_modules ne bloque pas cmd : le durcissement contre le shell arbitraire se fait au master, pas au minion.
  7. Le multi-master et le syndic traitent la disponibilité et l'échelle, une fois la sécurité en place.

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