5 May 2023

Migration de 50.000 secrets en production

Table of contents


Contexte

Tel qu’expliqué dans l’article Gestion de secrets dynamiques dans Puppet, trocla a rapidement été mis en place par des collègues dans l’infra Puppet Claranet pour la gestion des secrets. Au fil du temps il y a eu plusieurs évolutions, notamment sur la définition du backend. Testé en mémoire dans un premier temps il est passé par plusieurs étapes, fichier, MySQl, PostgreSQL, avant de rester dans une version stable, qui a duré plusieurs années, sur un simple MySQL en réplication primary/replica. Bien que la basse était légère elle contenait plus de 50k de secret.

En 2019, Après de nombreuses discussions, un collègue avait envie de tester Vault, et potentiellement d’y migrer la gestion des secrets Puppet. Nous avions déjà plusieurs objectifs en vu qui pourrait être rempli par cette migration:

  • Fin de support d’une infrastructure OS/MySQL vieillissante
  • Une Api native, qui est le principe mĂŞme du fonctionnement de Vault
  • Un gain en sĂ©curitĂ© apportĂ©e par le chiffrement et le fonctionnement d’accès portĂ© par Vault (chiffrement actuellement portĂ© par un certificat x509)
  • Une convergence Ă  la synergie technologique du groupe France
  • La gestion des metadatas (particulièrement les dates de l’historique et le versionning)
  • Le support d’ACL et de relation avec l’AD
  • Une webui intĂ©grĂ©, jusque lĂ  la gestion des secrets par les non-ops se faisait avec une webui custom qui stockait les secrets dans un API trocla dĂ©diĂ© Ă  ce projet
  • Une amĂ©lioration de la rĂ©silience, avec un backend consul pour Vault

Étude de la faisabilité du projet

Une année passe avant que l’on se relance concrètement sur le sujet et nous pouvons commencer par lister les premiers points de difficultés que cette migration re-présente.

  1. Les 4-5k d’agents Puppet tournent en production toutes les 30min, pour récupérer ou créer des milliers de mots de passe de comptes système, ftp, base de données ainsi que plusieurs centaines de certificats. Une incohérence de données mettrait KO plusieurs centaines de sites web de nos clients.
  2. Vault n’a pas le même fonctionnement que Trocla, l’intérêt principale de la librairie est de pouvoir gérer une multitude de format et de pouvoir genérer dynamiquement des secrets s’ils n’existent pas.
  3. Une interaction du contenu généré par Puppet pars Ansible via l’api Trocla et et une librairy dédié est en place. Une migration vers vault impliquerait la revue de presque 1k de playbook ansible.
  4. Trocla à une importance non négligeable dans le code Puppet, c’est utilisé dans nos modules de profiles (sur près de 100 modules) ou bien dans les hiera (pres de 15k de références).
  5. Le contenue de la base Trocla est continuellement en mouvement, que ce soit dynamique ou via interaction d’OPS.

Préparation de migration

Après avoir listé tous ces points, la tâche s’est avérée plus complexe que prévus. De ça né un groupe de travail de 3 personnes, chacun avec leur propre expertise. Nous avons grandement étudié plusieurs solutions, l’une d’elle était de faire tout simplement de Vault un nouveau backend pour Trocla. Étant donné que nous n’avons ni développeur Ruby ni grande expertise dans le sujet, il était difficile d’accepter de faire du mauvais code custom sur un élément aussi central. Il a été décidé de faire le travail et de le porter à la communauté afin d’avoir une revue de code. Par la suite, le projet fût séparé en 3 grands points et nous avons chacun pris le lead sur un sujet.

  • Une première personne, avec un profil lead tech senior et ayant participĂ© Ă  l’implĂ©mentation initiale de Trocla, a dĂ©jĂ  pris en main le sujet Vault. Dans un premier temps, il faut mettre en place l’architecture. On partira sur une architecture relativement simple de Vault avec 2 instances balancĂ©s par du Haproxy en local et un stockage des donnĂ©es dans backend sur un clusteur consul interne. Ă€ terme une migration sur Kubernetes pourrat ĂŞtre envisagĂ©. L’auto-unseal est Ă©galement en place grâce Ă  une autre instance Vault du groupe et qui est portĂ© sur une cloud provider public. Enfin des tests de performances et de montĂ© en charge sont rĂ©alisĂ©s afin de s’assurer que tout soit prĂŞt pour accueillir le traffic Puppet (que nous avions du mal Ă  Ă©valuer pour ĂŞtre honnĂŞte).

  • Un OPS du cĂ´tĂ© IngĂ©nieur Run, qui souhaite grandement monter en compĂ©tence sur le sujet, sera en charge de mettre en place la migration des donnĂ©es de MySQL vers Vault. Pour ça nous devons rĂ©cupĂ©rer de l’existant via Trocla (pour le dĂ©chiffrement x509) et pousser le contenue via la lib vault-ruby. L’algorithme Ă©tait le suivant:
    1. Lister toutes les clés
    2. Pour chaque clés, récupérer les formats
    3. Pour chaque formats récupérer le contenue et construire un nouveau hash avec le contenue format + secret
    4. Pousser la clé dans un kv v2 Vault
  • Et enfin moi, qui sera en charge de faire le code Ruby pour le backend Trocla, l’utilisation avec Puppet, l’Api Trocla et l’interaction Ansible. J’ai donc proposĂ© deux merges requests:

Côté plan d’action il était très simple, mais nécessitait l’interruption des opérations pour une journée (nous avons pris une journée non ouvré).

  1. Mettre en place un kv vide
  2. Stopper tous les agents Puppet afin de ne pas modifier les mots de passe de production durant l’opération.
  3. Lancer la migration des données
  4. Valider le fonctionnement sur quelques machines de tests
  5. RĂ©activer le parc et sur-veiller toutes anomalie

Gestion du token Vault

Ça à été un des points le plus complexe pour nous. Toute la sécurisation d’accès de vault se base sur la gestion de tokens, et l’idéal est d’avoir des tokens les plus courts et les moins utilisés possible. Mais ce n’est pas très compatible pour de l’utilisation DSL compilé par les Puppetserver. En effet, la configuration et l’initialisation de trocla est fait lors de la création des instances jruby, et il était in-envisageable de devoir re-faire des tokens à chaques appels.

Nous avons donc choisi de faire un token de service avec les sécurités suivantes:

  • Restriction par les cidrs des nos Puppetservers et notre API
  • Une policy avec les droits uniquement sur un kv dĂ©diĂ© Ă  Trocla
  • Un ttl de 24h renouvellement de manière infini

Il a fallut par la suite intégrer le renouvellement du ttl du token que j’ai placé sur les Puppetservers (qui avaient le token de service dans la configuration trocla).

#!/opt/puppetlabs/puppet/bin/ruby
require 'yaml'
require 'vault'

lock='/tmp/trocla-token-renew.lock'

raise format('Lock file %s exist', lock) if File.exist?(lock)
File.open(lock, 'w') {}

troclarc = YAML.load_file('/etc/troclarc.yaml')
vault = Vault::Client.new(
  troclarc.delete('store_options').reject { |k, _| k == :mount }
)
vault.auth_token.renew_self

File.delete(lock) if File.exist?(lock)

Ensuite il a suffit de fait l’intégration du timer systemd via Puppet:

$trocla_renew = '/usr/local/sbin/trocla-token-renew'

file { 'trocla-token-renew':
  ensure => file,
  path   => $trocla_renew,
  owner  => 'root',
  group  => 'root',
  mode   => '0500',
  source => "puppet:///modules/${module_name}/trocla/trocla-token-renew",
}

-> systemd::unit_file { 'trocla-token-renew.service':
  ensure  => 'present',
  content => template("${module_name}/systemd/trocla-token-renew.service.erb"),
}
-> systemd::unit_file { 'trocla-token-renew.timer':
  ensure  => 'present',
  content => template("${module_name}/systemd/trocla-token-renew.timer.erb"),
  enable  => true,
  active  => true,
}

trocla-token-renew.timer.erb:

[Unit]
Description=Timer to run the vault token renew

[Timer]
OnBootSec=15min
OnCalendar=*-*-* <%= format('%02d', @hour) %>:<%= format('%02d', @minute) %>:00

[Install]
WantedBy=timers.target

trocla-token-renew.service.erb:

[Unit]
Description=Script to renew the trocla vault token
ConditionFileIsExecutable=<%= @trocla_renew %>
After=network.target

[Service]
Type=oneshot
ExecStart=<%= @trocla_renew %>
Nice=10
IOSchedulingClass=best-effort
IOSchedulingPriority=5

[Install]
WantedBy=multi-user.target

Migration

Le jour de la migration tout se passe bien. Une dizaine de clé avec des cas spécifiques avaient été identifiés. Allant des clés contenant des caractères spéciaux, des / ou avec des noms de clés trop longs, nous les avions traités au cas par cas. L’import s’est fini de la même manière que la préproduction (en environ 20min), mais malheureusement au moment du test du contenu sur quelques noeuds, on voit vite qu’il y a un problème. Certains secrets changent. Il n’a pas fallu beaucoup de temps pour comprendre que lors de la migration nous avions importé le jeu de donné de la préproduction, issue d’un backup de plusieurs semaines.

Le contre temps était anticipé dans le plan d’action, mais lors de la destruction du kv nous avons eu un problème. Le kv avait trop de contenue et n’arrivais à se détruire correctement. C’est lors de nombreuses fouilles sur les issues Github que nous avons trouvé la solution. Nous avons changé la valeur du paramètre default_max_request_duration afin que consul ait enfin de temps de supprimer tout le contenue du kv.

L’import c’est ensuite re-fait correctement et nous avons pu finir la migration.

Retour d’expérience

Après la migration nous avons pu extraire plus de métriques de notre infra Trocla. C’est avec surprise que nous avons observé 2M d’appels par jour sur le kv Vault (pour 10M d’appels par jour de l’API Puppet) et que l’intégration d’une couche https supplémentaire n’avait pas d’impact sur les performances des compilations de catalogues Puppet. Bien au contraire Vault s’est trouvé être un meilleur backend que MySQL (on parle de quelques ms de gain).

J’ai ensuite ajouté la mise en place d’expiration de clé vault via la merge request #71 (avec un petit fix dans #80), nous permettant d’avoir un renouvellement des secrets de manière très simple tout en gardant un historique accessible via la webui vault.

Enfin, malheureusement, nous attendons toujours nos caisses de vin Trocla Nera de la pars de nos (anciens) managers.


Tags: