Aller au contenu
Sécurité medium

Vault : policies et contrôle d'accès (ACL)

29 min de lecture

logo vault

Une fois authentifié, Vault détermine les permissions via les policies. Chaque policy définit des capabilities (read, write, delete...) sur des paths (secret/data/*, auth/token/...).

Vault applique un modèle deny by default : sans policy explicite, tout accès est refusé. Quand plusieurs règles peuvent correspondre à un path :

  1. Vault choisit d'abord la règle la plus spécifique (priority matching)
  2. Si exactement le même pattern est défini dans plusieurs policies attachées au token, les capabilities se cumulent
  3. La capability deny reste toujours prioritaire

Une policy ne sert à rien sans une méthode d'authentification qui la distribue et sans un secrets engine sur lequel l'appliquer. Les deux guides ci-dessous mettent en place ce socle : userpass ou AppRole pour obtenir un token, et le moteur KV v2 pour disposer de paths réels à autoriser ou refuser. Sans eux, vous écrirez des règles que rien ne déclenche.

  • Vault installé et démarré
  • Accès root ou admin pour créer des policies

Vault crée deux policies dès l'initialisation, avant que vous n'écriviez la moindre règle. Elles encadrent les deux extrêmes du modèle : accès illimité d'un côté, socle minimal de l'autre. Les connaître évite deux erreurs fréquentes : administrer durablement avec le token root, et croire qu'un token authentifié n'a aucune permission alors que default lui en donne déjà.

La policy root donne un accès total à Vault. Le token root doit être réservé à l'initialisation ou aux situations d'urgence.

La policy default est attachée à la plupart des tokens par défaut. Elle fournit généralement un socle minimal pour que le token puisse se consulter et gérer certaines opérations de base (cubbyhole, renouvellement...).

Une policy est écrite en HCL (HashiCorp Configuration Language) ou JSON. Sa structure est toujours la même : une suite de blocs path, chacun portant une liste de capabilities. Le path n'est pas un chemin de système de fichiers, c'est l'URL de l'API HTTP de Vault privée de son préfixe /v1/. Autrement dit, autoriser un path revient à autoriser un appel API précis, ce qui explique la correspondance directe entre capabilities et verbes HTTP.

# Policy pour l'équipe développement
path "secret/data/dev/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/data/shared/config" {
capabilities = ["read"]
}
path "secret/metadata/dev/*" {
capabilities = ["list", "read", "delete"]
}

Vault ne connaît que huit capabilities, et six d'entre elles se calquent sur un verbe HTTP. Les deux dernières sont particulières : sudo débloque les endpoints dits root-protected (sys/auth, sys/seal, certaines opérations PKI) que même une policy « tout autoriser » ne couvre pas, et deny refuse un path quoi qu'en disent les autres policies attachées au token.

CapabilityDescriptionVerbes HTTP
createCréer une nouvelle entréePOST/PUT
readLire une entréeGET
updateModifier une entrée existantePOST/PUT
deleteSupprimer une entréeDELETE
listLister les entréesLIST
patchModification partielle (si l'endpoint le supporte)PATCH
sudoAccès aux paths protégés (root-protected)-
denyRefuser explicitement (prioritaire sur tout)-

Avec le moteur KV v2, le chemin que vous tapez au clavier n'est pas celui que la policy doit autoriser. La commande vault kv get secret/myapp interroge en réalité secret/data/myapp : le versioning insère un segment technique (data, metadata, destroy) entre le mount et le nom du secret. C'est la cause numéro un des permission denied inexpliqués sur KV v2.

PathUsage
secret/data/*Lecture/écriture des secrets
secret/metadata/*Métadonnées, versions, suppression soft
secret/delete/*Suppression soft de versions
secret/undelete/*Restauration de versions
secret/destroy/*Suppression définitive

Priority matching : comment Vault choisit la règle

Section intitulée « Priority matching : comment Vault choisit la règle »

Quand plusieurs patterns peuvent correspondre à un path, Vault applique une logique de priorité pour déterminer lequel utiliser.

Vault ne parcourt pas les règles dans l'ordre d'écriture : il classe tous les patterns qui correspondent au path demandé et ne retient que le premier. Les cinq critères ci-dessous s'appliquent en cascade, du plus discriminant au moins discriminant. L'idée directrice : plus un pattern est précis, plus il gagne, et un wildcard précoce dégrade fortement le rang d'une règle.

  1. Si le premier wildcard (+ ou *) apparaît plus tôt dans le path, la règle est moins prioritaire
  2. Une règle finissant par * est moins prioritaire qu'une sans wildcard
  3. Plus il y a de segments +, plus la priorité baisse
  4. Une règle plus courte est moins prioritaire
  5. En dernier recours : comparaison lexicographique

Le cas suivant est le plus courant en production : une règle large qui ouvre la lecture partout, et une exception deny sur l'environnement de production. Il fonctionne uniquement parce que le pattern de l'exception est plus spécifique que le pattern général. Retenez le mécanisme : le deny ne l'emporte pas parce qu'il est écrit en second, mais parce que son wildcard arrive plus tard dans le chemin.

# Règle générale : lecture sur tout secret/*
path "secret/*" {
capabilities = ["read"]
}
# Règle spécifique : deny sur prod
path "secret/data/prod/*" {
capabilities = ["deny"]
}

Pour le path secret/data/prod/app :

  • Les deux patterns matchent
  • secret/data/prod/* est plus spécifique (wildcard plus tard)
  • Vault applique deny → accès refusé

L'union ne se produit que si le même pattern exact existe dans plusieurs policies attachées au token :

# Policy A
path "secret/data/shared/*" {
capabilities = ["read"]
}
# Policy B
path "secret/data/shared/*" {
capabilities = ["list"]
}

Si un token a les deux policies, il obtient ["read", "list"] sur ce path.

Deux caractères seulement permettent d'écrire un pattern générique dans une policy, et ils n'ont pas la même portée. Confondre * et + produit soit une permission trop large (un * qui descend dans toute l'arborescence), soit une règle qui ne matche jamais. Ce chapitre fixe la syntaxe exacte et les cas limites que Vault refuse.

Le glob * avale tout le reste du chemin, y compris les segments intermédiaires, tandis que + ne consomme qu'un seul segment. Le + est donc l'outil du cloisonnement horizontal : il autorise un niveau de l'arborescence sans ouvrir ce qu'il y a en dessous.

PatternSignification
*Glob de suffixe, matche tout le reste du path
+Matche un seul segment de chemin

Deux détails de ce tableau piègent régulièrement. D'abord, secret/data/dev/* ne couvre pas secret/data/dev lui-même : le path exact du dossier doit être déclaré séparément si vous voulez le lister. Ensuite, secret/data/+/config fonctionne alors que le wildcard n'est pas en fin de chemin, car + n'est pas un glob de suffixe : cette forme est justement la seule façon d'autoriser un segment médian.

PatternCorrespond àNe correspond pas à
secret/data/dev/*secret/data/dev/a, secret/data/dev/a/b/csecret/data/dev
secret/data/apps/+secret/data/apps/a, secret/data/apps/bsecret/data/apps/a/b
secret/data/+/configsecret/data/prod/configsecret/data/a/b/config

Une policy n'existe dans Vault qu'après avoir été chargée par son nom : écrire un fichier .hcl sur disque ne produit aucun effet tant que vault policy write n'a pas été exécuté. Deux méthodes existent, et le choix n'est pas cosmétique : la version fichier se versionne dans Git et se rejoue en CI, la version inline sert aux tests ponctuels.

C'est la méthode à privilégier dès qu'une policy dépasse trois règles. Le fichier HCL devient l'artefact de référence que vous relisez en revue de code, et vault policy write se contente de le pousser. La commande écrase intégralement la policy existante portant le même nom : il n'y a pas de fusion, le fichier fourni fait foi.

  1. Créer le fichier HCL

    Fenêtre de terminal
    cat > dev-policy.hcl << 'EOF'
    # Accès complet à secret/dev/*
    path "secret/data/dev/*" {
    capabilities = ["create", "read", "update", "delete", "list"]
    }
    path "secret/metadata/dev/*" {
    capabilities = ["list", "read", "delete"]
    }
    # Lecture seule sur shared
    path "secret/data/shared/*" {
    capabilities = ["read", "list"]
    }
    EOF
  2. Charger la policy dans Vault

    Fenêtre de terminal
    vault policy write dev-policy dev-policy.hcl

    Sortie :

    Success! Uploaded policy: dev-policy
  3. Vérifier

    Fenêtre de terminal
    vault policy read dev-policy

Le tiret - à la place du nom de fichier indique à Vault de lire la policy sur l'entrée standard. Pratique pour un test rapide ou pour un script de bootstrap, mais rien n'est conservé sur disque : la seule trace subsiste dans Vault, et vous perdez l'historique de révision. Réservez cette forme aux policies courtes et jetables.

Fenêtre de terminal
vault policy write staging-readonly - << 'EOF'
path "secret/data/staging/*" {
capabilities = ["read", "list"]
}
EOF

Les policies vivent dans le backend sys/policies/acl et se manipulent comme des objets nommés. Attention à la suppression : vault policy delete retire la policy du store, mais les tokens déjà émis qui la référencent continuent d'exister. Ils perdent simplement les permissions correspondantes, ce qui se traduit par des permission denied soudains sur des workloads en cours.

Fenêtre de terminal
# Lister toutes les policies
vault policy list
# Lire une policy
vault policy read dev-policy
# Supprimer une policy
vault policy delete dev-policy

Une policy chargée mais rattachée à personne ne protège rien. L'attachement se fait au niveau de la méthode d'authentification : c'est elle qui, au moment du login, fabrique un token portant la liste de policies configurée. Les permissions sont donc figées à l'émission du token, pas évaluées à chaque requête depuis la configuration de l'utilisateur.

Le champ policies de l'entrée userpass est remplacé en totalité à chaque écriture : indiquez toujours la liste complète, sinon vous retirez sans le vouloir les policies absentes de la commande. Les tokens déjà distribués à alice conservent l'ancienne liste jusqu'à leur expiration.

Fenêtre de terminal
vault write auth/userpass/users/alice policies="dev-policy,staging-readonly"

Pour un pipeline, la policy se rattache au rôle AppRole, pas à une personne. Les paramètres de durée comptent autant que la policy elle-même : un secret_id_ttl de 10 minutes limite la fenêtre pendant laquelle un secret_id intercepté reste échangeable contre un token, et un token_ttl de 20 minutes borne la durée d'exploitation de ce token.

Fenêtre de terminal
vault write auth/approle/role/ci-app \
policies="ci-deploy-policy" \
secret_id_ttl=10m \
token_ttl=20m

La création directe de token court-circuite toute méthode d'authentification : c'est l'opération d'un administrateur, utile pour tester une policy ou dépanner. Un token enfant ne peut jamais recevoir plus de permissions que son parent, ce qui empêche une escalade depuis un token déjà restreint.

Fenêtre de terminal
vault token create -policy="dev-policy" -policy="staging-readonly"

Une policy se relit mal : le priority matching et les paths implicites de KV v2 rendent le résultat difficile à prédire à l'oeil. Les trois méthodes ci-dessous vérifient le comportement réel de Vault plutôt que votre intention. Testez toujours les deux faces : ce qui doit réussir, et surtout ce qui doit échouer.

C'est le test le plus fidèle : vous vous placez exactement dans la peau du porteur de la policy. Exportez le token dans un shell dédié pour ne pas écraser votre session administrateur, puis provoquez délibérément un refus. Une policy qui n'a jamais renvoyé de 403 dans vos essais n'a pas été testée.

Fenêtre de terminal
# Créer un token avec la policy
vault token create -policy="dev-policy" -field=token

Puis testez avec ce token :

Fenêtre de terminal
export VAULT_TOKEN="<token>"
# Doit réussir (dev/*)
vault kv put secret/dev/myapp password="test"
# Doit échouer (pas de permission)
vault kv put secret/prod/myapp password="test"

Sortie attendue pour l'échec :

Error writing data to secret/data/prod/myapp: Error making API request.
Code: 403. Errors:
* 1 error occurred:
* permission denied

Cette commande interroge le moteur d'évaluation de Vault lui-même et retourne la liste des capabilities effectives après priority matching. Elle répond donc à la vraie question, « qu'est-ce que ce token peut faire sur ce path », sans rien écrire ni consommer de secret. C'est l'outil de diagnostic à dégainer en premier devant un permission denied.

Fenêtre de terminal
# Avec le token actuel
vault token capabilities secret/data/dev/myapp
# Avec un token spécifique
vault token capabilities <token> secret/data/dev/myapp

Sortie :

create, delete, list, read, update

Certaines commandes CLI supportent -output-policy pour afficher les permissions nécessaires à une opération :

Fenêtre de terminal
vault kv get -output-policy secret/apps/webapp

Sortie :

path "secret/data/apps/webapp" {
capabilities = ["read"]
}

Utile pour construire vos policies de manière incrémentale.

Vault peut utiliser l'identité de l'utilisateur dans les paths grâce aux templated policies :

# Chaque utilisateur accède à son propre espace
path "secret/data/users/{{identity.entity.name}}/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

L'utilisateur alice pourra accéder à secret/data/users/alice/* mais pas à secret/data/users/bob/*.

Ces variables sont résolues par Vault au moment de la requête, à partir de l'entité liée au token. Une entité regroupe les différents alias d'une même personne à travers plusieurs méthodes d'authentification : alice en userpass et alice en OIDC peuvent pointer sur la même entité, donc sur le même espace de secrets. Les variables de groupe permettent le même mécanisme à l'échelle d'une équipe.

VariableDescription
{{identity.entity.id}}ID unique de l'entité
{{identity.entity.name}}Nom de l'entité
{{identity.entity.aliases.<mount>.id}}ID d'alias pour un auth method
{{identity.groups.ids.<group_id>.name}}Nom d'un groupe par ID
{{identity.groups.names.<group_name>.id}}ID d'un groupe par nom

Les policies peuvent restreindre quels paramètres peuvent être envoyés à un endpoint.

Ce bloc bascule le path en liste blanche : tout paramètre absent de la liste fait échouer la requête, même si la capability create est accordée. L'exemple suivant laisse écrire username et password dans un secret KV v2, et refuse l'ajout de tout autre champ. Le tableau vide [] signifie « n'importe quelle valeur » pour ce champ ; y placer des valeurs restreint aussi le contenu accepté.

path "secret/data/limited/*" {
capabilities = ["create", "update"]
# Seuls ces champs peuvent être écrits
allowed_parameters = {
"data" = {
"username" = []
"password" = []
}
}
}

C'est la logique inverse, une liste noire : tout passe sauf les champs nommés. Ce mode est plus permissif et donc plus fragile, puisqu'un champ sensible que vous n'avez pas anticipé reste autorisé. Il rend service quand un seul champ doit être verrouillé dans une structure par ailleurs libre, comme un jeton d'administration que les applications ne doivent jamais réécrire.

path "secret/data/apps/*" {
capabilities = ["create", "update"]
# Tout est autorisé sauf ces champs
denied_parameters = {
"data" = {
"admin_token" = []
}
}
}

required_parameters impose la présence d'un champ dans la requête, sans rien dire de sa valeur. Sur un endpoint PKI, exiger common_name empêche l'émission d'un certificat sans identité explicite, cas où le rôle appliquerait silencieusement sa valeur par défaut. Ce garde-fou force l'appelant à déclarer son intention plutôt qu'à hériter d'une configuration.

path "pki/issue/my-role" {
capabilities = ["create", "update"]
# Le common_name est obligatoire
required_parameters = ["common_name"]
}

Les quatre policies qui suivent couvrent les profils rencontrés sur presque toutes les installations. Elles illustrent surtout un principe : le moindre privilège ne consiste pas à réduire le nombre de paths, mais à séparer les verbes. Un pipeline qui chiffre n'a aucune raison de pouvoir déchiffrer, et un développeur qui écrit en dev n'a rien à faire en prod.

Cette policy repose entièrement sur le deny by default : prod n'apparaît nulle part, donc prod est refusé, sans avoir besoin d'une règle deny explicite. Notez l'asymétrie entre les environnements, complète sur dev et limitée à read/list sur staging, ainsi que l'absence de delete sur les métadonnées de staging, qui empêche d'effacer l'historique des versions.

# dev-policy.hcl - Développeur
# Accès complet à l'environnement de dev
path "secret/data/dev/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/dev/*" {
capabilities = ["list", "read"]
}
# Lecture seule sur staging
path "secret/data/staging/*" {
capabilities = ["read", "list"]
}
# Pas d'accès à prod (deny by default)

Un pipeline de build n'a généralement besoin que de chiffrer. La capability update sur transit/encrypt/ci-key autorise l'appel à l'endpoint de chiffrement, sans jamais donner accès à la clé elle-même, qui ne quitte pas Vault. Conséquence directe : un runner compromis pendant la phase de build peut produire du chiffré, mais ne peut pas relire les artefacts déjà chiffrés.

# ci-build-policy.hcl - Pipeline de build
# Chiffrement uniquement (pas de déchiffrement)
path "transit/encrypt/ci-key" {
capabilities = ["update"]
}
# Lecture des configs non sensibles
path "secret/data/build/config" {
capabilities = ["read"]
}

Le pipeline de déploiement porte le miroir exact du précédent : transit/decrypt mais pas transit/encrypt. Il lit les secrets de deploy et demande ses certificats via pki/issue/deploy-role. Cette séparation des verbes a un effet concret sur le modèle de menace : un attaquant qui prend le contrôle du job de déploiement ne peut pas réinjecter de données chiffrées qui seraient ensuite acceptées comme légitimes en amont.

# ci-deploy-policy.hcl - Pipeline de déploiement
# Lecture des secrets de déploiement
path "secret/data/deploy/*" {
capabilities = ["read"]
}
# Déchiffrement (pas de chiffrement)
path "transit/decrypt/ci-key" {
capabilities = ["update"]
}
# Accès aux certificats PKI
path "pki/issue/deploy-role" {
capabilities = ["create", "update"]
}

Cette policy remplace le token root pour l'exploitation courante. Elle donne la main sur les policies, les méthodes d'authentification et les secrets engines, mais reste bornée : elle ne couvre ni sys/seal, ni sys/rekey, ni les opérations de récupération, qui exigent les unseal keys. La capability sudo sur sys/auth/* est indispensable, car ces endpoints sont root-protected et resteraient refusés sans elle.

# admin-policy.hcl - Admin sans être root
# Gestion des policies
path "sys/policies/acl/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Gestion des auth methods (activer/désactiver)
path "sys/auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
# Configuration des auth methods
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Gestion des secrets engines
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Accès à tous les secrets
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}

Le nombre de policies grimpe vite, et une policy géante par personne devient impossible à relire. La solution consiste à découper selon des axes orthogonaux, puis à laisser Vault composer : un token porte plusieurs policies et l'utilisateur obtient l'intersection utile de ses appartenances. Les fichiers HCL sont alors versionnés dans Git et déployés par la CI.

Le découpage par équipe rattache la policy à une responsabilité organisationnelle : l'équipe backend gère ses secrets applicatifs, l'équipe sécurité les moteurs Transit et PKI. L'avantage tient à la revue, chaque fichier a un propriétaire identifié. La limite apparaît vite : rien dans ce découpage ne distingue la production du développement.

policies/
├── team-backend.hcl
├── team-frontend.hcl
├── team-devops.hcl
└── team-security.hcl

Le découpage par environnement porte l'axe qui compte le plus en sécurité : c'est lui qui matérialise la frontière prod. Une seule policy env-prod à auditer, une seule à restreindre, et la question « qui accède à la production » se répond en listant qui la porte. En revanche, ce découpage seul ne cloisonne pas les équipes entre elles à l'intérieur d'un même environnement.

policies/
├── env-dev.hcl
├── env-staging.hcl
└── env-prod.hcl

Croiser les deux axes donne le résultat recherché : alice porte sa policy d'équipe et les policies des environnements où elle est habilitée. Sur les paths couverts par les deux, les capabilities s'additionnent selon la règle d'union vue plus haut ; sur les autres, le priority matching tranche. Ne jamais ajouter env-prod à un profil de développement suffit à tenir la frontière.

Fenêtre de terminal
vault write auth/userpass/users/alice \
password="..." \
policies="team-backend,env-dev,env-staging"

Les capabilities se cumulent pour les mêmes patterns.

Vault renvoie presque toujours le même message, permission denied, quelle que soit la cause réelle. Ce laconisme est délibéré : détailler le motif du refus renseignerait un attaquant sur l'arborescence des secrets. Le tableau ci-dessous fait la traduction, et la dernière ligne mérite une attention particulière car elle explique les corrections de policy « sans effet ».

SymptômeCause probableSolution
permission deniedPolicy manquante ou pattern non couvertvault token capabilities <path>
invalid policyErreur de syntaxe HCLValider avec vault policy write
Capabilities inattenduesPriority matching → mauvaise règle sélectionnéeVérifier les wildcards
Wildcard ne marche pas* pas en fin de path, ou + vs *Revoir la syntaxe
KV v2 access deniedPath sans /data/Ajouter /data/ au path
Policy modifiée sans effetToken émis avant modificationLes tokens gardent leurs policies d'émission

vault token lookup affiche la liste de policies gravée dans le token au moment de son émission, ainsi que son TTL restant. Comparez-la à la configuration actuelle de l'utilisateur : un écart confirme que le token est antérieur à votre modification et qu'il faut se reconnecter plutôt que retoucher la policy.

Fenêtre de terminal
# Token actuel
vault token lookup
# Token spécifique
vault token lookup <token>

Plutôt que de deviner le path exact attendu par Vault, laissez la CLI l'écrire. -output-policy n'exécute pas la commande : il imprime le bloc HCL minimal qu'il faudrait accorder pour qu'elle réussisse, segment data de KV v2 compris. C'est la façon la plus fiable de construire une policy au strict nécessaire, règle après règle.

Fenêtre de terminal
vault kv get -output-policy secret/myapp

Les huit points ci-dessous condensent ce qui provoque le plus d'incidents en exploitation. Les trois premiers portent sur le moteur d'évaluation et expliquent la quasi-totalité des permissions inattendues ; les suivants relèvent de la syntaxe et de la conception. Relisez-les avant de pousser une policy en production.

  1. Deny by default : sans policy explicite, tout est refusé
  2. Priority matching : la règle la plus spécifique s'applique en premier
  3. Union conditionnelle : les capabilities se cumulent uniquement sur le même pattern dans plusieurs policies
  4. KV v2 paths : secret/data/*, secret/metadata/*, etc.
  5. Wildcards : * (suffixe uniquement, fin de path), + (un segment)
  6. Templates : {{identity.entity.id}}, préférez les IDs aux noms
  7. deny prioritaire : toujours évalué en dernier, gagne toujours
  8. Moindre privilège : séparez les rôles (build ≠ deploy, dev ≠ prod)

Une policy ne prend son sens qu'appliquée à un moteur concret. Les guides ci-dessous fournissent les paths à autoriser : pki/issue/<role> pour l'émission de certificats TLS, transit/encrypt et transit/decrypt pour le chiffrement, et les endpoints auth/ pour rattacher vos policies aux identités qui les porteront.

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