Introduction

Le but de l’exercice était d’utiliser Vault pour stocker nos secrets qui sont habituellement dans des fichiers de configuration.
Je souhaitais également que le script puisse être lancer indépendamment depuis un serveur CICD (Jenkins dans notre cas) ou depuis mon laptop.

Pré-requis :

  • Un outil de CICD ou un scheduler ou ce que vous voulez pour lancer des jobs (je ferai référence à Jenkins dans le reste de l’article).
  • un script dans un langage quelconque (python ici).
  • Vault fonctionnel avec un accès privilégié.
  • Terraform (peut être fait via des commandes vault directement mais j’utilise Terraform pour populer Vault personnellement).

Configuration de Vault

Principe général

L’architecture cible de notre solution est de partir du principe que Vault est la seule source de vérité des secrets.
Le Jenkins sera considéré comme source fiable dans notre infrastructure et sera capable de générer des token pour d’autres applications.
Le Jenkins mettra ensuite à disposition le token précédemment généré pour récupérer les secrets nécessaires au bon fonctionnement du script.

Les policies

Nous avons besoin de plusieurs policies afin de rendre ce process possible :

  • Une policy pour Jenkins pour lui permettre de récupérer des credentials pour le script.
  • Une policy pour le script lui permettant de récupérer les secrets nécessaires.

Commençons par celle pour Jenkins :

path "auth/approle/role/python-grafana/role-id" {
  capabilities = ["read"]
}
path "auth/approle/role/python-grafana/secret-id" {
  capabilities = ["read","create","update"]
}

Cette dernière permet de récupérer le role-id qui est une chaine auto-générée et de générer des secret-id pour le role défini.

Ensuite pour la policy liée à notre approle :

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

Pour cette dernière, il suffit de lui donner les droits minimum nécessaire.
Dans mon cas il s’agit de lire les credentials pour accéder à Grafana.
Ce secret contiendra par exemple le nom d’utilisateur et le password admin qui permettront au script d’effectuer des actions sur Grafana.

L’authentification

Pour s’authentifier à Vault nous utiliserons un approle.
J’ai fait le choix d’utiliser un seul approle : celui pour mon script.
Par exemple mon script a pour but d’interagir avec Grafana j’ai donc quelque chose comme cela pour la configuration de mon approle :

resource "vault_auth_backend" "approle" {
  type = "approle"
}

resource "vault_approle_auth_backend_role" "roles" {
  backend        = vault_auth_backend.approle.path
  role_name      = "python-grafana"
  secret_id_ttl  = 3600
  token_num_uses = 10
  token_ttl      = 1200
  token_max_ttl  = 1800
  secret_id_num_uses = 40
  token_policies = ["python-grafana"]
}

Initialisation

Dans mon cas j’ai fait le choix de générer un token vault pour Jenkins et non d’utiliser un approle.
Cela me permet d’être plus flexible. Je peux facilement revoquer le token si nécessaire.
Pour cela : vault token create -policy jenkins

Avec cela Vault est normalement prêt.

Utilisation dans Jenkins

Pour ce qui est de Jenkins il faut maintenant enregistrer le token dans la partie credentials de jenkins.
J’utilise un simple user / password même si l’utilisateur n’est pas nécessaire (j’ai nommé ce dernier jenkins-vault).

Utilisation dans une pipeline

Définissons maintenant une pipeline afin d’utiliser ce que nous avons créer.
Mon script récupère les informations via variable d’environnement ce qui donne quelque chose comme cela :

pipeline {
    stages {
      stage("Running script") {
        steps {
          script {
            VAULT_ADDR = vault.domain.tld
            withCredentials([usernamePassword(credentialsId: "jenkins_vault", passwordVariable: 'VAULT_TOKEN',usernameVariable: 'user')]) {
                withEnv(["VAULT_TOKEN=${VAULT_TOKEN}", "VAULT_ADDR=${VAULT_ADDR}"]) {
                  def ROLE_ID = sh(
                      returnStdout: true,
                      script: "vault read -field=role_id auth/approle/role/python-grafana/role-id"
                  )
                  def SECRET_ID = sh(
                      returnStdout: true,
                      script: "vault write -f -field=secret_id auth/approle/role/python-grafana/secret-id"
                  )
                  sh """
                    ### HIDE OUTPUT
                    set +x
                    vault renew token
                    TOKEN=$(vault write -field=token auth/approle/login role_id=${ROLE_ID} secret_id=${SECRET_ID}")
                    export VAULT_TOKEN=${TOKEN}
                    ## Init env var required by script or we could output in a file
                    export GRAF_USER=$(vault kv get -field=username secret/grafana)
                    export GRAF_PASSWORD=$(vault kv get -field=password secret/grafana)
                    ## Run script with env var
                    python main.py 
                  """
                }
            }
          }
        }
      }
    }
}

Donc c’est assez complexe vu comme cela cependant cela permet d’avoir une pipeline relativement secure.
Il est possible de simplifier la chose en passant par les modules Jenkins qui peuvent extrapoler la partie génération du token pour l’application et ainsi diminuer la lourdeur de la pipeline.