Salt Junos Proxy Minion

Salt, de la société SaltStack, est un sujet dont je n’ai pas parlé depuis plus d’un an car le support de Python3 pour le « Syslog Engine » n’était pas fonctionnel et nécessitait d’utiliser un environnement de développement spécifique.

Entre le fait que ce problème soit réglé et que SaltStack ait été rachetée par VMWare, c’est le bon moment pour proposer un rappel sur cette solution qui ne manque pas d’intérêt pour faire du « Human-driven Automation », du « Event-driven Automation » et du « Machine-driven Automation » avec un outil structurant.

Salt utilise un modèle de communication serveur-agent, bien qu’il fonctionne aussi en tant qu’utilitaire de gestion de serveur unique autonome, et offre également la possibilité de s’exécuter sans agent via SSH.

Flexible Network Control
Source SaltStack

Le composant serveur s’appelle « Salt master » et l’agent s’appelle « Salt minion ». Le « Salt master » est responsable de l’envoi des commandes aux « Salt minions », puis de l’agrégation et de l’affichage des résultats de ces commandes. Un seul « Salt Master » peut gérer des milliers de systèmes. Salt communique avec les systèmes gérés à l’aide d’un modèle « publish-subscribe ». Les connexions sont initiées par le « Salt Minion », ce qui signifie que vous n’avez pas besoin d’ouvrir de ports entrants sur ces systèmes (réduisant ainsi le vecteur d’attaque). Le « Salt Master » utilise les ports 4505 et 4506, qui doivent être ouverts pour accepter les connexions entrantes.

Salt master minion
Source SaltStack

Lorsque le « Salt minion » démarre pour la première fois, il recherche sur le réseau un système nommé salt (bien que cela puisse être facilement changé en une adresse IP ou un nom d’hôte différent). Une fois trouvé, le minion initie un « handshake » et envoie ensuite sa clé publique au « Salt master ». Après cette connexion initiale, la clé publique du « Salt minion » est stockée sur le serveur et doit être acceptée sur le « Salt Master » à l’aide de la commande salt-key (ou via un mécanisme automatisé). Le « Salt minion » n’exécutera aucune commande tant que sa clé ne sera pas acceptée . Une fois la clé acceptée, le « Salt master » renvoie sa clé publique avec une clé AES rotative qui est utilisée pour crypter et décrypter les messages envoyés par le « Salt master ». La clé AES retournée est chiffrée à l’aide de la clé publique initialement envoyée par le « Salt minion », et ne peut donc être déchiffrée que par ce « Salt minion ».

Toutes les communications ultérieures entre le « Salt master » et le « Salt minion » sont cryptées à l’aide de clés AES. Une clé AES rotative est utilisée pour crypter les jobs envoyés au « Salt Minion » par le « Salt master » et pour crypter les connexions au serveur de fichiers du « Salt master ». Une nouvelle clé est générée et utilisée à chaque redémarrage du « Salt master » et à chaque fois qu’une clé de « Salt minion » est supprimée à l’aide de la commande salt-key.

Pour accéder aux équipements Juniper, un proxy Junos tourne sur le « Salt minion » pour chaque équipement à administrer. Le proxy Junos se connecte au « Salt master » à l’aide du bus d’événements ZeroMQ et utilise la librairie Juniper Junos PyEZ pour établir une session NETCONF via SSH avec l’équipement exécutant Junos OS. Le « Junos syslog engine » surveille les messages syslog envoyés par les équipements, extrait les informations sur les événements et les publie au format Salt sur le bus d’événements Salt.

Pour ce lab, tous les composants Salt sont sur la même machine virtuelle Ubuntu 20.04 et accèdent à un switch Juniper vQFX light, en version 19.4R1.10, tournant sur le simulateur EVE-NG. L’authentification par certificat est utilisée vis à vis de l’équipement Junos, sur lequel le service « NETCONF-over-SSH » est activé.

Junos syslog engine

La configuration de l’équipement et du « Salt master » peut être synthétisée comme décrit ci-dessous.

sudo apt update
sudo apt install curl
curl -o bootstrap_salt.sh -L https://bootstrap.saltstack.com
sudo sh bootstrap_salt.sh -P -M -x python3
sudo mkdir -p /srv/{salt,pillar,reactor}
sudo mkdir /var/log/salt/backup-configs/
sudo apt install python3-pip
sudo pip3 install junos-eznc
sudo pip3 install jxmlease
sudo pip3 install twisted
ssh-keygen -t rsa -b 2048
python3 -m http.server 8000 --bind 192.168.199.220
set system login user gilbert authentication load-key-file http://192.168.199.220:8000/.ssh/id_rsa.pub
set system login user gilbert class super-user
set system services netconf ssh
set system syslog host 192.168.199.220 any any
set system syslog host 192.168.199.220 port 9999
#/etc/salt/master
file_roots:
  base:
    - /srv/salt
pillar_roots:
  base:
    - /srv/pillar
engines:
  - junos_syslog:
      port: 9999
reactor:
  - 'jnpr/syslog/*/UI_COMMIT_COMPLETED':
    - /srv/reactor/junos_backup_on_commit.sls
#/srv/pillar/vqfx-re-proxy.sls
proxy:
  proxytype: junos
  host: 192.168.199.62
  username: gilbert
  ssh_private_key_file: /home/gilbert/.ssh/id_rsa
  port: 830
#/srv/pillar/top.sls
base:
  'vqfx-re':    # proxy minion name
    - vqfx-re-proxy    # pillar file
#/srv/reactor/junos_backup_on_commit.sls
Backup config for commit complete event:
  local.state.apply:
    - tgt: {{ data['hostname'] }}
    - arg:
        - junos_backup_config
#/srv/salt/junos_backup_config.sls
{% set curtime = None | strftime("%Y-%m-%d-%H-%M-%S") %}
get_config:
  junos.rpc:
    - dest: /var/log/salt/backup-configs/{{ grains['id'] }}-config-{{ curtime }}.conf
    - format: text

Une fois ces configurations réalisées, on peut redémarrer le « Salt master » à l’aide des commandes :

sudo killall salt-master
sudo salt-master -d

Dans cet exemple, le « Salt minion » est sur la même machine virtuelle que le « Salt master », ce qui simplifie les actions à mener.

#/etc/salt/proxy
master: localhost

Le proxy peut être démarré à l’aide de la commande : sudo salt-proxy --proxyid=vqfx-re -d

Comme expliqué précédemment, la clé du « Proxy minion » doit être autorisée sur le « Salt master » à l’aide de la commande : sudo salt-key -a vqfx-re ou sudo salt-key -A.

Avec ce paramétrage, il est possible de réaliser des interrogations et configurations d’équipements, sachant que chaque fois qu’un « commit » sera réalisé sur l’équipement, une sauvegarde de la configuration sera effectuée sur la machine Salt en mode « Machine-driven Automation ». En effet, chaque fois que l’évènement UI_COMMIT_COMPLETED sera observé, le « reactor » correspondant sera activé.

L’analyse des évènements reçus par le « Syslog engine » peut être réalisée à l’aide de la commande : sudo salt-run state.event pretty=True

Voici des exemples de syntaxes de commandes :

sudo salt '*' test.ping
sudo salt vqfx* junos.facts 
sudo salt vqfx* grains.ls
sudo salt vqfx* grains.item os_family
sudo salt -G 'os_family:junos' junos.cli "show interfaces em0 terse" 
sudo salt vqfx* pillar.get proxy
sudo salt vqfx* network.arp
sudo salt vqfx* sys.list_modules
sudo salt vqfx* sys.list_functions
sudo salt -G 'os_family:junos' junos.cli "show configuration system syslog"
Reponse Salt
Résultat d’une commande CLI

Il est possible de réaliser un paramétrage d’équipement en appliquant des lignes de configuration.

#/srv/salt/myconfig.set
set system time-zone Europe/Paris
set system ntp boot-server 162.159.200.1
set system ntp server 37.187.104.44
sudo salt vqfx* junos.install_config 'salt://myconfig.set' replace='True'
Configuration avec Salt
Application de la configuration à l’aide d’un fichier de commandes

De façon plus avancée, il est également possible d’appliquer des états système. Le coeur du concept « Salt State » est le fichier SLS (SaLt State). Le SLS est une représentation de l’état dans lequel un système doit se trouver et est configuré pour contenir ces données dans un format simple. Cela s’appelle souvent la gestion de la configuration. Dans un contexte d’exploitation c’est le concept vers lequel il faut se tourner pour générer les configurations des équipements dans une approche « Infrastructure as Code ». Il faut noter que les fichiers SLS tirent parti du langage Jinja2 et du format de données YAML pour écrire des scénarios avancés de configuration et de contrôle d’état d’un système.

#/srv/pillar/infrastructure_data.sls
ntp_servers:
  - 162.159.200.1
  - 37.187.104.44
#/srv/pillar/top.sls
base:
  'vqfx-re':    # proxy minion name
    - vqfx-re-proxy    # pillar file
  'vqfx*':
    - infrastructure_data
#/srv/salt/infrastructure_config.conf
system {
  time-zone Europe/Paris;
  replace: ntp {
    boot-server {{ pillar.ntp_servers[0] }};
    server {{ pillar.ntp_servers[1] }};
  } 
}
#/srv/salt/provision_infrastructure.sls
Install the infrastructure services config:
  junos.install_config:
   - name: salt:///infrastructure_config.conf
   - replace: True
   - template_vars: True
   - timeout: 100

Une fois les fichiers configurés, on peut relancer la prise en compte des informations du « pillar » et appliquer le fichier « state ».

sudo salt vqfx* saltutil.refresh_pillar 
sudo salt vqfx* state.apply provision_infrastructure
Salt state apply
Application de la configuration à l’aide d’un fichier SLS

Comme évoqué à plusieurs reprises, automatiser les tests unitaires est une bonne pratique dans l’approche « Infrastructure as Code ». Voici un exemple pour tester le bon aboutissement de la configuration ntp à l’aide de la commande sudo salt vqfx* state.apply test-ntp qui utilise le fichier SLS ci-dessous.

{# /srv/salt/test-ntp.sls #}
{% set ntp_associations = salt['junos.rpc']('get-ntp-associations-information', 'json') %}

Print results of ntp associations test:
  module.run:
{% if '193.190.230.37' in ntp_associations['rpc_reply']['output'] %}
    - name: test.echo
      text:
        - ntp protocol up and running for {{ grains['id'] }}
{% else %}
    - name: test.exception
      message:
        - ntp issue on {{ grains['id'] }} node
{% endif %}
Résultat test ntp state
Résultat du test ntp

Cet autre exemple propose une autre syntaxe utilisant loop.until pour tester une valeur retour d’une requête RPC.

#/srv/salt/test-em0.sls
validate_logical_interface_em0:
  loop.until:
    - name: junos.rpc
    - condition: m_ret['rpc_reply']['interface-information']['physical-interface']['logical-interface']['name'] == 'em0.0'
    - period: 5
    - timeout: 10
    - m_args:
      - get-interface-information
    - m_kwargs:
        interface_name: 'em0'
        terse: True
Salt test loop
Test utilisant la fonction loop.until