Sécuriser son déploiement Terraform sur AWS via Gitlab-CI et Vault – partie 3

Temps de lecture : 8 minutes

Comment sécuriser son déploiement Terraform sur AWS au travers de Gitlab-CI et de l’aide de Vault ? Nous avons vu dans les précédents articles la problématique des déploiements en CI/CD sur le Cloud, puis la résolution de ces problématiques par la génération de secrets dynamiques par Vault et l’authentification du pipeline Gitlab-CI. Dans ce troisième et dernier article, nous aborderons la récupération des secrets du côté de l’application.

Le challenge côté applicatif

Nous avons pu voir dans la précédente section comment résoudre les différentes problématiques rencontrées au niveau du pipeline. Cependant notre CI avec Gitlab ne se limite pas au déploiement de l’infrastructure, mais inclut aussi l’application en elle-même.

Si nous faisons un récapitulatif sur ce que nous avons réussi à faire jusqu’à maintenant au niveau workflow, nous avons le schéma suivant :

Comme nous pouvons le voir, notre application a besoin de récupérer les secrets stockés dans Vault pour pouvoir se connecter à la base de données.

Cependant, nous voulons que l’interaction avec l’application et Vault soit la plus transparente possible et pour cela nous avons besoin de réduire les dépendances :

  • Au niveau de l’authentification: quelle méthode choisir avec notre application pour qu’elle soit la plus transparente possible et sécurisée ?
  • Au niveau de l’utilisation des secrets: comment récupérer un secret dynamique (TTL court) sans pour autant impacter le code applicatif ?

Authentification de notre application

Concernant l’authentification de notre application avec Vault, si nous souhaitons qu’elle aussi transparente et sécurisée que possible, il est important de nous baser sur l’environnement où est déployée notre application.

Notre application est déployée sur AWS, ce qui est parfait car côté Vault nous avons une méthode d’authentification de type AWS.

Cette méthode d’authentification se déroule en 2 types: IAM et EC2.

Comme notre application est déployée sur une instance EC2, nous utiliserons le type EC2.

Si nous regardons de près comment fonctionne cette méthode, nous avons le scénario suivant :

  • 0 – Jusqu’à maintenant notre Gitlab-CI, au travers de Terraform, a déployé notre application au niveau d’une instance EC2 et a stocké les secrets de base de données dans Vault.
  • 1 – Notre instance EC2, une fois déployée, obtient ses metadata au travers du service EC2 metadata (ex: Instance ID, subnet et VPC ID où est déployé notre instance EC2, etc). Vous pouvez trouver plus de détails sur la documentation officielle d’AWS.
  • 2 – Notre application s’authentifie à Vault au travers de la méthode AWS de type EC2 en utilisant une signature PKCS7.
  • 3 – Vault vérifie l’identité ainsi que l’instance EC2 hébergeant notre application respecte nos conditions d’authentification (bound parameters) (ex: Est-elle dans le bon VPC et subnet ? Est-ce la bonne instance ID ? etc)
  • 4 – Si l’authentification est concluante, Vault retourne un token.

Pour mettre en place cette méthode d’authentification, il faut :

  • Que Vault soit en mesure de vérifier l’identité et metadata de l’instance EC2 sur le compte AWS cible.
  • Indiquer les conditions d’authentification (bound parameters) à Vault sur lesquelles nous souhaitons autoriser l’application à s’authentifier.

Vérification de l’identité et des metadata

Pour que Vault soit en capacité de vérifier les informations de notre instance EC2, il lui faut des droits sur le compte cible afin de décrire l’instance concernée via l’action suivante: ec2:DescribeInstances

Pour ce faire, et rester dans la même logique que dans nos précédentes démonstrations, nous allons créer un rôle IAM sur lequel Vault ira assumer celui-ci.

Le rôle IAM devra contenir la policy suivante :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances"
            ],
            "Resource": "*"
        }
    ]
}

Ainsi que la Trust Relationships autorisant le compte source (où se situe Vault) à assumer le rôle IAM.

Côté configuration Vault, nous configurons celui-ci au travers de Terraform :

resource "vault_auth_backend" "aws" {
  description = "Auth backend to auth project in AWS env"
  type        = "aws"
  path        = "${var.project_name}-aws"
}

resource "vault_aws_auth_backend_sts_role" "role" {
  backend    = vault_auth_backend.aws.path
  account_id = split(":", var.vault_aws_assume_role)[4]
  sts_role   = var.vault_aws_assume_role
}

Comme nous pouvons le voir, nous indiquons aussi le rôle IAM (STS) que Vault doit assumer.

A ce stade, Vault est en mesure de vérifier les informations de notre instance EC2.

Mettre les contraintes d’authentification (Bound parameters)

En terme de sécurité, à partir de quelles conditions autorisons-nous notre application à s’authentifier auprès de Vault pour récupérer ses secrets ?

Si nous regardons de plus près la méthode d’authentification AWS de type EC2 de Vault, nous pouvons nous baser sur plusieurs critères, tels que: l’AMI ID, l’account ID, la région, le VPC ID, subnet ID, l’ARN du rôle IAM, l’ARN de l’instance profile ou encore l‘ID de l’instance EC2.

Nous pouvons indiquer plusieurs critères et plusieurs valeurs pour chaque critère. Pour que l’authentification soit acceptée par Vault, il faut qu’une valeur pour chaque critère soit respectée.

Dans notre cas, notre instance EC2 est déployée par Terraform. Ce qui est idéal, car au travers de Terraform nous sommes en capacité de récupérer l’ensemble des attributs de notre instance et de les spécifier côté Vault en tant que bound parameters.

Ce qui nous donne le snippet Terraform suivant afin de créer le rôle Vault pour notre backend d’authentification AWS :

resource "vault_aws_auth_backend_role" "web" {
  backend             = local.aws_backend
  role                = var.project_name
  auth_type           = "ec2"
  bound_ami_ids       = [data.aws_ami.amazon-linux-2.id]
  bound_account_ids   = [data.aws_caller_identity.current.account_id]
  bound_vpc_ids       = [data.aws_vpc.default.id]
  bound_subnet_ids    = [aws_instance.web.subnet_id]
  bound_region        = var.region
  token_ttl           = var.project_token_ttl
  token_max_ttl       = var.project_token_max_ttl
  token_policies      = ["default", var.project_name]
}

Dans notre cas, nous nous basons sur l’AMI ID, le VPC ID, le subnet ID et la region AWS. Nous aurions pu ajouter l’instance ID afin de renforcer la sécurité de notre authentification mais ce critère est à éviter au sein d’un Auto Scaling Group.

A ce stade, notre application est en capacité de s’authentifier et de récupérer ses secrets auprès du Vault.

Utilisation des secrets de notre application

Du côté de l’intégration de Vault au niveau de notre application, nous allons utiliser le Vault agent.

Pour ceux qui souhaitent plus de détails avec l’intégration de Vault agent, vous pouvez vous référer à l’article suivant.

Regardons de plus près le workflow de notre application avec Vault :

Comme nous pouvons le voir, Vault agent s’occupe de 2 phases:

  • L‘authentification de type AWS avec Vault ainsi que la rotation du token Vault
  • La récupération des secrets et le rafraîchissement de ceux-ci

Nous avons dans la configuration de l’agent Vault suivante :

auto_auth {
  method {
    mount_path = "auth/${vault_auth_path}"
    type = "aws"
    config = {
      type = "ec2"
      role = "web"
    }
  }
  sink {
    type = "file"
    config = {
      path = "/home/ec2-user/.vault-token"
    }
  }
}
template {
  source = "/var/www/secrets.tpl"
  destination = "/var/www/secrets.json"
}

Ce fichier peut être templatisé par Terraform afin de remplacer certaines valeurs comme le nom du rôle Vault ou encore le mount_path.

Et enfin, côté de notre template de secret, nous souhaitons récupérer les secrets sous format JSON ce qui nous donne le format suivant :

{
  {{ with secret "web-db/creds/web" }}
  "username":"{{ .Data.username }}",
  "password":"{{ .Data.password }}",
  "db_host":"${db_host}",
  "db_name":"${db_name}"
  {{ end }}
}

Vault s’occupe de toute la partie en relation avec Vault (token Vault, secrets, rafraichissement, etc) laissant ainsi notre application juste a récupérer ses secrets dans le fichier concerné :

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'};
  $host = json_decode($secrets_json)->{'db_host'};
  $dbname = json_decode($secrets_json)->{'db_name'};
}
else{
  echo "Secrets not found.";
  exit;
}

Ce qui nous donne le résultat attendu :

Ce qu’il faut retenir

Par rapport à ce que nous avons réussi à faire jusqu’à maintenant, nous avons le workflow suivant :

  • Terraform a un provider Vault permettant de simplifier l’interaction entre les deux outils.
  • Il est possible d’authentifier un pipeline ou encore une branche précise d’un job Gitlab-CI avec Vault via la méthode d’authentification JWT.
  • Vault permet de générer des credential de cloud provider pour déployer de l’IaC via Terraform. Concernant AWS, celui-ci est en capacité d’assumer des rôles IAM sur plusieurs comptes AWS permettant à notre Terraform de déployer de l’IaC sur plusieurs comptes AWS.
  • Vault permet de centraliser plusieurs types de secrets pour un projet de façon agnostique à l’environnement, incluant les secrets générés par l’IaC.
  • Vault agent simplifie l’intégration de Vault dans une application en s’occupant de l’authentification et du cycle de vie des secrets.
    • Le token Vault utilisé par le Vault agent a une durée de vie courte et change souvent.
    • Les secrets de l’application (base de données dans notre exemple) ont une durée de vie courte et sont mis à jour souvent via le Vault agent.
  • Nous autorisons notre application à s’authentifier à Vault en fonction de l’environnement où elle se situe. Concernant notre application dans AWS, nous nous basons sur une authentification AWS de type EC2 et les bound parameters tels que le subnet ID où se situe notre application, le VPC ID, la région AWS, etc.

Comme nous avons pu le voir dans cet article, Vault permet de sécuriser notre Terraform au travers de Gitlab-CI de bout en bout en incluant l’IaC ou encore notre application elle-même.

Aussi, Vault agent nous permet de réduire les dépendances applicatives avec Vault.

Utilisé de la bonne façon, cette intégration peut être transparente pour les ops, dev et applications. Les secrets deviennent transparents pour tous et avec un cycle de vie court.

Pourquoi chercher à connaître un secret si nous pouvons avoir du Secret as a Service transparent ?

Commentaires :

A lire également sur le sujet :