Ansibled : Consul sécurisé

Beaucoup de monde parle de Consul, ce formidable outil des ateliers Hashicorp. En 2 mots pour ceux qui découvrent : c’est un système clés-valeurs distribué, de service discovery, résilient et extrêmement simple à mettre en place. Comme tous les outils distribués, Consul nécessite une communication entre les noeuds d’un cluster. Dans ce billet, nous allons voir ce qu’il faut sécuriser, et une façon de le faire avec Ansible.

Choix d’architecture

Tout d’abord, vous trouverez beaucoup de tutoriels de prise en main vous décrivant le déploiement de Consul dans des containers. Pour cela, il faudrait s’assurer d’une sécurité confortable sur le démon portant les containers. C’est bien au delà du périmètre de ce billet. Si vous comptez vous appuyer sur Consul pour un déploiement de production, je ne peux que vous conseiller de le déployer au niveau des hosts de votre parc de machine. Cela vous évitera une bonne part de complexité.

De plus, si vous déployez Consul, assurez-vous de le faire sur l’intégralité des noeuds qui auront besoin de consommer son API HTTP ou son interface DNS. Cela présente l’avantage de laisser le point d’ancrage de ces deux ports à leur position par défaut : l’interface de loopback.

Nous allons ici discuter de certificats, de clés privées et de secrets partagés, veillez bien au respect des bonnes pratiques concernant les permissions de fichiers pour éviter qu’ils soient accessibles à d’autres utilisateurs que celui qui porte l’agent Consul.

Les échanges entre noeuds

Voici résumé en une image les différents points d’échange (par défaut) d’un agent Consul et la méthode de sécurisation que je vous recommande.

Ports Consul

Assurez-vous que tous vos noeuds peuvent bien communiquer entre eux sur les ports Serf LAN (fédération intra-cluster) et RPC (propagation de requêtes). Si vous déployez Consul en multi-datacenter, assurez-vous que les noeuds assurant la liaison inter-site puissent également communiquer sur le port Serf WAN (fédération inter-cluster).

Interface DNS et HTTP API

Sur l’interface HTTP API, il est possible de poser un certificat TLS et de refuser l’accès sans certificat client. Je trouve cependant que la limitation au loopback est suffisante et évite la contrainte de rajouter des options à rallonges pour interagir en local ou avec un noeud auquel on a déjà accès. Je vous conseille de n’activer l’interface web que sur les noeuds master et de n’y accéder qu’en forwardant le port 8500 en local via SSH.

L’interface DNS, quant à elle, ne supporte pas DNSSEC au moment de l’écriture de cet article. Si vous souhaitez faire profiter tout votre système de la résolution de la zone .consul ou si vous souhaitez malgré tout exposer le port DNS sur votre réseau, je vous conseille de mettre un DNS Resolver en frontal et de laisser l’interface DNS de Consul en loopback. Evitez également l’usage des recursors pour éviter des soucis. Laissez donc ce genre de mécanismes à des outils tels que Bind ou Unbound, c’est leur travail et ils le font très bien.

Serf WAN & LAN

Serf est la solution de fédération décentralisée de cluster de Hashicorp. C’est cette partie qui assure l’élection du master et maintient la liste des membres. Serf crée beaucoup de messages de taille réduite, pour s’assurer en permanence de la présence des noeuds. Le chiffrement de ces échanges se fait par clé symétrique.

Il suffit donc de partager un secret dans les configurations de tous les noeuds d’un même cluster pour que Serf ne communique qu’en chiffré. La clé a un format défini : 16 caractères encodés en base64. Si vous manquez d’imagination, vous pouvez générer une clé directement au bon format avec la commande :

$ consul keygen

une fois que vous aurez votre clé, il faut l’injecter dans la configuration de tous les noeuds de votre cluster.

{
  "encrypt": "cg8StVXbQJ0gPvMd9o7yrg=="
}

Voilà une bonne chose de faite.

Interface RPC

L’interface RPC sert à la propagation des requêtes initiées via l’API HTTP. Si un noeud recevant une requête HTTP ne peut répondre, il va propager la requête dans le cluster via l’interface RPC.

Pour chiffrer ce point d’échange, il faut que chaque noeud dispose d’un certificat client à présenter, et d’un certificat de CA, auquel comparer les certificats client des appels entrants.

Il y a là un peu de gymnastique à faire pour rester dans les clous :

  • Création du certificat et clé privée du CA local à machine de contrôle. Qu’il soit auto-signé ou issu de la PKI de votre infrastructure existante importe peu.
  • Envoi du certificat sur chaque noeud.
  • Création d'une Certificate Signing Request (CSR) et d'une clé privée sur chaque noeud.
  • Récupération des CSR de chaque noeud, sur la machine de contrôle.
  • Signature des CSR de chaque noeud avec le certificat CA, sur la machine de contrôle.
  • Envoi des certificats signés sur chaque noeud.

Pas de panique ! Tout ceci est automatisé dans la boîte à outils Ansible décrite plus bas.

Si on suit cette procédure :

  • La clé privée CA ne quitte pas la machine de contrôle.
  • La clé privée du noeud ne quitte pas le serveur.
  • Aucun fichier de génération de certificat de noeud ne quitte la machine de contrôle.
  • La CSR n'expire pas, on peut refaire des certificats frais régulièrement.

Sachez juste qu’une fois ces fichiers générés et en place, il faut ajouter une section dans la configuration de Consul :

{
  "ca_file": "/etc/consul.d/tls/consul-root.cer",

  "cert_file": "/etc/consul.d/tls/consul-node.cer",

  "key_file": "/etc/consul.d/tls/consul-node.key",

  "verify_incoming": true,

  "verify_outgoing": true
}

Here comes the hotstepper

Pour faciliter un peu toute cette gymnastique et vous fournir un exemple jouable en live, je vous ai préparé un joli rôle Ansible qui automatise tout ceci. Pour le cas d’école, nous considérons un cluster Consul composé de 3 masters et 3 serveurs. Ce qui pourrait donner un inventaire Ansible ressemblant à ceci :

[consul_nodes]
bastions_0 ansible_host=34.250.10.64  
logstores_0 ansible_host=10.42.110.156  
monitor_0 ansible_host=10.42.110.137

[consul_masters]
masters_0 ansible_host=10.42.110.244  
masters_1 ansible_host=10.42.120.214  
masters_2 ansible_host=10.42.130.48  

Variables à fournir

Le rôle Consul que je vous propose s’appuie sur plusieurs variables qu’il faut fournir. Des assertions sont faites en début de rôle pour s’assurer de la conformité de vos variables.

  • consul_ansible_master_group_name : Le nom du groupe qui contient les machines avec le rôle de master dans le cluster.

  • secrets_dir : Le chemin complet du répertoire dans lequel stocker les secrets qui vont être générés par le rôle. Ce chemin est local à la machine de contrôle Ansible et doit être accessible en lecture/écriture sans escalade de droits. Nous allons revenir sur ce qui va être déposé dedans.

  • consul_ca_info : Le dictionnaire contenant les différents champs du certificat. Le champ Common Name (CN) sera généré à partir du FQDN du noeud dans le domaine de Consul.

Les machines doivent être reliées à Internet pour pouvoir télécharger les binaires Consul. Enfin, pour appliquer le rôle, un simple playbook de ce type suffit :

---
- hosts: all
  become: yes
  vars:
    consul_ansible_bootstrap_group_name: "masters"
    secrets_dir: "{{ playbook_dir }}/secrets"
    consul_ca_info:
      C: "FR"
      ST: "IDF"
      L: "Paris"
      O: "WeScale"

  roles:
    - consul-role

Keep it secret

Pendant l’application du rôle, le répertoire pointé par la variable secrets_dir se voit rempli avec un ensemble de fichiers dont voici le détail. Ces fichiers doivent être conservés avec soin. Ils servent pour l’historique des certificats attribués aux noeuds Consul.

secrets  
└── consul
    ├── encrypt.key          <- clé de chiffrement Serf
        ├── consul-root-key.pem  <- clé privé du certificat racine
        ├── consul-root.cer      <- certificat racine
        └── tls
            ├── bastions_0       <- un répertoire par noeud Consul géré
            │   ├── 0A.pem
            │   ├── consul-node.cer   <- certificat signé par le certificat racine
            │   ├── consul-node.cer.index
            │   ├── consul-node.cer.index.attr
            │   ├── consul-node.cer.index.old
            │   ├── consul-node.cer.serial
            │   ├── consul-node.cer.serial.old
            │   ├── consul-node.csr   <- CSR importée depuis le noeud
            │   └── consulca.conf     <- configuration de signature, générée en local
            ├── bastions_1
            │   ├── 0A.pem
            │   ├── consul-node.cer
            │   ├── consul-node.cer.index
[...]

Les fichiers en .index, .serial et .pem servent à tracer l’historique des certificats attribués à chaque noeud au fil du temps.

Renouvellement des certificats

Si vous avez besoin de renouveler vos certificats (et vous en aurez besoin, régulièrement, just because), il vous suffit de supprimer le fichier consul-node.cer du répertoire contenant l’usine à certificats du noeud à rafraîchir et de relancer votre playbook. Un nouveau certificat sera créé, mis en place et le démon Consul sera relancé pour le prendre en compte.

Et maintenant ?

Ce guide a été testé et validé sur des machines Debian 9, avec Ansible 2.4. Il ne devrait pas vous poser (trop) de soucis à adapter pour d’autres systèmes. À ce stade, nous avons monté un cluster Consul dont les communications inter-noeuds sont chiffrées selon les bonnes pratiques d’Hashicorp.

Il reste un autre chapitre à traiter : les ACL. C’est via les ACL que vous pourrez restreindre les droits de chaque noeud sur les API de Consul. Cela dépasse le cadre de ce billet, et sera peut-être le sujet d’un prochain épisode.

Il est beau mon cluster, il est frais ! Vous n’avez plus d’excuse pour laisser votre Consul discuter en clair sur le réseau. J’espère que cela pourra vous servir, soit en l’état, soit comme base de travail pour l’adapter à votre environnement. Si vous avez des questions ou des remarques, pensez à passer sur les issues du projet.

Have fun. Hack in peace.