Hashicorp Vault chez Numberly #1

L’histoire de la gestion de milliers de certificats TLS chez Numberly

Contexte

Numberly crée et héberge des milliers de sites Web et d’API développés par plus d’une centaine de développeurs.

Représentant plus de 4000 projets sur Gitlab, ces interfaces et API deviennent des éléments critiques de l’activité de nos clients une fois en production.

Il est depuis longtemps nécessaire pour nous de protéger l’accès à ces ressources, en commençant par le chiffrement réseau et le HTTPS.

Au fil des ans, nous avons été confrontés à de nombreux défis de mise à l’échelle qui doivent être résolus pour surmonter à la fois la friction que représente la gestion des certificats SSL pour la production mais aussi l’automatisation de leur cycle de vie et de leur maintenance. À bien des égards, nos défis sont similaires à ceux auxquels les hébergeurs à grande échelle tels qu’OVH ont été confrontés.

Il y a un grand fossé entre la gestion d’une douzaine de certificats SSL et la gestion de milliers : on ne peut pas se contenter d’optimiser le temps que les humains passent à les créer/installer/surveiller/renouveler, à un moment donné, vous devez simplement rendre ces opérations transparentes pour que vos développeurs et votre infrastructures puissent grandir sans friction.

Nous avons écrit cette série d’articles pour partager le chemin parcouru depuis 20 ans et notre expérience dans la gestion de ce que l’on pourra regrouper sous le terme de “secrets”. Depuis la gestion des certificats SSL à l’échelle jusqu’à la généralisation de la gestion des secrets aux équipes et aux applications.

L'Histoire

Numberly existe depuis 2000, nous avons traversé la bulle Internet et avons eu la chance d’organiser de nombreuses itérations pour notre stratégie d’hébergement.

Certaines choses n’ont cependant jamais changé : notre autonomie et notre indépendance technique.

  • 2011: Numberly obtient son propre numéro d’Autonomous System et migre le routage du courrier électronique et l’hébergement web vers ses propres IP.
  • 2012: Nous achetons des répartiteurs de charge F5 pour gérer la charge de trafic pour le lancement de notre service de suivi RTB (30k RPS).
  • 2016:Nous commençons à migrer certaines charges de travail vers Kubernetes et abandonnons progressivement F5 pour nos usages internes.
  • 2017: Numberly devient un Local Internet Registry.
  • 2018: Nous hébergeons la plupart de nos applicatifs exposés sur Internet ainsi que nos traitements de données sur Kubernetes.
  • 2020: Nous gérons des certificats SSL à grande échelle, utilisés par des milliers de sites web et d’API.
  • 2021: La refonte de nos réseaux internes et externes nous a permis de faire évoluer notre hébergement Kubernetes vers plusieurs centres de données en toute transparence.

Ce dernier défi nous a amenés à repenser l’ensemble de notre stratégie SSL pour répondre aux services accessibles au public et en interne (pas seulement les sites web).

Avant ce projet, nos plateformes d’hébergement web étaient composées de :

  • Équilibreurs de charge : une série de boîtes noires F5.
  • Des certificats SSL : géré par Digicert avec une automatisation perfectible pour l’émission et le renouvellement des certificats. Sans parler de son énorme coût financier.
  • D’automatisation : une infâme feuille de calcul Google partagée entre les chefs de projet afin qu’ils puissent nous indiquer quels sites web nécessitaient ou non un renouvellement de certificat SSL par le biais des problèmes de Gitlab.

De ce point de vue, toute refonte du design nous donnerait de meilleurs résultats !

Nos besoins :

  • Ne plus utiliser les boîtes noires de F5, en raison de leur manque d’automatisation, du coût des licences et de leur faible observabilité
  • Ne pas utiliser de procédures soutenues par des humains pour générer des certificats SSL
  • Pouvoir générer des certificats SSL “at scale”
  • Surveillance par défaut de tous ces certificats, avec alerte automatique
  • Stocker ces certificats en toute sécurité
  • Et cela sans coûts supplémentaires

 

Ce que nous avons décidé de faire

  • Remplacer F5 par des serveurs (Dell, HPE) avec NGINx pour tirer parti de notre nouveau réseau topologie BGP anycast fonctionnant dans nos deux centres de données.
  • Let’s Encrypt pour la génération de certificats
  • Leverage on our existing Prometheus and AlertManager stack for respectively our monitoring and alerting
  • S’appuyer sur nos infrastructures PrometheusAlertManager pour respectivement notre surveillance et nos alertes
  • Utilisez Vault comme un stockage sécurisé et hautement disponible

L’automatisation de ce travail nous a demandé de créer le pipeline suivant :

Stockage sécurisé de nos certificats avec Hashicorp Vault

Le stockage des certificats SSL a été le point d’entrée de cette technologie chez Numberly. Nous couvrirons les cas d’utilisation suivants dans des articles de blog ultérieurs (restez à l’affût).

Vault a résolu le problème du stockage et de l’exposition de manière sécurisée et hautement disponible de données sensibles telles que les certificats SSL.

Accès & audit : Seuls les membres de l’équipe d’infrastructure ont accès à ce point de montage KV. Et tout est consigné dans les journaux d’audit de Vault.

Hébergement et mise en réseau : nos nœuds Vault sont hébergés dans nos deux datacenters, AWS faisant office de troisième datacenter.

Ils sont tous capables de traiter les demandes en annonçant la même adresse IP anycast de service dans notre réseau interne.

Tout client sera acheminé vers le serveur Vault le plus proche. Et si ce serveur Vault n’est pas maître, il est de toute façon capable de traiter la requête.

 

Format de stockage pour les certificats SSL

$ vault secrets enable -version=2 kv-certificates

 

Nous avions besoin que les clés de nos certificats SSL aient toujours le même schéma. Cela ressemble à ceci :

 

  • cert : pour stocker le certificat SSL au format PEM
  • chaîne : la chaîne Let’s Encrypt
  • fullchain : la concaténation de la chaîne et des clés de certificat
  • key : la clé du certificat SSL
  • propriétaire : quelques informations sur le propriétaire dans le cas où il s’agit du certificat d’un client
  • timestamp : horodatage de la création du certificat

Automatiser le déploiements des Policy et AppRole avec terraform

Les certificats SSL étant très sensibles, nous utilisons la fonction AppRole de Vault.
Ainsi, nos applications ne disposent jamais du même jeton Vault et peuvent être informées de l’expiration de leur jeton afin qu’elles puissent le renouveler.

Nous avons automatisé cette partie en utilisant terraform :

resource "vault_auth_backend" "approle" {
     type = "approle"
     tune {
       default_lease_ttl = "60s"
     }
}

data "vault_policy_document" "loadbalancer" {
     rule {
       description = "Used by nginx load-balancers to read SSL certificates"
       path = "kv-certificates/data/*"
       capabilities = ["read"]
     }
}

resource "vault_policy" "loadbalancer" {
     name = "loadbalancer"
     policy = data.vault_policy_document.loadbalancer.hcl
}

resource "vault_approle_auth_backend_role" "loadbalancer" {
     backend = vault_auth_backend.approle.path
     role_name = "loadbalancer"
     token_policies = [vault_policy.loadbalancer.name]
     token_ttl = 600
}

Pipeline d'automatisation

Chez Numberly, nous exécutons des milliers de tâches par jour grâce à Gitlab CI.
Nos runners Gitlab s’exécutent dans nos clusters Kubernetes au sein de nos datacenters et nous utilisons parfois des runners externes pour absorber les pics.

Il était logique d’utiliser nos plateformes CD existantes pour ce travail d’automatisation.

Let’s Encrypt met en œuvre le protocole ACME, nous devions trouver un client ACME facilement modifiable pour gérer les intégrations que nous voulions. Plus de 50 clients ACME existent et sont référencés sur le site de Let’s Encrypt website.

Nous sommes de grands fans du principe de KISS et certains d’entre nous ont eu une expérience antérieure avec un client écrit en bash : dehydrated.

Le projet dehydrated met en œuvre un principe de hooks qui permet d’écrire des comportements personnalisés en bash, ce qui était pratique pour nos prochains objectifs.

En utilisant dehydrated, nos défis restants étaient :

  • Trouver un hook pour gérer les challenges DNS avec AWS route53. Il existe des hooks pour dehydrated sur Github comme dehydrated-route53-hook-script.
  • Trouver un hook pour pousser nos certificats vers Hashicorp Vault : Nous avons formé un projet existant pour corriger certains problèmes avec le stockage KV v2 store et nous avons créé le dehydrated-vault-hook.

Après avoir implémenté tout cela, notre  .gitlab-ci.yaml ressemblait à cela :

image: registry/docker-images/alpine:latest

stages:
     - test
     - trigger

before_script:
     - apk --update-cache add curl

lint:
     # Some linting to make sure we didn't declare wrong domains
     stage: test
     script:
       - apk add bash grep
       - ./check.sh

main:
     stage: trigger
     script:
      # Generating DNS challenge with AWS route53 hook
       - dehydrated --config /etc/dehydrated/config --cron --hook /var/lib/dehydrated/dehydrated-route53-hook-script/hook.sh --keep-
      # Pushing generated certificates with our Hasicorp vault hook
       - dehydrated --config /etc/dehydrated/config --cron --hook /var/lib/dehydrated/dehydrated-vault-hook/vault-hook.sh --keep-going
     only:
       - master

Utiliser les certificats SSL de manière transparente

L’une des évolutions de l’infrastructure a été de démanteler les load-balancers F5 et d’utiliser NGINx, plus précisément  OpenResty qui est une version modifiée de NGINx avec le support de LuaJIT.

Nous utilisons une configuration NGINx “catch-all” et la directive ssl_certificate_by_lua_block pour récupérer automatiquement le certificat SSL d’un site.

La seule limitation connue est que les requêtes SSL doivent être effectuées avec la compatibilité SNI afin que nous puissions disposer du nom du serveur lors de l’établissement du “handshake” de la connexion SSL.

Notre lua lit le nom du serveur à partir du SNI et interroge Vault via son API HTTP.

Nous utilisons un AppRole avec un TTL bas (600s). Ce jeton est sauvegardé dans un lua_shared_dict shm storage que chaque worker OpenResty peut obtenir pour faire des requêtes à Vault.

  • Premier essai : récupérer le certificat correspondant au Host server_name certificate, ie: foo.acme.com
  • Second essai : upper wildcard, ie: *.acme.com
  • Troisème essai: lower wildcard, ie: *.foo.acme.com

En supposant que le server_name soit un SAN de la wildcard fille.

Nous utilisons 3 systèmes de cache différents de type LRU :

  • certs_cache : cache pour les certificats trouvés avec un paramètre  cache_expire_time
  • fallback_certs_cache : un cache sans expiration qui couvre le cas où un certificat expiré dans certs_cache pendant un incident sur notre infrastructure Vault.
  • unknown_certs_cache : un cache pour les domaines qui n’ont pas de certificats SSL dans Vault (ce qui signifie qu’ils ont atteint le troisième essai).

Ce troisième cache est vraiment important car il nous évite de submerger notre cluster Vault de requêtes pour vérifier si un certificat existe pour chaque requête entrante.

Kubernetes integration

Pour permettre à nos développeurs d’utiliser des certificats dans notre cluster Vault, nous avons utilisé le projet terriblement efficace de vault-secrets-operator.

Il permet à nos développeurs de créer des objets Custom Resource Definition que secrets-manager utilisera pour savoir quels certificats SSL doivent être synchronisés avec un Kubernetes secret.

Nous irons parti de Kubernetes RBAC pour n’autoriser que des utilisateurs spécifiques à utiliser cette technique, car elle pourrait être utilisée de manière abusive pour récupérer tous les certificats SSL.

Voici à quoi ressemble un CRD :

---
apiVersion: ricoberger.de/v1alpha1
kind: VaultSecret
metadata:
     name: vault-star.numberly.com
     namespace: team-xxx
spec:
     keys:
     - fullchain
     - key
     path: kv-certificates/*.numberly.com
     templates:
       tls.crt: '{% .Secrets.fullchain %}'
       tls.key: '{% .Secrets.key %}'
     type: kubernetes.io/tls

Surveillance et alerte sur les certificats SSL

Maintenant que tous nos certificats SSL sont stockés dans un endroit sûr et central, nous pouvons facilement automatiser leur surveillance.

À l’aide d’une tâche Gitlab CI, nous générons un fichier YAML contenant les URL de tous nos certificats SSL que nous mettons à la disposition de Prometheus

à l’aide de file_sd_configs et d’un blackbox_exporter externe pour qu’il soit “scrapé” par un de nos clusters Prometheus.

- job_name: blackbox-http-static
     file_sd_configs:
     - files:
       - /etc/prometheus/blackbox/static-http-targets/*.yml
     metrics_path: /probe
     params:
       module:
         - http_2xx

Voici un exemple d’une alerte Prometheus :

- alert: SslCertExpiringShortly7days
     expr: last_over_time(probe_ssl_earliest_cert_expiry{job="blackbox-http-static"}[2h]) - time() < 86400 * 7
     labels:
       severity: critical
     annotations:
       summary: "{{ $labels.instance }} expires in {{ $value | humanizeDuration }}"
       grafana: <grafana url>
       documentation: <documentation url to know what to do>

Conclusion

Sur une période de deux ans, en utilisant cette méthode, nous pouvons esquisser quelques grandes victoires :

  • Nous n’avons jamais manqué un renouvellement de certificat SSL !
  • Aucun humain n’a été blessé en créant/renouvelant un certificat SSL à la main.
  • Aucun membre de l’équipe n’a passé du temps à redémarrer les serveurs web pour ajouter/renouveler un certificat SSL.
  • Le temps d’attente des certificats SSL pour les chefs de projet et les développeurs a pu être utilisé pour des choses plus utiles et s’assurer que le service rendu soit le plus efficace possible.
  • Nous avons systématiquement été alertés pour les certificats qui allaient expirer en raison de certains problèmes (changement de DNS, erreur de l’API Let’s encrypt, etc.).
  • Nous avons effectué d’innombrables mises à jour de Vault sans aucune coupure de service.

Nous n’aurions pas pu réaliser ce travail d’ingénierie sans plusieurs grands projets Open Source et notamment sans Let’s Encrypt qui rend l’Internet plus sûr depuis fin 2015. Nous souhaitons remercier tous les développeurs pour le temps et le dévouement qu’ils ont consacrés à tous les projets et initiatives Open Source que nous avons utilisés ❤️.

Comme toujours, le temps que nous avons consacré aux forks ou aux nouveaux projets a été reversé ou mis en Open Sourced sur notre Github account.