Nous vous avons déjà parlé de l’outil HashiCorp Vault pour la centralisation des secrets statiques, dynamiques ou encore pour l’Encryption as a Service. Dans cet article, nous allons nous intéresser à Vault Agent, qui va nous permettre de réduire la dépendance de HashiCorp Vault au niveau du code. Ce premier article est une introduction au sujet, avant de voir en détail comment intégrer Vault Agent dans une application.

Prérequis

Précision : au moment où nous écrivons cet article, Vault est en version 1.6.2.

Si vous ne connaissez pas encore Vault, je vous invite à essayer le tutoriel interactif sur le site officiel Vault d’HashiCorp afin de vous faire une première idée.

Enfin, cet article fait suite à la trilogie d’articles sur comment faire disparaître les secrets applicatifs avec Vault et Terraform :

Découverte du Vault Agent

Qu’est ce que Vault Agent ?

Vault Agent n’est autre que le binaire Vault lancé en mode Agent. Il permet en l’occurrence de bénéficier de certaines fonctionnalités nous facilitant l’interaction avec Vault, notamment :

  • Auto-Auth: Prend en charge l’authentification avec Vault définie par une méthode. Une fois authentifié, celui-ci dépose le token Vault dans un Sinks.
    • Methods: Il s’agit de la configuration côté Vault Agent nous permettant de nous authentifier avec Vault. A savoir que TOUTES les méthodes d’authentification qu’on l’on retrouve côté Vault ne sont pas forcément disponibles via le Vault Agent. Si le token Vault expire, Vault agent est en capacité de renouveler celui-ci.
    • Sinks: Il s’agit de la configuration permettant d’indiquer à l’Agent où déposer le token Vault ou encore chiffrer celui-ci si besoin. Pour le moment, seul File (stockage fichier) est supporté.
  • Caching: Permet de mettre en cache les réponses de Vault (token, lease, etc). Le caching est utilisé le plus souvent lors d’un usage plus intensif de Vault et n’entrera pas le cadre de cet article.
  • Templating: Anciennement Consul-template, le templating permet de transformer un fichier template source en un fichier de destination contenant nos secrets et a lieu une fois que le token Vault a été généré par l’Auto-Auth. Le templating se base sur Consul Template markup. Enfin, celui-ci permet de gérer le renouvellement dynamique de nos secrets en remettant à jour notre fichier de destination (toujours en se basant sur le template).

Vault Agent, pourquoi ?

Dans les précédents articles, nous avons vu comment intégrer Vault dans une application. Cependant, dans chacune des actions entreprises, nous avons constaté que l’intégration pouvait impacter le code et créer des dépendances avec Vault.

On note deux dépendances :

  1. Au niveau de l’authentification et renouvellement du token Vault :
    1. Premièrement, l’application doit être “consciente” de la méthode d’authentification (plus d’information sur l’article: Comment choisir sa méthode d’authentification) afin d’obtenir son token.
    2. Deuxièmement, le token fourni par Vault a une durée d’expiration et l’application doit être en capacité de renouveler celui-ci si besoin
  2. Si le secret est dynamique, l’application doit être en capacité de le renouveler.

Pour résumer, nous avons donc les dépendances suivantes, en prenant l’exemple d’une application sur AWS :

Vault dependencies

Vault Agent permet de s’affranchir des dépendances. Il prend en charge les différentes tâches, notamment:

  • Auto-Auth: S’authentifier auprès de Vault et renouveler le token fourni par Vault si celui-ci expire (si spécifié)
  • Template: Récupérer les secrets et renouveler les secrets dynamiques via le token généré par la méthode Auto-Auth. Anciennement Consul-template

Si nous reprenons l’exemple précédent, voici ce que ça donnerait : Vault agent solution

Le Vault Agent nous permet dans un premier temps de s’authentifier auprès du Vault avec la méthode d’authentification AWS et de renouveler le token si la durée de vie de celui-ci arrive à expiration.

Dans un second temps, il permet de récupérer les secrets pour les déposer dans un fichier template et enfin renouveler ses secrets si besoin.

Si vous êtes sur Kubernetes, il est possible de passer par le Vault Agent Sidecar Injector, sujet qui ne sera pas abordé ici.

Mise en place de notre environnement

Afin de rendre l’intégration la plus transparente possible pour notre application, notre intégration du Vault Agent doit se faire en 2 parties:

  1. Mettre en place la méthode d’authentification et valider la récupération du token Vault. Au besoin, renouveler celui-ci
  2. Récupérer les secrets ainsi que le templating de notre fichier contenant nos secrets. Au besoin, vérifier que nos secrets se renouvellent

Dans notre exemple, nous allons nous baser sur :

  • Vault en version 1.6.2
  • Terraform en version 0.14.6
  • Une base de données qui sera ici MySQL en version 5.7
  • Une application web PHP 7.2 et Apache

Vous trouverez les sources permettant de reproduire cet environnement ici.

L’environnement se base sur des containers docker afin que vous puissiez reproduire l’exemple de votre côté.

Concernant la stack docker (docker-compose.yml, app.yml, etc), vous pouvez en retrouver les détails dans les précédents articles ou encore dans le readme du projet.

Intégrer le Vault Agent pour la méthode d’authentification

Commençons par installer Vault Agent.

Pour ce faire, il suffit de télécharger le binaire Vault ou de le packager avec l’application. Vous trouverez toutes les instructions d’installation dans la documentation officielle HashiCorp.

Dans notre cas, nous ajoutons celui-ci dans notre dockerfile pour télécharger le Vault Agent au build de notre image.

Une fois l’installation faite, nous pouvons nous pencher sur l’authentification qui est de type Approle avec le Vault Agent.

Dans notre cas, dans un cadre de test, nous allons injecter directement le role_id et secret_id dans notre container via les variables d’environnements. Dans le cas d’un pipeline ou encore une CI/CD, l’implémentation de la méthode Approle de façon sécurisée est plus technique et mériterait un article entier sur le sujet. Vous pouvez retrouver un HashiCorp Learn sur le sujet AppRole With Terraform & Chef.

Pour que notre Vault Agent puisse s’authentifier en Auto-Auth nous devons lui fournir un fichier de configuration comprenant: la méthode d’authentification et le Sinks (où doit-il déposer le token Vault ?), actuellement n’ayant que pour seule possibilité un fichier.

Dans notre cas, nous aurons la configuration suivante :

pid_file = "./pidfile"

auto_auth {
  method "approle" {
    config = {
      role_id_file_path = "/root/role-id"
      secret_id_file_path = "/root/.secret-id"
      remove_secret_id_file_after_reading = true
    }
  }

  sink "file" {
    config = {
      path = "/var/www/.vault-token"
      mode = 0644
    }
  }
}

template {
  source = "/var/www/secrets.tpl"
  destination = "/var/www/secrets.json"
}

Côté method Approle, nous devons:

  • Préciser où se situe le role_id et secret_id. Dans notre cas, ces valeurs sont passées en variable d’environnement cependant Vault Agent n’est en mesure de lire ces valeurs que via un fichier. Il nous faudra donc, au niveau de l’entrypoint, créer ces fichiers avec les valeurs attendues.
  • L’option remove_secret_id_file_after_reading permet de supprimer le secret_id après la lecture du fichier par le Vault Agent. Une méthode sécurisée afin de garantir que personne d’autre que le Vault Agent soit en capacité de s’authentifier à nouveau.

Côté sink, la configuration est plutôt simple:

  • Le path sur lequel le Vault Agent ira écrire le token Vault.
  • Le mode sur lequel le Vault Agent ira écrire les droits du fichier. Ici nous avons mis 644 pour que le serveur apache soit en capacité de lire le token. Nous pouvons aussi très bien mettre 600 afin de garantir que seul le Vault Agent est en capacité de lire et modifier celui-ci.

Enfin, les derniers éléments de notre configuration:

  • L’adresse du Vault: nous passons la variable d’environnement VAULT_ADDR contenant l’adresse de notre Vault. Le Vault Agent est en capacité de se référer à celle-ci. Dans le cas inverse, il faudra renseigner l’adresse du Vault directement dans le fichier de configuration.
  • pid_file: est le path sur lequel l’ID du process Vault Agent est écrit. Sachant que le Vault Agent fonctionnera en background, il peut être pertinent de se référer à ce fichier afin de kill le process si besoin.

Afin de finaliser notre intégration, nous devons rajouter un entrypoint à notre container afin de:

  • Provisionner les fichiers role_id et secret_id au travers de nos variables d’environnement
  • Démarrer le processus du Vault Agent en background
  • Et enfin lancer notre serveur apache

Ce qui nous donne comme entrypoint :

#! /bin/bash

echo "$VLT_ROLE_ID" >> /root/role-id
echo "$VLT_SECRET_ID" >> /root/.secret-id


vault agent -config=/root/config.hcl &
apache2-foreground

A ce stade, le Vault Agent est en mesure de s’authentifier auprès de Vault et de renouveler le token Vault.

Intégrer le Vault Agent pour la récupération de secrets

Comme nous l’avons vu précédemment, le Vault Agent se base sur un fichier template afin de transformer celui-ci en fichier contenant nos secrets.

Pour notre template, nous allons récupérer notre secret dynamique de type database mais il est tout à fait possible de faire la même chose pour les autres secrets dynamiques ou statiques.

Voulant que les secrets soit stockés dans un format JSON, nous avons donc le template suivant :

{
  {{ with secret (env "VAULT_PATH") }}
  "username":"{{ .Data.username }}",
  "password":"{{ .Data.password }}"
  {{ end }}
}

Nous récupérons la valeur de VAULT_PATH, une variable d’environnement qui contient comme valeur database/creds/web, étant notre path afin de récupérer notre secret.

Vault Agent devrait, en se basant sur notre template, nous allons créer notre fichier de secret qui devrait ressembler à ceci :

{
  "username":"vault-1430158508-126",
  "password":"132ae3ef-5a64-7499-351e-bfe59f3a2a21"
}

Si vous souhaitez en savoir plus sur le langage de templating utilisé par Vault Agent, vous pouvez vous référer à la documentation de Consul Template.

Il nous reste plus qu’à indiquer à notre Vault Agent, dans son fichier de configuration, le fichier source template et le fichier de destination des secrets :

template {
  source = "/var/www/secrets.tpl"
  destination = "/var/www/secrets.json"
}

Le fichier de template des secrets ainsi que le fichier de configuration du Vault Agent doivent être copiés au sein du dockerfile.

Pour finir, il nous reste plus qu’à indiquer à notre application de lire le fichier de secret et de stocker les secrets en variables si ce n’est pas déjà le cas :

if (file_exists("/var/www/secrets.json")) {
  $secrets_json = file_get_contents("/var/www/secrets.json", "r");
  $user = json_decode($secrets_json)->{'username'};
  $pass = json_decode($secrets_json)->{'password'};
}
else{
  echo "Secrets not found.";
  exit;
}

Tester notre exemple

Une fois que le Vault Agent est en capacité de s’authentifier et de récupérer ses secrets, il nous reste plus qu’à tester notre application. Le README du projet explique en détail chaque étape du test et un Makefile est à votre disposition.

Commençons par notre infrastructure :

$ docker run --rm -v $(pwd)/terraform:/app/ -w /app/ hashicorp/terraform:light init
$ docker-compose up

L’infrastructure étant opérationnelle, nous pouvons accéder à notre Vault via cette addresse: http://127.0.0.1:8200

Du côté de notre application :

$ docker-compose -f app.yml build
$ role_id=$(docker run --rm -v $(pwd)/terraform:/app/ -w /app/ hashicorp/terraform:light output -raw approle_role_id)
$ secret_id=$(docker run --rm -v $(pwd)/terraform:/app/ -w /app/ hashicorp/terraform:light output -raw approle_secret_id)
$ docker-compose -f app.yml run -e VLT_ROLE_ID=$role_id -e VLT_SECRET_ID=$secret_id --service-ports web

L’application est maintenant disponible sur l’adresse suivante: http://127.0.0.1:8080

La page suivante devrait apparaître : Vault resultat 1

Si nous rafraichissons la page, seule la valeur chiffrée via l’EaaS (Encryption as a Service) de Vault change et notre application ré-utilise bien notre secret de base de donnée : Vault resultat 2

Pour tester le bon fonctionnement de la rotation des secrets et du token Vault, nous pouvons révoquer les leases : Vault lease approle côté token Vault: auth/approle/login

Vault lease database côté secrets Vault: database/creds/web

Et voir ainsi les logs côtés Vault Agent : Vault logs

Comme nous pouvons le voir:

  • Si le token Vault expire : le Vault Agent s’authentifie à nouveau
  • Si les secrets expirent : le Vault Agent récupère les nouveaux secrets puis met à jour notre fichier de secret

Enfin, à des fins de nettoyage, n’oubliez pas d’exécuter les commandes suivantes :

$ docker-compose down
$ docker-compose -f app.yml down
$ rm terraform/terraform.tfstate

Ce qu’il faut retenir

  • Si votre application consomme ses secrets au travers d’un fichier, alors l’utilisation du Vault Agent permet d’intégrer Vault de façon transparente. Sinon, si celui-ci utilise les variables d’environnement il existe une possibilité d’intégration avec envconsul. Pour ceux qui souhaitent tester cette méthode, vous pouvez consulter le repository GitHub qui se base sur notre exemple.
  • Le Vault Agent s’occupe de la rotation de vos secrets (si ils sont dynamiques) et du token Vault.
  • Le Vault Agent se sert d’un fichier template pour créer le fichier contenant nos secrets. Vous pouvez en apprendre plus sur la documentation de Consul Template.
  • Nous ne modifions plus le code de notre application pour intégrer Vault a l’exception de l’EaaS (Encryption as a Service). L’application est en capacité de récupérer un token Vault qui est renouvelé par le Vault Agent pour l’EaaS.
  • Dans notre exemple, nous utilisons un rôle dynamique pour le secret engine Database. Il est tout à fait possible d’utiliser des rôles statiques si l’on souhaite éviter de générer un grand nombre d’utilisateurs de base de donne a la volée.
  • Nous avons modifié l’entrypoint de notre application pour y lancer le Vault Agent mais il est possible de le lancer au démarrage de notre container via systemd ou autre system/service manager afin d’éviter de toucher à l’entrypoint.

Comme nous l’avons vu dans cet article, nous avons retiré les dépendances entre notre code applicatif et HashiCorp Vault au travers du Vault Agent. Ceci apporte aussi une rotation automatique du token Vault et des secrets faite par le Vault Agent.

L’intégration du Vault au sein de notre application devient transparente et de même pour la gestion des secrets par les différentes équipes (dev, ops, etc).

Cependant, afin de rendre l’intégration du Vault transparente de bout en bout, il reste une question que nous n’avons pas abordée dans cet article : Comment rendre l’intégration de Vault transparente pour le déploiement d’une application via un pipeline ?

Ce point fera certainement l’objet d’un autre article !