C’est toujours une question importante et souvent oubliée lors des phases d’architecture ou même dans l’”Infrastructure as Code”. Trop souvent sur les phases de développement puis de tests, les mots de passe sont stockés sur des fichiers voire directement dans le code. C’est une solution temporaire qui fonctionne tant qu’un certain niveau de sécurité n’a pas besoin d’être activé. Mais comme toutes ces solutions “temporaires”, on s’en aperçoit lors des mises en production, voire au premier audit de sécurité.
L’objectif de ce tutoriel : Je veux déployer un Vault permettant de stocker des secrets puis les récupérer dans mon application. Je veux également attribuer des droits différents suivant l’application.
J’ai choisi de présenter mes déploiements via Docker Compose que je trouve plus facile à lire dans le cas d’un article. Docker Compose n'est pas un outil à utiliser en production par contre !
Vault est une des solutions de la société HashiCorp.
Il faut savoir que Vault est juste un service permettant d’échanger des données qu’il chiffre pour les stocker, mais il ne stocke rien lui-même ! Il a donc besoin d’un backend de stockage.
Ce backend a un rôle très important : il doit assurer la disponibilité et la résilience des données. Il doit donc vérifier lui-même ces critères.
Pour ce tutoriel, j’ai choisi une solution qui est également éditée par HashiCorp : Consul. Pour plus d’informations : Consul, expliqué.
Regardons maintenant le déploiement de Consul.
Ce déploiement est présenté ici en exemple mais il ne respecte pas les normes de sécurité établies par Hashicorp dans leurs bonnes pratiques. Cela fera l’objet d’un article prochainement mais ce n’est pas le sujet ici.
consul-master-1:
image: consul:0.9.0
command: agent -server -bootstrap-expect=3 -datacenter=local1 -node=consul-master-1 -bind='' -client=0.0.0.0 -ui
ports:
- 8500:8500
environment:
- 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true, "acl_datacenter":"local1", "acl_master_token":"${CONSUL_MASTER_TOKEN}", "acl_default_policy": "deny" }'
volumes:
- consul-1:/consul/data
networks:
- netgate
consul-master-2:
image: consul:0.9.0
entrypoint: consul
command: agent -server -retry-join=consul -datacenter=local1 -node=consul-master-2 -bind='' -data-dir=/consul/data -client=0.0.0.0 -dns-port=53 -recursor=8.8.8.8
depends_on:
- consul-master-1
environment:
- 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true, "acl_datacenter":"local1", "acl_master_token":"${CONSUL_MASTER_TOKEN}", "acl_default_policy": "deny" }'
volumes:
- consul-2:/consul/data
links:
- consul-master-1:consul
networks:
- netgate
consul-master-3:
image: consul:0.9.0
entrypoint: consul
command: agent -server -retry-join=consul -datacenter=local1 -node=consul-master-3 -bind='' -data-dir=/consul/data -client=0.0.0.0 -dns-port=53 -recursor=8.8.8.8
depends_on:
- consul-master-1
environment:
- 'CONSUL_LOCAL_CONFIG={"skip_leave_on_interrupt": true, "acl_datacenter":"local1", "acl_master_token":"${CONSUL_MASTER_TOKEN}", "acl_default_policy": "deny" }'
volumes:
- consul-3:/consul/data
links:
- consul-master-1:consul
networks:
- netgate
Comme vous pouvez le constater, je crée un cluster de 3 Consul pour la résilience et la haute disponibilité. J’active également les ACL qui me permettent, à l’aide de la policy par défaut deny
, de rendre mon Consul non accessible en lecture sans le token adéquat.
J’utilise une variable d’environnement CONSUL_MASTER_TOKEN que j’ai générée précédemment. À partir de Consul 0.9.1, il est possible de demander à Consul de générer ce token grâce à la route “/v1/acl/bootstrap”. Cela évite de générer un token qui pourrait être présent dans vos logs (Jenkins par exemple).
Vault stocke ses secrets dans la partie base clefs/valeurs de Consul.
Note : il me sera de toute façon impossible, même en possédant ce token Consul, de lire les secrets via Consul ! Vault est bien là pour protéger ses secrets et donc ceux-ci apparaîtront chiffrés.
Toujours grâce à Docker Compose, je vais déployer mon Vault :
vault:
image: vault:0.7.3
command: server
ports:
- 8200:8200
cap_add:
- IPC_LOCK
depends_on:
- consul-master-1
environment:
- 'VAULT_LOCAL_CONFIG={"backend":{"consul":{"address":"consul:8500", "scheme":"http", "service":"vault-service"}}, "listener":{"tcp":{"address":"0.0.0.0:8200", "tls_disable":"1"}}}'
# - 'VAULT_DEV_ROOT_TOKEN_ID=97CFFBCE-18EE-42F7-B60E-E69F3EAE0E32'
- 'VAULT_ADDR=http://127.0.0.1:8200'
- "CONSUL_HTTP_TOKEN=${CONSUL_MASTER_TOKEN}"
- 'VAULT_REDIRECT_ADDR=http://127.0.0.1:8201'
volumes:
- vault:/vault/file
links:
- consul-master-1:consul
networks:
- netgate
Dans ce déploiement je configure mon backend de stockage et puisqu’il s’agit de Consul, un service registry, je crée un service “vault-service”. C’est très pratique : dans mon application je n’ai plus qu’à demander à Consul les données de ce service pour y accéder.
En commentaire dans ce document on peut définir un token à Vault à condition de préciser qu’il s’agit d’une version de développement (“-dev” dans la commande). Je reviendrai tout à l’heure sur la raison d’être de ce “root” token.
Pour que Vault puisse communiquer avec Consul malgré l’activation des ACL, il faut lui préciser le token : "CONSUL_HTTP_TOKEN=${CONSUL_MASTER_TOKEN}".
Ici, j’utilise le master token de Consul, mais un token permettant l’écriture dans la base clefs/valeurs et l’ajout de service suffiraient.
Pour l’instant, vous ne pouvez que constater l’apparition dans Consul de votre service Vault. Mais Vault dans cet état est inutilisable, sauf dans le cas où vous l’avez lancé en mode “développement”. Dans ce cas, vous pouvez sauter la prochaine section. Le token se trouve soit dans votre code de déploiement (voir le chapitre précédent) soit dans les logs de l’instance.
Il existe une CLI Vault pour réaliser les commandes suivantes. J’ai choisi de montrer les appels webservices pour une meilleure compréhension de tous les appels.
Il va falloir initialiser Vault pour générer vos tokens. Regardons la séquence complète.
secrets=$(curl -X PUT -d "{\"secret_shares\":1, \"secret_threshold\":1}" http://localhost:8200/v1/sys/init -s)
root_token=$(echo $secrets | jq -r '.root_token')
key_1=$(echo $secrets | jq -r '.keys[0]')
echo "ROOT_TOKEN: $root_token"
echo "FIRST_KEY: $key_1"
data=$(curl -X PUT -d "{\"key\": \"$key_1\"}" http://localhost:8200/v1/sys/unseal -s)
data_sealed=$(echo $data | jq -r '.sealed')
echo "Vault sealed: $data_sealed"
La première commande vous permet de générer des secrets permettant de sceller ou de désceller votre Vault.
La notion de scellement dans Vault permet de protéger vos données en les rendant inaccessibles en lecture et en écriture. Par exemple, lors d’une attaque de votre application, vous pouvez sceller votre Vault et plus personne n’y aura accès, même vos applications.
Cela implique aussi qu’un redémarrage du démon vous obligera à désceller à nouveau votre Vault. Il vous faudra désceller votre Vault pour retrouver l’état initial. Si vous utilisez Consul comme backend de stockage vous pouvez constater que le service n’est indiqué comme accessible que si le Vault est déscellé.
Ici la commande ne me renvoie qu’une clef car je ne demande qu’une seule clef. C’est une stratégie d’entreprise ou d’équipe que de définir le nombre de clefs et comment elles sont réparties dans l’équipe. Vous pouvez également consulter sur le site de Vault les bonnes pratiques sur la question.
Dans la variable “secrets”, j’ai :
{
"keys": ["e5621a61c4...ad0aee3b45ea"],
"keys_base64": ["5WIaYcT...KtCu47Reo="],
"root_token": "bccce153-63cc-bcf2-eed7-c3c5d62960f4"
}
Le “root_token” est le token “maître” qui me permet pour l’instant de tout faire.
Je peux ensuite désceller mon Vault avec ma seul clé via “/v1/sys/unseal”. Si j’avais demandé plusieurs clefs j’aurais juste eu à exécuter cet appel une fois par clef.
Mon Vault est maintenant “déscellé”.
Quelques définitions pour commencer :
Deux stratégies peuvent être mises en place au niveau des applications. Soit on fournit un token au démarrage de l’application, mais celui-ci arrivant à expiration l’application ne pourra plus se connecter (donc un pirate dans cette application non plus). Soit on fournit à l’application le moyen de se connecter à un rôle et celle-ci pourra donc renouveler son token.
Il faut activer une configuration de Vault pour utiliser des rôles applicatifs :
curl -X POST -H "X-Vault-Token:$root_token" -d '{"type":"approle"}' http://localhost:8200/v1/sys/auth/approle
Note : le root_token correspond à celui trouvé ci dessus.
Je veux créer un rôle pour mon Infrastructure as Code, il doit pouvoir écrire dans mon Vault les mots de passe qu’il a générés.
Je commence par écrire une policy dans iac-policy.json.
{
"rules": "path \"secret/*\" { capabilities = [\"create\", \"list\"]}"
}
Mon rôle aura donc les droits de créer et de lister tous mes secrets.
Je crée ma policy dans Vault.
curl -X POST -H "X-Vault-Token:$root_token" --data @policy/iac-policy.json http://localhost:8200/v1/sys/policy/iac-policy
Puis je crée le rôle :
curl -X POST -H "X-Vault-Token:$root_token" -d '{"policies":"default, iac-policy"}' http://localhost:8200/v1/auth/approle/role/iacrole
iacrole=$(curl -X GET -H "X-Vault-Token:$root_token" http://localhost:8200/v1/auth/approle/role/iacrole/role-id -s)
iac_role_id=$(echo $iacrole | jq -r '.data.role_id')
echo "IaC role_id: $iac_role_id"
La première commande me permet de créer le rôle auquel j’attribue ma policy “iac-policy” définie précédemment.
La deuxième commande me permet de récupérer l’identifiant de mon rôle.
Je dois maintenant créer un secret pour ce rôle afin qu’il puisse se connecter à Vault.
iaclogin=$(curl -X POST -H "X-Vault-Token:$root_token" http://localhost:8200/v1/auth/approle/role/iacrole/secret-id -s)
iac_secret_id=$(echo $iaclogin | jq -r '.data.secret_id')
echo "IaC secret_id: $iac_secret_id"
Puis je me connecte pour récupérer un token pour mon application.
iactoken=$(curl -X POST -d "{\"role_id\":\"$iac_role_id\",\"secret_id\":\"$iac_secret_id\"}" http://localhost:8200/v1/auth/approle/login -s)
iac_client_token=$(echo $iactoken | jq -r '.auth.client_token')
echo "Vault token for iacrole: $iac_client_token"
Je peux maintenant me connecter pour insérer un secret, ici un login/password pour un MongoDB me permettant de stocker des joueurs.
curl -X POST -H "X-Vault-Token:$iac_client_token" -d "{\"login\":\"$MONGO_PLAYER_LGN\", \"password\":\"$MONGO_PLAYER_PWD\"}" http://localhost:8200/v1/secret/playerdb
Je veux créer un rôle pour mon microservice “player”. Contrairement à mon rôle lié à l’IaC, je ne veux que des droits en lecture sur un secret précis.
{
"rules": "path \"secret/playerdb\" { capabilities = [\"read\", \"list\"]}"
}
Puis, comme pour mon rôle IaCRole, je dois créer dans l’ordre :
# create player-policy
curl -X POST -H "X-Vault-Token:$root_token" --data @policy/player-policy.json http://localhost:8200/v1/sys/policy/player-policy
# create playerrole
curl -X POST -H "X-Vault-Token:$root_token" -d '{"policies":"default, player-policy"}' http://localhost:8200/v1/auth/approle/role/playerrole
playerrole=$(curl -X GET -H "X-Vault-Token:$root_token" http://localhost:8200/v1/auth/approle/role/playerrole/role-id -s)
player_role_id=$(echo $playerrole | jq -r '.data.role_id')
echo "Player role_id: $player_role_id"
# create a secretid for playerrole
playerlogin=$(curl -X POST -H "X-Vault-Token:$root_token" http://localhost:8200/v1/auth/approle/role/playerrole/secret-id -s)
player_secret_id=$(echo $playerlogin | jq -r '.data.secret_id')
echo "Player secret_id: $player_secret_id"
# login with playerrole and get token
playertoken=$(curl -X POST -d "{\"role_id\":\"$player_role_id\",\"secret_id\":\"$player_secret_id\"}" http://localhost:8200/v1/auth/approle/login -s)
echo $playertoken
player_client_token=$(echo $playertoken | jq -r '.auth.client_token')
echo "Vault token for playerrole: $player_client_token"
On peut vérifier la bonne création et les droits de l’utilisateur en testant le token.
test=$(curl -X GET -H "X-Vault-Token:$player_client_token" http://localhost:8200/v1/secret/playerdb -s)
test_data=$(echo $test | jq -r '.data')
echo "Test: $test_data"
Je dois récupérer mon login/password défini à la fin de la création du rôle IaCRole.
Lors du lancement de l’application, il suffit de préciser le lien vers le Consul ainsi que notre token créé dans Vault.
player:
image: slavayssiere/player:0.1
external_links:
- consul-master-1:consul
- playerdb:playerdb
- vault:vault
environment:
- CONSUL_HOST=consul:8500
- 'CONSUL_HTTP_TOKEN=${CONSUL_MASTER_TOKEN}'
- MONGO_HOST=playerdb
- 'VAULT_HOST=http://vault:8200'
- 'VAULT_TOKEN=${VAULT_PLAYER_TOKEN}'
labels:
- "traefik.backend=player"
- "traefik.frontend.rule=Host:player.localhost"
networks:
- ext_netgate
Dans mon application, si je veux utiliser Vault, je dois :
test=$(curl -X GET -H "X-Vault-Token:$player_client_token" http://localhost:8200/v1/secret/playerdb -s)
test_data=$(echo $test | jq -r '.data')
echo "Test: $test_data"
Vault est une excellente solution pour stocker mes données de connexion et mes clefs dans un environnement microservice.
Une fois passée une prise en main un peu complexe, il est très facile de protéger ses informations dès les environnements de développement.
Enjoy ;-)