Synchroniser les clefs SSH sur Cisco IOS XR avec un module Ansible sur mesure
Vincent Bernat
La collection cisco.iosxr
d’Ansible Galaxy fournit
un module iosxr_user
pour gérer les utilisateurs
locaux, ainsi que leurs clés SSH. Cependant, le module est plutôt
lent, n’affiche pas les différences pour les clés SSH, ne signale pas
de changement lorsqu’une clé est modifiée, et ne gère pas le retrait
des clefs obsolètes. Écrivons un module Ansible gérant uniquement les
clefs SSH tout en résolvant ces limitations.
Note
Je vous recommande de lire « Écrire un module Ansible sur mesure » en introduction.
Comment ajouter une clé SSH à un utilisateur#
L’ajout de clés SSH aux utilisateurs dans Cisco IOS XR est assez peu documenté. Tout d’abord, vous devez encoder la clé au format ASN.1, comme une clé publique OpenSSH, mais sans l’encodage base64 :
$ awk '{print $2}' id_rsa.pub \ > | base64 -d \ > > publickey_vincent.raw
Ensuite, vous devez envoyer la clé en SCP sur
harddisk:/publickey_vincent.raw
et l’importer pour l’utilisateur
connecté avec la commande IOS suivante :
crypto key import authentication rsa harddisk:/publickey_vincent.b64
Toutefois, pour importer la clé pour un autre utilisateur, vous devez
faire partie du groupe root-system
:
username vincent group root-lr group root-system
La commande suivante permet alors d’attacher la clé à un autre utilisateur :
admin crypto key import authentication rsa username cedric harddisk:/publickey_cedric.b64
Code#
Le module a la signature suivante et installe pour chaque utilisateur la clé spécifiée. Il retire également les clefs des utilisateurs inconnus.
iosxr_users: keys: vincent: ssh-rsa AAAAB3NzaC1yc2EAA[…]ymh+YrVWLZMJR cedric: ssh-rsa AAAAB3NzaC1yc2EAA[…]RShPA8w/8eC0n
Prérequis#
Contrairement au module iosxr_user
, notre module ne gère que les
clés SSH, une seule par utilisateur. Par conséquent, les utilisateurs
doivent déjà être définis dans la configuration en cours1. De
plus, l’utilisateur défini par ansible_user
doit se trouver dans le
groupe root-system
. La collection cisco.iosxr
doit également être
installée car le module repose sur son code.
Lors de l’exécution du module, ansible_connection
doit être défini à
network_cli
et ansible_network_os
à iosxr
. Ces variables sont
généralement déclarées dans l’inventaire.
Définition du module#
En se basant sur le squelette présenté dans l’article précédent, nous définissons le module :
module_args = dict( keys=dict(type='dict', elements='str', required=True), ) module = AnsibleModule( argument_spec=module_args, supports_check_mode=True ) result = dict( changed=False )
Obtenir les clefs installées#
L’étape suivante consiste à récupérer les clefs déjà installées, ce qui peut être fait avec la commande suivante :
# show crypto key authentication rsa all Key label: vincent Type : RSA public key authentication Size : 2048 Imported : 16:17:08 UTC Tue Aug 11 2020 Data : 30820122 300D0609 2A864886 F70D0101 01050003 82010F00 3082010A 02820101 00D81E5B A73D82F3 77B1E4B5 949FB245 60FB9167 7CD03AB7 ADDE7AFE A0B83174 A33EC0E6 1C887E02 2338367A 8A1DB0CE 0C3FBC51 15723AEB 07F301A4 B1A9961A 2D00DBBD 2ABFC831 B0B25932 05B3BC30 B9514EA1 3DC22CBD DDCA6F02 026DBBB6 EE3CFADA AFA86F52 CAE7620D 17C3582B 4422D24F D68698A5 52ED1E9E 8E41F062 7DE81015 F33AD486 C14D0BB1 68C65259 F9FD8A37 8DE52ED0 7B36E005 8C58516B 7EA6C29A EEE0833B 42714618 50B3FFAC 15DBE3EF 8DA5D337 68DAECB9 904DE520 2D627CEA 67E6434F E974CF6D 952AB2AB F074FBA3 3FB9B9CC A0CD0ADC 6E0CDB2A 6A1CFEBA E97AF5A9 1FE41F6C 92E1F522 673E1A5F 69C68E11 4A13C0F3 0FFC782D 27020301 0001 […]
ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.iosxr
contient une fonction run_commands()
que nous pouvons utiliser :
command = "show crypto key authentication rsa all" out = run_commands(module, command) out = out[0].replace(' \n', '\n')
Une bibliothèque assez courante pour extraire des informations du résultat d’une commande est textfsm : un module Python utilisant une machine à état basée sur des patrons pour analyser un texte semi-formaté.
template = r""" Value Required Label (\w+) Value Required,List Data ([A-F0-9 ]+) Start ^Key label: ${Label} ^Data\s+: -> GetData GetData ^ ${Data} ^$$ -> Record Start """.lstrip() re_table = textfsm.TextFSM(io.StringIO(template)) got = {data[0]: "".join(data[1]).replace(' ', '') for data in re_table.ParseText(out)}
got
est un dictionnaire associant des noms de clefs, assimilés à des
noms d’utilisateurs, avec une représentation hexadécimale de la clé
publique actuellement installée. Il ressemble à ceci :
>>> pprint(got) {'alfred': '30820122300D0609[…]6F0203010001', 'cedric': '30820122300D0609[…]710203010001', 'vincent': '30820122300D0609[…]270203010001'}
Comparer avec l’état cible#
Construisons le dictionnaire wanted
avec une structure similaire.
Dans module.params['keys']
, nous avons un dictionnaire associant les
noms d’utilisateurs aux clefs publiques SSH au format OpenSSH :
>>> pprint(module.params['keys']) {'cedric': 'ssh-rsa AAAAB3NzaC1yc2[…]', 'vincent': 'ssh-rsa AAAAB3NzaC1yc2[…]'}
Nous devons les convertir en utilisant la représentation hexadécimale
utilisée par Cisco. La commande ssh-keygen
et un peu d’huile de
coude permettent de faire cette conversion2 :
$ ssh-keygen -f id_rsa.pub -e -mPKCS8 \ > | grep -v '^---' \ > | base64 -d \ > | hexdump -e '4/1 "%0.2X"' 30820122300D06092[…]782D270203010001
En supposant que nous avons une fonction ssh2cisco()
pour faire
cette conversion, nous pouvons construire le dictionnaire wanted
:
wanted = {k: ssh2cisco(v) for k, v in module.params['keys'].items()}
Appliquer les changements#
Pour revenir au squelette décrit dans l’article précédent, la dernière étape consiste à appliquer les
changements s’il y a une différence entre got
et wanted
lorsqu’on
ne fonctionne pas en mode vérification. La partie comparant got
et
wanted
ne contient pas de modifications par rapport au squelette :
if got != wanted: result['changed'] = True result['diff'] = dict( before=yaml.safe_dump(got), after=yaml.safe_dump(wanted) ) if module.check_mode or not result['changed']: module.exit_json(**result)
Copions les nouvelles clefs ou celles qui ont changé et attachons les
à leur utilisateurs respectifs. À cet effet, nous réutilisons les
fonctions get_connection()
et copy_file()
de
ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.iosxr
.
conn = get_connection(module) for user in wanted: if user not in got or wanted[user] != got[user]: dst = f"/harddisk:/publickey_{user}.raw" with tempfile.NamedTemporaryFile() as src: decoded = base64.b64decode( module.params['keys'][user].split()[1]) src.write(decoded) src.flush() copy_file(module, src.name, dst) command = ("admin crypto key import authentication rsa " f"username {user} {dst}") conn.send_command(command, prompt="yes/no", answer="yes")
Enfin, retirons les clefs obsolètes :
for user in got: if user not in wanted: command = ("admin crypto key zeroize authentication rsa " f"username {user}") conn.send_command(command, prompt="yes/no", answer="yes")
Le code complet est disponible sur GitHub. Par rapport au
module iosxr_user
, ce module affiche les différentces
lorsqu’il est lancé avec --diff
, signale correctement les
changements, est plus rapide3 et supprime les clés SSH indésirables.
Cependant, il est incapable de créer des utilisateurs et ne peut pas
définir des mots de passe ou plusieurs clefs SSH pour un même
utilisateur.
-
Dans notre environnement, Ansible déploie une configuration complète, comprenant la définition des utilisateurs. Ensuite, il synchronise les clés SSH. ↩︎
-
Malgré l’argument fourni à
ssh-keygen
, le format utilisé par Cisco n’est pas PKCS#8. Il s’agit de la représentation ASN.1 d’une structure Subject Public Key Info, telle que définie dans la RFC 2459. De plus, PKCS#8 est un format pour une clé privée, et non une clé publique. ↩︎ -
Les principales différences expliquant cette amélioration des performances sont de :
- ne pas créer des utilisateurs,
- ne pas renvoyer les clefs existantes.