LightcodeSysadmin Open Source

Utiliser Vault depuis vos pipelines Gitlab CI

Gitlab simplifie l’authentification à Vault depuis les pipelines. Cet article explique pas à pas comment configurer cette fonctionnalité afin d’accéder à vos secrets de manière sécurisée depuis vos pipelines.

Introduction

Lorsque vous créez des pipelines dans Gitlab CI, il est souvent nécessaire d’accéder à des secrets, comme des identifiants de connexions à des services externes. Gitlab intègre un système de secret simple pour répondre à ce besoin. Néamoins, Hashicorp Vault proprose des fonctionnalités bien plus avancées. Je vais donc vous montrer comment les deux outils peuvent s’intégrer ensemble. Cette fonctionnalité est disponible dans toutes les versions de Gitlab, y compris la version gratuite.

Gitlab vous permet de vous authentifier auprès de Vault en générant un JSON Web Token (JWT). Lors de l’exécution d’un pipeline, Gitlab génère un token puis le stocke dans la variable d’environnement CI_JOB_JWT. Après avoir configuré Vault, il est possible d’utiliser ce token pour vous authentifier auprès de Vault afin de récupérer un token Vault. Enfin, le token Vault sera utilisé pour récupérer des secrets dans Vault.

Le token JWT contient ses champs une fois décodé :

{
  "namespace_id": "1",
  "namespace_path": "demo",
  "project_id": "42",
  "project_path": "demo/demo-gitlab-vault",
  "user_id": "1",
  "user_login": "lightcode",
  "user_email": "lightcode@example.com",
  "pipeline_id": "1337",
  "job_id": "1984",
  "ref": "master",
  "ref_type": "branch",
  "ref_protected": "true",
  "jti": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
  "iss": "gitlab.example.com",
  "iat": 1000000000,
  "nbf": 1000000000,
  "exp": 1000000000,
  "sub": "job_1984"
}

Ces champs vont permettre à Vault de valider ou non la demande d’authentification en fonction des contraintes que vous allez ajouter.

Configuration de Vault

Dans cette partie, je vais expliquer pas à pas comment configurer Vault en utilisant Terraform afin de garder une approche Infrastructure as Code. Vous pourrez reproduire assez simplement cette configuration en reprenant les morceaux de code de cet article.

La première étape consiste à activer le backend d’authentification JWT de Vault et de configurer un endpoint pour Gitlab :

# Enable JWT backend
resource "vault_auth_backend" "jwt" {
  type = "jwt"
}

# Configure JWT backend with the Gitlab URL
resource "vault_jwt_auth_backend" "gitlab" {
  path         = "gitlab-jwt"
  jwks_url     = "${var.gitlab_url}/-/jwks"
  bound_issuer = var.gitlab_host
}

La configuration de l’endpoint d’authentification nécessite deux paramètres :

  • jwks_url : URL contenant les certificats permettant de vérifier la validité des tokens JWT. JWKS signifie “JSON Web Key Set”. Gitlab expose une URL contenant les clés utilisée pour signer le token et valider qu’il a bien été émis par Gitlab.
  • bound_issuer : cette chaîne est comparée avec le champs iss du token JWT et contient le nom DNS d’instance Gitlab.

Nous allons ensuite créer un nouveau rôle qui sera utilisé lors de l’authentification. Le rôle est configuré avec une ou plusieurs policies permettant de définir les actions qui pourront être faites une fois connecté à Vault. Dans notre cas, j’ai ajouté une policy pour pouvoir lire des secrets dans un chemin de démo.

# Role for our demo pipeline
resource "vault_jwt_auth_backend_role" "read_secret_demo" {
  backend        = vault_jwt_auth_backend.gitlab.path
  role_name      = "read-secret-demo"
  role_type      = "jwt"
  token_policies = [vault_policy.read_secret_demo.name]
  user_claim     = "user_email"
  bound_claims = {
    project_id = var.gitlab_project_id
    ref        = "master"
    ref_type   = "branch"
  }
}

Notre rôle est associé au backend JWT créé précédement. Il est associé avec une policy que je décrirai dans le paragraphe suivant. Le champ user_claim permet de spécifier quel champ Vault doit utiliser pour créer son alias d’authentification.

Le champ bound_claims permet de valider certains champs du token JWT. Tous les champs spécifiés dans les bound_claims doivent être présents dans le token et avoir les mêmes valeurs. Dans notre exemple, nous validons que l’id du projet Gitlab, nous vérifions que l’utilisateur est bien sur la branche master. Si nous essayons d’exécuter le pipeline sur une autre branche, l’authentification va échouer.

Nous allons ensuite créer une policy simple permettant de lire les secrets présents dans le chemin kv/demo/ :

# Policy to allow our role to read secrets in kv/demo/*
resource "vault_policy" "read_secret_demo" {
  name   = "read-secret-demo"
  policy = <<EOT
path "kv/demo/*" {
  capabilities = ["read"]
}
EOT
}

Les policies Vault permettent de définir les actions qu’un rôle/utilisateur a le droit de faire. Une policy peut inclure plusieurs chemins avec la liste des actions autorisées sur chacun de ces chemins.

Enfin, pour la démo, nous allons créer un secret dans Vault :

# Demo secret in the KV store
resource "vault_generic_secret" "demo_secret" {
  path      = "kv/demo/hello"
  data_json = <<EOT
{
  "username": "foo",
  "password": "bar"
}
EOT
}

Utilisation depuis un pipeline

L’authentification auprès de Vault se fait avec la commande suivante :

export VAULT_TOKEN="$(vault write -field=token auth/gitlab-jwt/login role=read-secret-demo jwt=$CI_JOB_JWT)"

Le chemin auth/gitlab-jwt/login est celui spécifié lors de la création du backend JWT. Le rôle que vous devez spécifier est celui qui a été créé précédement. Enfin, le paramètre jwt correspond au token que Gitlab va vous donner lors de l’exécution du pipeline, nous le reprennons tel quel grâce à la variable $CI_JOB_JWT.

Le pipeline complet ressemple à ceci :

demo:
  image: vault:1.6.0
  script:
    - export VAULT_ADDR=https://vault.example.com
    - export VAULT_TOKEN="$(vault write -field=token auth/gitlab-jwt/login role=read-secret-demo jwt=$CI_JOB_JWT)"
    - USERNAME="$(vault kv get -field=username kv/demo/hello)"
    - PASSWORD="$(vault kv get -field=password kv/demo/hello)"
    - echo "The secret is ${USERNAME}:${PASSWORD}"
  only:
    refs: ["master"]

Quand nous exécutons le pipeline, nous pouvons constater qu’il est capable d’accéder à Vault et de récupérer notre secret :

$ export VAULT_ADDR=https://vault.example.com
$ export VAULT_TOKEN="$(vault write -field=token auth/gitlab-jwt/login role=read-secret-demo jwt=$CI_JOB_JWT)"
$ USERNAME="$(vault kv get -field=username kv/demo/hello)"
$ PASSWORD="$(vault kv get -field=password kv/demo/hello)"
$ echo "The secret is ${USERNAME}:${PASSWORD}"
The secret is foo:bar

Conclusion

Cet article nous a permis de voir comment intéragir avec Vault depuis vos pipelines Gitlab CI. Vous pouvez retrouver l’intégralité du code sur mon Github.

Voici quelques liens qui m’ont aidé à rédiger cet article :