Combien de fois avez-vous pesté car vous n’aviez plus les credentials pour vous connecter sur un environnement de staging ou de production ?
Vous importez un dump en dev et vous vous dépêchez d’aller changer le mot de passe en console car vous ne connaissez pas celui que vous avez importé ?
Ou vous lancez un conteneur sans être sûr qu’il ne manque pas des variables d’environnement ou que leurs valeurs sont à jour ?
Pour répondre à ces problématiques, arrêtez de vous envoyer des mots de passe dans Slack, il existe Vault, maintenu par Hashicorp parmi leur excellente suite d’outils.
Vault vise à résoudre les problématiques suivantes :
Par ailleurs chez Synbioz nous avions quelques pré-requis avant de choisir un outil particulier :
Dans le déroulé nous allons donc voir tour à tour comment lire et écrire dans un coffre de façon basique, utiliser les jetons, intégrer PGP dans la boucle et enfin sceller et desceller un coffre.
Dans un deuxième article nous verrons comment utiliser un backend de stockage différent avec S3, des points de montage pour sécuriser l’accès à différents services, se servir de Vault pour provisionner des variables d’environnement utilisées dans un conteneur et enfin mettre son service Vault derrière un Nginx.
Voyons tout de suite un exemple d’utilisation. J’utilise Docker pour tester, mais vous pouvez sans doute l’installer avec votre package manager favori :
docker run --name vault-server vault
Par défaut Vault va démarrer en mode développement, c’est à dire qu’il sera accessible sur l’interface de loopback (127.0.0.1), sans sécurité particulière (pas de TLS ni de jeton d’accès).
Le log nous explique comment faire pour se connecter à Vault.
RNING: Dev mode is enabled!
In this mode, Vault is completely in-memory and unsealed.
Vault is configured to only have a single unseal key. The root
token has already been authenticated with the CLI, so you can
immediately begin using the Vault CLI.
The only step you need to take is to set the following
environment variables:
export VAULT_ADDR='http://0.0.0.0:8200'
The unseal key and root token are reproduced below in case you
want to seal/unseal the Vault or play with authentication.
Unseal Key: VJconzd1Ogx60tDSFZPlb0VW0J5Ekh+GmqtdJ0vC/jM=
Root Token: 145a0045-b35e-8624-fa0c-aef810667f34
On va maintenant lancer un client en passant en variable d’environnement l’URL et le jeton :
docker run --link vault-server -e VAULT_ADDR=http://vault-server:8200 -e VAULT_TOKEN=145a0045-b35e-8624-fa0c-aef810667f34 -it vault sh
Si vous n’utilisez pas docker en développement vous aurez juste à exporter VAULT_ADDR
dans un shell différent comme indiqué au démarrage du serveur.
On peut maintenant commencer à écrire un secret :
vault write secret/hello value=world
Et consulter nos secrets :
/ # vault read secret/hello
Key Value
--- -----
refresh_interval 768h0m0s
value world
Ici secret
représente le point de montage par défaut, nous sommes obligés d’utiliser ce préfixe (nous y reviendrons dans le deuxième article). Pour le reste nous pouvons utiliser le nombre de sous niveaux que nous voulons. Dans notre cas nous utilisons une arborescence de type client/projet/environnement.
Rien ne nous empêche d’écrire plusieurs clés / valeurs d’un coup. Attention toutefois, si vous écrivez foo=bar
puis bar=baz
, la deuxième instruction va tout écraser, il faut donc utiliser soit des namespaces différents soit repasser l’ensemble des clés / valeurs.
Vault, quand il est lancé en tant que serveur, est accessible via une API au travers d’HTTP. Dans notre conteneur client, on pourrait très bien faire :
curl -X GET -H "X-Vault-Token:$VAULT_TOKEN" http://vault-server:8200/v1/secret/hello
{"request_id":"929e52b1-dcda-6082-702a-2a17e7faa292","lease_id":"","renewable":false,"lease_duration":2764800,"data":{"value":"world"},"wrap_info":null,"warnings":null,"auth":null}
Dans notre exemple précédent nous avons utilisé la clé root pour lire et écrire dans le coffre. C’est clairement une pratique à proscrire. La clé root doit uniquement servir à gérer la configuration du coffre.
Sortons un peu du mode développement pour explorer le fonctionnement de Vault. Vault fonctionne avec des fichiers de configuration HCL, qui est une syntaxe de plus haut niveau que JSON (c’est à dire que des fichiers JSON sont lisibles par un parseur HCL, l’inverse n’est pas vrai).
Voici notre fichier config
:
backend "inmem" {
}
listener "tcp" {
address = "0.0.0.0:8200"
tls_disable = 1
}
Nous relançons notre serveur en conteneur :
docker run --rm --name vault-server -v $(pwd):/vault vault server
Notre fichier de configuration viendra donc prendre la place /vault/config
dans notre conteneur pour surcharger le fichier par défaut.
Pour le moment nous restons sur le backend inmem
pour in memory. On écoute sur 0.0.0.0
plutôt que l’interface de loopback car on accédera à notre conteneur depuis un autre conteneur. Si vous testez localement, 127.0.0.1
est suffisant.
Si nous lançons notre conteneur de cette façon nous allons tomber sur cette erreur : Error initializing core: Failed to lock memory: cannot allocate memory.
Par défaut mlock est activé et permet d’empêcher l’écriture de la mémoire dans la swap. La première solution est de désactiver mlock dans notre fichier de configuration disable_mlock = true
. C’est acceptable en développement mais pas en production.
Nous allons donc donner quelques pouvoirs en plus à notre conteneur avec -cap-add
:
docker run --rm --name vault-server --cap-add IPC_LOCK -v $(pwd):/vault vault server
Nous avons également désactivé TLS, ce que nous ne ferions pas en environnement de production, notre server Vault ne doit pas être accessible sans certificat.
Connectons-nous avec notre conteneur client :
docker run -it --link vault-server -e VAULT_ADDR=http://vault-server:8200 vault sh
Étant donné que notre serveur n’est pas lancé avec l’option -dev
, le coffre n’est pas initialisé.
vault init
Vault va nous générer un certain nombre de clés cette fois, en plus de la clé maîtresse (initial root token) :
Unseal Key 1: dF42haxHtfRsFQH7FSwKEYW+u2988+pETp20GX1wZK4B
Unseal Key 2: qlkaWcv1+uwlMLRFsxXIIZ8HdcNo2R8YchRkE+Ft2u8C
Unseal Key 3: gsF8DlJJRSH7CBnQsudrv/a/F4opmYfzM2Ee5VWxLgYD
Unseal Key 4: BQXqS6NIk8NHuONQ44Ob2ri3ITDm4qspJFPiIO6m1pME
Unseal Key 5: LZ2MHDr0LA6ZgE7F4nE4RNEPQ3mnojPCZSaY1lp6InoF
Initial Root Token: 8733cbf6-679d-9ebd-a207-f67c560ded2a
Ces clés permettront de desceller le coffre. La protection du coffre repose sur un partage des clés, selon l’algorithme de Shamir ; c’est à dire qu’il est nécessaire de posséder un certain nombre de clés pour accéder au coffre.
Par défaut Vault est configuré de manière à ce qu’il faille 3 clés sur les 5, mais il est possible de changer ces deux chiffres via la configuration. Nous en verrons un exemple un peu plus bas.
Si vous êtes paranoïaque ou curieux quant à cet algorithme, sachez que son auteur n’est autre que le co-inventeur de [RSA](https://en.wikipedia.org/wiki/RSA_(cryptosystem).
Rappel : la clé maîtresse a vocation à protéger ces clés-ci, pas à accéder directement au coffre. Cette clé est en fait attachée à la policy root, la police principale, qui a tous les droits.
Ces clés elles-même servent à récupérer l’accès au coffre, par exemple s’il a été scellé (manuellement ou si Vault redémarre)
Pour générer un jeton d’accès nous allons donc nous connecter avec notre jeton root via :
vault auth
On se connecte avec le jeton root et on lance la création d’un jeton via :
vault token-create
Qui nous donne :
Key Value
--- -----
token 1a792fec-18c5-63fa-fea6-ff5801cc3227
token_accessor 95c9ec10-207a-bd14-8633-ab31136b579a
token_duration 0s
token_renewable false
token_policies [root]
Vous avez compris les grands principes concernant la lecture, l’écriture et la protection du coffre. Allons maintenant un peu plus loin pour faire en sorte que l’utilisateur A puisse accéder au coffre Synbioz, le B au coffre client et le C au deux.
Pour cela il nous faut mettre en place des polices d’accès. Ces polices sont gérées comme le reste via des fichiers HCL. Dans /vault
nous créons le fichier synbioz.hcl
:
path "*" {
policy = "deny"
}
path "secret/synbioz/*" {
policy = "write"
}
Et client.hcl
:
path "*" {
policy = "deny"
}
path "secret/client/*" {
policy = "write"
}
Notre stratégie est celle de la liste blanche, nous bloquons l’accès à toutes les ressources par défaut, pour définir ensuite lesquelles sont accessibles.
À noter que les polices peuvent être ajoutées à chaud via un client connecté avec le jeton root :
vault policy-write synbioz synbioz.hcl
vault policy-write client client.hcl
Nous pouvons maintenant créer un jeton d’accès avec ces polices :
vault token-create -policy synbioz
Ce qui nous donne:
Key Value
--- -----
token 1d78e687-ec82-fcd3-6b9a-47c8446fc3b6
token_accessor 59fa5cd9-90c9-719c-3be7-7d5a4dbe7cc0
token_duration 768h0m0s
token_renewable true
token_policies [default synbioz]
Nous sommes prêts à écrire dans le coffre Synbioz :
vault write secret/synbioz/website/production foo=bar
Et à lire :
vault read secret/synbioz/website/production
Le format par défaut est de type table :
Key Value
--- -----
refresh_interval 768h0m0s
foo bar
Mais on peut également utiliser json
ou yaml
:
vault read -format json secret/synbioz/website/production
{
"request_id": "3fc38113-d333-9df3-5132-87110a1e3465",
"lease_id": "",
"lease_duration": 2764800,
"renewable": false,
"data": {
"foo": "bar"
},
"warnings": null
}
Par contre si j’essaie de lire les informations du namespace client :
vault read secret/client/website/production
Error reading secret/client/website/production: Error making API request.
URL: GET http://vault-server:8200/v1/secret/client/website/production
Code: 403. Errors:
* permission denied
Pour créer un jeton d’accès ayant plusieurs polices, il suffit de répéter le paramètre policy
:
vault token-create -policy client -policy synbioz
Dans ce cas notre jeton aura les droits des deux polices. Les polices peuvent parfaitement être modifiées à chaud. L’effet des modifications est immédiat.
Évidemment on peut à tout instant révoquer un jeton via :
vault token-revoke token
Pour aller un cran plus loin en terme de sécurité on peut faire en sorte de chiffrer les jetons avec des clés PGP. Vault sait s’interfacer directement avec le service Keybase, que nous utiliserons dans l’exemple ci-dessous. Toutefois il est tout à fait possible d’utiliser directement le client GPG. À l’initialisation du coffre :
vault init -key-shares=3 -key-threshold=2 -pgp-keys="keybase:fuse,keybase:fuse,keybase:fuse"
Ici on va donc définir que le coffre est protégé par 3 clés et qu’il en faut au moins 2 pour le déverrouiller. Mon exemple n’est pas très intéressant car les 3 jetons seront chiffrés avec ma clé publique. L’intérêt est de chiffrer ces clés avec les clés de personnes différentes pour faire en sorte qu’un quorum soit nécessaire pour sceller / desceller le coffre.
On récupère la même chose qu’auparavant excepté que les clés sont chiffrées :
Unseal Key 1: wcBMA3BSIcJ3gMZmAQgACglV/1XRqo556XEwhA1q5tRzFo89d4kSuWOZpYU+KS8eyGMdYI+Uzw3+Ts9pxrCpg7VfDPQSlK39qys4QPH6jasqv/uR7LNYOjddDw8gQLxYc1m3VQP+4Gvz94yNEeLz0h1AcKoBoJZO3kmoLiqao6MYlMLfB6RlurY5A441UtqrfmtTU+l6vSsL8PO+t2UXw4HwFQSEAaBiX7cVLvUvsPk2B2RNdVb9JB/TyUilRpHe8cV6ijV1yZlr5oqMR9vR1L3cN03c9SGgoqj/we1VG5d36ucsCui90H3gEWH2snMfWKh8TXR8xX04QbP2ssmUcj2WzK73iuBFmqcSpDcPHtLgAeRRbXvR3p4HfceZcsaD+SED4cIS4AbgJOEDieAQ4jIgFb3geebkBw1Oxq+D8pJl03zToGrYIoUfFSXTQbPt8gqBBTbHsbtDERbYS3f1n/hwhgHiboX9XJ04WNMkAP1QiHk2xZqe4Lvh06zgvOT5WbGEGf2WeumSOBkcwEMj4u5TQUvhJqwA
Unseal Key 2: wcBMA3BSIcJ3gMZmAQgAuMcRamfiG4cgdsuhkSjipWgyk0z3zDYlbflzOB2T9YwN6+1LhoPcK26N/RHwNsM/FDI67WYYGEhohDkisdBqhBQt7TCogsJjIoV0EpUiiqowxJH/rEr6AUQdgMSt5NbabzMypHyFz3SXKGijmZLqW66lfQ3aY9Hc1M4H/p/2BLQA9elI39UJbIElIP6NnNYGy7CDD/s6jyHeVSoqmC1E1ma6GyaQFEDUDhUzSbacW/dPjIqDcA8DIlHJDJKjZ4u7tKXQkr2dI2DWEpckc/i7W5+29c4PYG7P8+nS+xwV/1ydOJ+DLV6XJDGyOS8tBaLYOcIUY0d1S5jXtP8Sqf9ELdLgAeS2sm1ppwUrtkdZZJz46E+u4W3F4O/gF+F1PeA54qsbk7PgR+YcVdEa831YEzGcotCb7scie+lHLx5vCZPT5oRD2kBgUlXGctU64DfaoeTDska9Pq29e6Yb5h0a3CiqIWwbO7s24BjhtyXgseSsPs+1xPzP0sYD93Gateq54s1D4aTh3FgA
Unseal Key 3: wcBMA3BSIcJ3gMZmAQgA6MnGDWZMcUk9s9pxu5/cmE1DxFyKeAjJ54UApsPQy3IEWe72N8bFx0jzXYNQ13XZxfZmsQRDaVYBvcwTtUs29BP8OGb0RXJsFSJUzRW7bemA5DvWd6o3SgKKCYs1Qt65b9Ju5dJs4YZgjwXYttHtqT037EzRPIWgyHNyM+UlNe8JWk7SjPSXgtP0kuRY99cU8Ef/zkVXzuY++AvfgIEy3oAB5Js91p6McqMHHu7UTU+ouGd47UvIstWcUoxklgwcugT3hwjxzeWREdmSMSf8VufI1Kt7/5ZfeYBdX/OQnXn6te++p0EBq0bRswvTTxMonHrTZcV4Hcll20eQH2lOJtLgAeTR6TtdEaMXf5YqPf8WT4Su4YEy4HDg3eF6SuC94o5zmv7g8OahpuuvGeiNqNfVSL8uEc67kVaKXTGc6r08oNn4GDDo/NE/JAeYzv1RG5M1Z7u4HMFtsw93GgthjqDTW7nD8lji4PvhJMjgyuQUfdkOiwryYccVEiPGceBE4ppPkIvhtncA
Initial Root Token: b565ce84-b273-8644-76a5-ded5b9ff4a66
Mettons que l’on stocke quelques clés dans notre coffre puis que l’on stoppe notre serveur avant de le relancer.
Au moment où l’on va vouloir s’authentifier avec vault auth
on va tomber sur cette erreur :
Code: 503. Errors:
* Vault is sealed
Le processus ayant été arrêté puis relancé, le coffre est scellé. Si le processus est arrêté un peu sauvagement le coffre ne sera pas corrompu, il sera scellé également.
Avant de pouvoir l’utiliser de nouveau nous devons donc le desceller avec au moins 2 jetons. En premier lieu il nous faut déchiffrer nos jetons :
echo "wcBMA3BSI…" | base64 -D | keybase pgp decrypt
Une fois la clé récupérée on commence à desceller le coffre :
# vault unseal
Key (will be hidden):
Sealed: true
Key Shares: 3
Key Threshold: 2
Unseal Progress: 1
Unseal Nonce: 1a22bfb2-c3ad-c855-27e9-bc3d5c7ee066
On voit ici que le processus s’est bien passé et qu’il nous reste à refaire l’opération une fois (unseal progress). On répète l’opération avec la deuxième clé.
# vault unseal
Key (will be hidden):
Sealed: false
Key Shares: 3
Key Threshold: 2
Unseal Progress: 0
Unseal Nonce:
Le coffre n’est plus scellé à présent. On peut s’authentifier et l’utiliser normalement.
J’espère que ce premier article vous aura convaincu de l’intérêt de Vault, que ce soit pour l’utiliser pour partager des accès à des ressources ou pour le mettre en place dans une chaine de déploiement plus poussée.
Dans notre prochain article nous irons un peu plus loin pour se rapprocher de ce deuxième cas d’usage.
L’équipe Synbioz. Libres d’être ensemble.