Road to Ansible : je construis mon réseau NSX-T comme un grand

Deuxième épisode de cette longue route dans la maitrise de l’automatisation avec Ansible, on s’attaque maintenant au design réseau NSX-T. Là, on change de braquet par rapport à la création de petites VM dans leur coin, il s’agit de construire des desgins cohérents et ordonnés de segments, routeurs, règles DFW, services DHCP et j’en passe. Ca rigole plus du tout (en fait si, on continue à s’amuser, rassurez-vous).

Nous en étions restés, dans mon précédent volet, à la création et la suppression de petites VM, un peu customisées au niveau réseau. Désormais pour pouvoir exploiter NSX-T et son jeu d’API, il faut rajouter des bibliothèques spécifiques à notre instance Ansible. VMware a publié cela sous github, directement. Autrement dit, il « suffit » de cloner le repository en question au bon endroit, pour qu’Ansible s’y retrouve.

Installons, que diable !

Personnellement et comme on est chez nous, dans notre coin, je n’ai pas fait dans le compliqué. J’ai créé 3 répertoires imbriqués .ansible/plugins/modules à la racine de mon homedir. Sur Mac, ce sera du /Users/moncomptesuperclasse/, sur Linux/Unix, en général vous serez sur un /home/moncomptebarbu/ … et sur Windows, si vous tenez vraiment à travailler sur ce machin (du calme les haters, je plaisante ^^) ce sera sans doute sur /Users/monsupercompteactivedirectoryquejai-mais-jai-encore-des-problemes-de-roaming/.

Ensuite, après avoir installé la commande « git » (yum install git, apt install git … vous connaissez), il vous suffit de cloner le dépot https://github.com/vmware/ansible-for-nsxt.git dans le sous-répertoire « modules ». Ca donne ça :

cd ~
mkdir -p .ansible/plugins/modules
cd .ansible/plugins/modules
git clone https://github.com/vmware/ansible-for-nsxt.git 

… et le tour est joué. Vous êtes prêt à tater du NSX-T dans Ansible !

Premier essai, que diable !

Alors, on commence par quoi ? Question intéressante ? Le plus simple (mais pas le plus logique), clairement c’est la création d’un segment avec un range dédié sous une hiérarchie TIER1/TIER0 déjà montée. Pas de fioritures style DHCP… juste un petit segment avec son subnet et sa gateway. Vous allez voir, c’est super facile :

---
- name: Création premier segment, youhou !
  hosts: localhost
  tasks:
  - name: Variables globales
    include_vars: vars.yml
  - name: Creation du segment sous le TIER1
    nsxt_policy_segment:
      hostname: "{{ vars_nsxHostname }}"
      username: "{{ vars_nsxUserName }}"
      password: "{{ vars_nsxPassword }}"
      validate_certs: False
      display_name: "seg-ansible-0"
      state: "present"
      tier1_display_name: "tier1-ansible"
      transport_zone_display_name: "nsx-overlay-transportzone"
      subnets:
      - gateway_address: "192.168.42.254/24"

… exécutons cela :

cidou@posseidon:~/Documents/tests-ansible$ ansible-playbook  createSEG.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [Création premier segment, youhou !] *******************************************************
TASK [Gathering Facts] **************************************************************************
ok: [localhost]

TASK [Variables globales] ***********************************************************************
ok: [localhost]

TASK [Creation du segment sous le TIER1] ********************************************************
changed: [localhost]

PLAY RECAP **************************************************************************************
localhost: ok=3    changed=1    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

cidou@posseidon:~/Documents/tests-ansible$

You-hou, qu’est ce que je vois sur mon NSX-T ? Un nouveau segment tout neuf, créé avec sa gateway comme décrit dans le YAML. Mine de rien, même avec ce petit truc, on gagne beaucoup de temps, quand vous avez à monter 5/6/7 segments d’affilée quand vous avez un nouvel environnement d’hébergement à monter, croyez-moi (hein Romain, qu’on en gagne du temps ^^). Evidement, comme déjà vu dans le billet précédent, il vous suffit d’indiquer le chaine « absent » en face de la directive « state: » et en un tournemain, le segment est détruit (sous réserve qu’il ne soit pas déjà utilisé, bien sûr).

Au passage, le WARNING concernant l’absence d’inventaire est lié au fait qu’on n’applique pas ce playbook sur un ensemble de machines données, mais que l’on a juste besoin que du « localhost » c’est à dire notre propre machine pour piloter les instances vSphere ou NSX-T via leur jeu d’API. Si vous souhaitez utiliser les playbook pour par exemple sécuriser les serveurs SSH d’un ensemble de machines en enlevant le PermitRootLogin (c’est juste un exemple ^^) dans le fichier de config sshd_config … là, pour le coup, vous aurez besoin de décrire les hosts cibles et leur identité.

Maintenant qu’on sait créer un segment, plus dur, on s’occupe d’un TIER1, que diable !

Vous allez voir que ce n’est pas plus compliqué, ou presque. Le YAML ressemble comme deux gouttes d’eau à celui du segment :

---
- name: Création TIER1 NSX
  hosts: localhost
  tasks:
  - name: Variables globales
    include_vars: vars.yml
  - name: Creation TIER1
    nsxt_policy_tier1:
      hostname: "{{ vars_nsxHostname }}"
      username: "{{ vars_nsxUserName }}"
      password: "{{ vars_nsxPassword }}"
      validate_certs: False
      display_name: "tier1-ansible-bis"
      state: "present"
      failover_mode: "PREEMPTIVE"
      enable_standby_relocation: False
      route_advertisement_types: ['TIER1_CONNECTED']
      tier0_display_name: "tier0-pfsense"

Vous noterez malgré tout que quelques directives spécifiques ont été rajoutées, comme le route_advertisement_types, dans lequel on indique la propriété du TIER1 à avertir son TIER0 de tous les segments connectés directement via l’ajout de « TIER1_CONNECTED », mais on aurait aussi pu y rajouter d’autres propriétés comme « TIER1_NAT » ou « TIER1_STATIC_ROUTES ». Après exécution du playbook, on se retrouve avec un nouveau TIER1 « tier1-ansible-bis », qui vient se connecter au tier0-pfsense, indiqué avec la variable tier0_display_name. Pour en savoir plus sur tous ces paramètres de configuration, personnellement, je prends directement mes sources dans la documentation du module, par l’intermédiaire de la commande Ansible ansible-doc nsxt_policy_tier1. L’équivalent d’un man en somme. Ceci étant vous pouvez aller dans le code source des modules NSX-T en question via github, tout est documenté.

On va quand même pas déjà écrire une policy DFW si vite, que diable ?

Bien sur que si, mais, là, il va falloir au préalable « préparer le terrain » en création les groupes de sécurité associés. Vous le savez sans doute, mais NSX-T ne travaille jamais avec des ranges ou des IP brutes, il faut d’abord les intégrer dans des groupes de sécurité avant de pouvoir les intégrer à des politiques DFW puis des règles firewall à l’intérieur de ces politiques. C’est précisément là que ça se corse un peu. En théorie la création d’un groupe de sécu semble assez simple, mais c’est dans la description des membres dudit groupe qu’il faut être très précis.

Première chose à faire, donc créer le fameux groupe de sécurité. On va l’appeler « sg-allow-ssh ». On va y intégrer deux/trois éléments pour visualiser comment ça s’articule. Et après, on commente comme d’habitude :

---
- name: Création d'un groupe de sécu
  hosts: localhost
  tasks:
  - name: Variables globales
    include_vars: vars.yml
  - name: creation du groupe sg-allow-ssh
    nsxt_policy_group:
      hostname: "{{ vars_nsxHostname }}"
      username: "{{ vars_nsxUserName }}"
      password: "{{ vars_nsxPassword }}"
      validate_certs: False
      display_name: sg-allow-ssh
      state: present
      domain_id: "default"
      expression:
        - resource_type: "Condition"
          member_type: "VirtualMachine"
          value: "vmToto"
          key: "Name"
          operator: "EQUALS"
        - resource_type : "ConjunctionOperator"
          conjunction_operator: "OR"
        - resource_type : "IPAddressExpression"
          ip_addresses: ["1.2.3.4","10.0.0.1","192.168.0.0/24"]

Toute la première partie jusqu’à la directive « expression » est assez familière maintenant. Il y a tout de même la variable « domain_id », qui, d’après ce que j’ai compris, est utilisé pour la fédération multi NSX-T et pour le cloud (si quelqu’un a l’info ou peut confirmer, je suis preneur, car c’est loin d’être clair). Pour du mono-instance, comme beaucoup d’entre nous, « default » est une valeur qui convient ^^.

Ensuite, voici le truc un peu « tricky » comme disent les anglophones. Il faut construire une expression qui va être composé d’éléments qui vont liés entre eux via des opérateurs logiques (OR, AND). Si on regarde comment est construit un groupe, on peut faire des trucs très complexes avec des ports de segments, des mac address, des IP, des VM, des tags NSX etc … pour le coup, on va rester simple, d’autant que l’exemple que j’ai pris devrait facilement être adapté à vos contextes.

Si vous suivez bien dans la syntaxe du YAML. Vous repérez la structure d’une liste (les différents items pré-fixés par « -« ). Ici, on liste, dans l’ordre, trois « resource_type différents » : une « Condition », une « ConjunctionOperator » et enfin, une « IPAddressExpression ». En gros, ce qui est écrit signifie : je veux que que mon groupe soit composé d’un ensemble d’IP et d’une VM dont le nom est vmToto, quel que soit ce qui matche (OR), ça fait partie du groupe. Le groupe est constitué d’une VM et de 3 déclarations IP et tous ces membres font partie du groupe.

Dans la pratique, si vous regardez bien, la définition d’un groupe correspond exactement à cela. Et c’est d’ailleurs grâce à une astuce que j’ai compris ce « langage » de description assez chiadé. J’ai tout simplement extrait un groupe de sécu directement avec l’API NSX-T pour voir comment elle le décrivait en JSON. Pour notre fameux groupe, une fois créé, ça donne effectivement ça :

cidou@posseidon:~/Desktop$ curl -u admin:TOto44+-VLBE -k https://nsx.myvlab.io/policy/api/v1/infra/domains/default/groups/sg-allow-ssh
{
  "expression" : [ {
    "member_type" : "VirtualMachine",
    "key" : "Name",
    "operator" : "EQUALS",
    "value" : "vmToto",
    "resource_type" : "Condition",
    "id" : "f00a2c1d-72ec-4850-a104-d1febb858781",
    "path" : "/infra/domains/default/groups/sg-allow-ssh/condition-expressions/f00a2c1d-72ec-4850-a104-d1febb858781",
    "relative_path" : "f00a2c1d-72ec-4850-a104-d1febb858781",
    "parent_path" : "/infra/domains/default/groups/sg-allow-ssh",
    "marked_for_delete" : false,
    "overridden" : false,
    "_protection" : "NOT_PROTECTED"
  }, {
    "conjunction_operator" : "OR",
    "resource_type" : "ConjunctionOperator",
    "id" : "9d587b81-506c-455c-876f-e7b8279104f3",
    "path" : "/infra/domains/default/groups/sg-allow-ssh/conjunction-expressions/9d587b81-506c-455c-876f-e7b8279104f3",
    "relative_path" : "9d587b81-506c-455c-876f-e7b8279104f3",
    "parent_path" : "/infra/domains/default/groups/sg-allow-ssh",
    "marked_for_delete" : false,
    "overridden" : false,
    "_protection" : "NOT_PROTECTED"
  }, {
    "ip_addresses" : [ "1.2.3.4", "10.0.0.1", "192.168.0.0/24" ],
    "resource_type" : "IPAddressExpression",
    "id" : "82aa4e6f-2439-4f71-803f-f18b7c34b30d",
    "path" : "/infra/domains/default/groups/sg-allow-ssh/ip-address-expressions/82aa4e6f-2439-4f71-803f-f18b7c34b30d",
    "relative_path" : "82aa4e6f-2439-4f71-803f-f18b7c34b30d",
    "parent_path" : "/infra/domains/default/groups/sg-allow-ssh",
    "marked_for_delete" : false,
    "overridden" : false,
    "_protection" : "NOT_PROTECTED"
  } ],
  "extended_expression" : [ ],
  "reference" : false,
  "resource_type" : "Group",
  "id" : "sg-allow-ssh",
  "display_name" : "sg-allow-ssh",
  "path" : "/infra/domains/default/groups/sg-allow-ssh",
  "relative_path" : "sg-allow-ssh",
  "parent_path" : "/infra/domains/default",
  "unique_id" : "015897e4-0daf-4e83-8875-79318ba6e790",
  "marked_for_delete" : false,
  "overridden" : false,
  "_create_user" : "admin",
  "_create_time" : 1614803167219,
  "_last_modified_user" : "admin",
  "_last_modified_time" : 1614808252537,
  "_system_owned" : false,
  "_protection" : "NOT_PROTECTED",
  "_revision" : 3
}
cidou@posseidon:~/Desktop$

On retrouve ces fameux resource_type, dans le même ordre.

Faut peut-être y aller et nous montrer une DFW policy, que diable ?

Vous avez raison, et on va terminer par ça pour ce deuxième billet. L’objectif est, je le rappelle, de créer une nouvelle règle DFW. Voici le playbook Ansible :

---
- name: Création policy/première rule DFW
  hosts: localhost
  tasks:
  - name: Variables globales
    include_vars: vars.yml
  - name: Creation du policy et regle de base
    nsxt_policy_security_policy:
      hostname: "{{ vars_nsxHostname }}"
      username: "{{ vars_nsxUserName }}"
      password: "{{ vars_nsxPassword }}"
      validate_certs: False
      display_name: "pol-allow-ssh-http-https"
      state: "present"
      description: "Politique allowSSH+allowHTTP.S"
      category: Environment
      rules:
        - action: "ALLOW"
          display_name: rule1-ssh
          source_groups: /infra/domains/default/groups/sg-allow-ssh
          destination_groups: Any
          direction: IN_OUT
          services: ["/infra/services/HTTP", "/infra/services/HTTPS"]
          ip_protocol: IPV4
          scope: /infra/domains/default/groups/sg-allow-ssh
        - action: "ALLOW"
          display_name: rule2-ssh
          source_groups: /infra/domains/default/groups/sg-allow-ssh
          destination_groups: Any
          direction: IN_OUT
          services: ["/infra/services/SSH"]
          ip_protocol: IPV4
          scope: /infra/domains/default/groups/sg-allow-ssh

Nous y sommes ! La politique est créé et ressemble à ça :

Détaillons tout cela. La encore, la déclaration initiale de la politique qui encadre les deux règles est relativement simple à comprendre, on peut noter qu’on choisit dans quelle « section » du DFW on va placer celle-ci en indiquant on mot-clé dans la variable « category ». Là, je l’ai placé dans Environment, mais les autres « Emergency », « Infrastructure » ou encore « Application » sont aussi possibles.

Ensuite, pour chaque règle, les choses sont aussi assez simples : source, destination, service/port, le scope (le fameux Apply-To, voir ce billet) et le comportement attendu, ALLOW, DROP ou REJECT. Vous pouvez aussi constater que les informations de groupes, scopes et autre services, sont indiqués via des chemins logiques. Pour les trouver, rien de plus simple, allez dans votre console NSX, placez-vous sur la ligne de l’élément en question dans la section ad-hoc, puis cliquez sur les trois points en tête de ligne : vous avez un option « Copy path to Clipboard » qui contient ce chemin.

Faut peut-être finir ce billet un peu long, que diable !

Ce deuxième billet s’achève sur un bien beau résultat, nous sommes arrivés à nos fins : créer un TIER1, un segment, un security group et une politique de filtrage DFW. Digérez bien tout cela, car la prochaine fois, on s’attaque à des trucs un peu alambiqués du style DHCP Server, DNS et autre ^^. Mais ce sera pour un peu plus tard en Mars, la je vais me coucher après une journée de boulot chargée et ENFIN, deux billets de publiés sur mon nouveau chouchou, Ansible, alors qu’ils sont en gestation depuis des semaines :)

Faites de beaux rêves :)

Références

Medium, NSX-T, partie 1 : https://medium.com/swlh/nsx-t-security-with-ansible-pt1-basic-firewall-rules-6aa08c25e226
Medium, NSX-T, partie 2 : https://medium.com/@laidbackware/nsx-t-security-with-ansible-pt-2-ns-groups-665edd8bac6
La doc de l’API NSX-T, directement chez vous, On Premise sur votre console NSX : https://votremanagerNSX-T/policy/api.html
Ansible For NSX-T : https://github.com/vmware/ansible-for-nsxt