samedi 28 janvier 2017

Chiffrement/déchiffrement des mots de passe de vos scripts gérés avec Ansible


Pour les paresseux, un exemple d'implémentation est disponible à l'adresse suivante : https://github.com/Yannig/yannig-ansible-playbooks/tree/master/scripts-vault

Mais pourquoi chiffrer d'abord ?

L'automatisation c'est bien mais des fois vous pouvez rencontrer quelques petits problèmes. Un que je rencontre régulièrement vient de la présence de mot de passe en clair dans mes scripts d'administration WebLogic/Oracle etc. Pour comprendre le problème, prenons l'extrait de playbook Ansible suivant :

- name: "Dépot d'un template de création"
  template:
    src: "template.sh.j2"
    dest: "/tmp/creation.sh"
    mode: "755"
  register: _

- name: "Exécution du template"
  shell: "/tmp/creation.sh"
  when: _.changed|d('no')|bool

Le déroulement est assez simple : on dépose un template sur la machine et on exécute le script si ce dernier a changé (suite à une mise à jour ou simplement parce que le script n'existait pas). Problème : si ce script contient un mot de passe, il est en clair sur la machine ce qui ne fait pas très sérieux.

Nous allons voir comment faire en sorte que les mots de passe soient à disposition sur la machine distante au moment de leur exécution sans pour autant les mettre en clair ou en tout cas rendre très difficile leur exploitation. Pour cela, nous passerons par une clé de chiffrement dans une variable d'environnement et nous verrons comment récupérer ça depuis différents langages (ici Python, Java et shell Unix).

Utilisation du 3DES pour chiffrer

Le principe va être le suivant : on génère une clé de chiffrement sur 24 caractères, nous chiffrons le mot de passe avec et nous la mettons à disposition sur la machine distante que nous déchiffrons via l'utilisation d'une variable d'environnement.

Cette clé peut-être positionné dans votre inventaire ou via un générateur de clé avec une graine connue (seed). Cette graine peut venir du nom de votre serveur, une variable déjà existante propre à votre plateforme, etc. J'en parlerai sûrement une prochaine fois pour voir ce qu'il est possible de faire.

Mais revenons à nos moutons. Première chose à faire : créer le programme qui nous permettra de faire le chiffrement à partir de cette clé. Ci-dessous un programme python permettant de faire du chiffrement 3DES :

import sys, os, base64
from Crypto.Cipher import DES3

BS    = 8
pad   = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS) 

key = os.getenv('DES_KEY')
des = DES3.new(key, DES3.MODE_ECB)
print(base64.b64encode(des.encrypt(pad(sys.argv[1]))))

La clé de chiffrement se trouve stockée dans la variable d'environnement DES_KEY. Ci-dessous un exemple d'utilisation de ce mécanisme :

$ DES_KEY=111111111111111111111111 ./chiffre.py abcdefghijklmnopqrstuvwxyz
eHPtqHbKD+oGdodkRUxdwo8Z4EcsTUBrrSeCYcBIQ0g=

Relançons notre programme en changeant notre clé :

$ DES_KEY=111111111111111111111112 ./chiffre.py abcdefghijklmnopqrstuvwxyz
vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=

Parfait, on voit bien que le chiffrement de notre mot de passe change complètement lorsqu'on change la clé.

Déchiffrons notre message

Nous arrivons à chiffrer. Écrivons maintenant le petit bout de code python qui va nous permettre de récupérer le mot de passe :

import sys, os, base64
from Crypto.Cipher import DES3

BS    = 8
unpad = lambda s: s[0:-ord(s[-1])]

key = os.getenv('DES_KEY')
des = DES3.new(key, DES3.MODE_ECB)
print(unpad(des.decrypt(base64.b64decode(sys.argv[1]))))

Faisons maintenant quelques tests :

$ DES_KEY=111111111111111111111112 ./dechiffre.py vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=
abcdefghijklmnopqrstuvwxyz

Youpi ! On récupère notre mot de passe !

Maintenant que nous avons notre code python, voyons comment récupérer ce mot de passe depuis un autre langage.

Déchiffrement Java

Si comme moi vous n'avez pas été sage et que vous faites de l'administration Java, vous aurez peut-être besoin de récupérer ce mot de passe depuis un programme en Java. Le bout de code suivant devrait nous aider à réaliser cette opération :

// Récupère un mot de passe (ou autre chose) chiffré avec une clé 3DES et renvoie
// la chaîne à l'aide de la clé contenue dans la variable d'environnement DES_KEY

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;

// Decrypt a password
class dechiffre {
  public static void main(String [] args) throws Exception {
    System.out.println(decrypt(args[0]));
  }
  public static String decrypt(String password) throws Exception {
    String env_key = System.getenv("DES_KEY");

    Cipher out_cipher = Cipher.getInstance("DESede/ECB/PKCS5Padding");
    SecretKeySpec key = new SecretKeySpec(env_key.getBytes(), "DESEDE");
    out_cipher.init(Cipher.DECRYPT_MODE, key);
    return new String(out_cipher.doFinal(Base64.getDecoder().decode(password)), "UTF-8");
  }
}

Notons au passage la qualité première de Java : sa concision.

Lançons maintenant notre petit programme et voyons ce que ça donne :

$ DES_KEY=111111111111111111111112 java dechiffre vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=
abcdefghijklmnopqrstuvwxyz

C'est pas trop mal. Voyons ce que ça donne en changeant la clé :

DES_KEY=111111111111111111111111 java dechiffre vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos=
Exception in thread "main" javax.crypto.BadPaddingException: Given final block not properly padded
       at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:975)
       at com.sun.crypto.provider.CipherCore.doFinal(CipherCore.java:833)
       at com.sun.crypto.provider.DESedeCipher.engineDoFinal(DESedeCipher.java:294)
       at javax.crypto.Cipher.doFinal(Cipher.java:2165)
       at dechiffre.decrypt(dechiffre.java:19)
       at dechiffre.main(dechiffre.java:11)


Magnifique, une superbe stacktrace Java pour nous indiquer que ça ne fonctionne pas bien !

Et si on faisait la même chose en shell ?

Pas mal mais j'écris rarement mes programmes de création d'environnement en Java. Je me suis donc posé la question s'il serait possible de faire la même chose en shell. En effet, j'ai régulièrement besoin de déposer des scripts shell afin de procéder à la création d'objets divers avec des morceaux de mot de passe dedans. Là aussi, j'aurai bien aimé pouvoir chiffrer mes mots de passe sur la machine distante.

Cette fois ci, la solution vient d'openssl. Petit bonus, il s'agit d'un programme relativement courant à trouver sur une machine Linux (sauf si vous êtes sur une image Docker où en général il est supprimé). Nous allons voir comment l'utiliser dans notre contexte :

$ export DES_KEY=111111111111111111111112
$ echo vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos= | \
    openssl des-ede3 -d -a -K $(echo -n "$DES_KEY" | xxd -pu)
abcdefghijklmnopqrstuvwxyz

A noter que j'ai un peu tâtonné pour trouver le bon algo (ici des-ede3). En effet, avec l'algo des3, ça fonctionnait presque mais ça plantait sur des chaînes de plus de 8 caractères. Autre problème : openssl veut une chaîne au format hexadécimal. Il faut donc faire cette transformation avec la chaîne de la clé avec xxd (et l'option -pu). Autre point d'attention lors de la transformation : faire attention à ne pas rajouter de retour à la ligne à xxd sinon vous fausserez votre clé.

En dehors de ça, mission accomplie, nous avons notre principe.

Pour conclure

Nous avons vu comment faire le chiffrement reste maintenant à combiner ça avec votre outil de gestion de conf préféré (Ansible, Puppet etc.). L'astuce consistera à passer la variable d'environnement au moment du lancement du script. Ci-dessous un exemple de passage de variable :

- name: "Dépot script"
  template:
    scr: "test.sh.j2"
    dest: "/tmp/monscript.sh"
  vars:
    encrypted_value: "vD37nk4ME9n5BUa9pLH0zrHGWl2xUkLRlLhEwfkFmos="
- name: "Rafraichir configuration"
  shell: "/tmp/monscript.sh"
  environment:
    DES_KEY: "{{des_key}}"

Ci-dessous le contenu du template test.sh.j2 :

#!/bin/bash
mdp=$(echo {{encrypted_value}} | openssl des-ede3 -d -a -K $(echo -n "$DES_KEY" | xxd -pu) &> /dev/null) && echo $mdp

Depuis Ansible, tout va bien, le shell se lance bien. Essayons de lancer maintenant ce script depuis la machine :

$ ./test.sh

Le script n'affiche rien. Si on vérifie la valeur du code retour, on se rend compte qu'il y a eu un problème lors du lancement du script :
$ echo $?
1

Parfait ! Nous sommes incapable d'obtenir le mot de passe directement depuis la machine : mission accomplie ! Reste maintenant à généraliser ce mécanisme.

Petite astuce au passage : passer par l'utilisation de filtre Ansible pour gérer le chiffrement de vos mots de passe. Ça simplifiera grandement votre travail.