--- # Devis d'etancheite de la frontiere — RELEVE seul. Ne modifie rien (D-23). # # Question posee : la frontiere laisse-t-elle passer ce que la politique n'autorise PAS ? # `devis-expositions.yml` pose la question inverse — chaque exposition declaree repond-elle. # Les deux sont necessaires : un pare-feu peut servir tout ce qu'on lui demande *et* # laisser passer tout le reste. # # LES CIBLES SONT LES PORTS REELLEMENT EN ECOUTE, releves ici. Sonder un port ferme ne # prouverait rien du pare-feu : le refus viendrait de la machine, pas de la bordure. # # LE CONTROLE D'ABORD. La sonde interroge une adresse ou personne n'ecoute avant tout # verdict ; si elle repond, le releve est declare NUL. C'est la lecon du 2026-08-09 : # `connect()` a menti deux jours durant a travers un `synproxy` herite. # # make frontiere-mesurer - name: Relevé des ports réellement en écoute hosts: hotes_actifs become: true gather_facts: false tasks: - name: Recenser les ports TCP en écoute ansible.builtin.shell: executable: /bin/bash # `|| true` en fin de chaîne : avec `pipefail`, un `grep -v` qui ne garde rien # tuerait la tâche. Un hôte sans écoute publique est un relevé vide, pas une panne. cmd: >- set -o pipefail; ss -ltnH | awk '{print $4}' | grep -vE '^127\.|^\[::1\]' | sed 's/.*://' | sort -un | tr '\n' ' ' || true register: devis_frontiere_ss changed_when: false - name: Retenir l'écoute de cet hôte ansible.builtin.set_fact: devis_frontiere_ecoute: ip: "{{ ansible_host }}" ports: "{{ devis_frontiere_ss.stdout.split() | map('int') | list }}" - name: Sondage depuis les deux points de vue hosts: localhost connection: local become: false gather_facts: false tasks: - name: Rassembler l'écoute de toute la flotte ansible.builtin.set_fact: devis_frontiere_ports: >- {{ dict(groups['hotes_actifs'] | map('extract', hostvars, 'devis_frontiere_ecoute') | list | zip(groups['hotes_actifs']) | map('reverse') | list) }} # Forme `argv` et non `cmd` : le chemin du dépôt peut contenir une espace, et `cmd` # découpe dessus. Mesuré ici même — « /home/.../Espace Chezlepro/... » cassait. - name: Dériver les cibles à sonder depuis la politique ansible.builtin.command: argv: - python3 - "{{ playbook_dir }}/../../scripts/devis_frontiere.py" - --cibles stdin: "{{ devis_frontiere_ports | to_json }}" register: devis_frontiere_cibles changed_when: false - name: Sonder depuis le VLAN d'administration (le poste de l'exploitant) ansible.builtin.command: argv: - python3 - "{{ playbook_dir }}/../../scripts/sonde_tcp.py" stdin: >- {{ {'controle': (devis_frontiere_cibles.stdout | from_json).controle, 'cibles': (devis_frontiere_cibles.stdout | from_json).admin} | to_json }} register: devis_frontiere_admin changed_when: false # Play distinct, et ce n'est pas cosmétique. Dans un play `connection: local`, un # `delegate_to` hérite de cette connexion : la sonde « tenant » s'exécutait en réalité sur # le poste. Elle rendait donc la frontière JOIGNABLE — depuis le VLAN d'administration, # elle l'est. Mesuré et corrigé le 2026-08-09 ; c'est la même famille de piège que le # `connect()` : vérifier d'où l'instrument mesure avant de croire ce qu'il dit. - name: Sondage depuis un hôte du tenant (le sens sortant) # Un seul hôte suffit : le devis mesure la bordure, pas les hôtes. Les règles sortantes # portent sur l'alias du tenant, donc sur tous ses hôtes indistinctement. # Motif `groupe[0]` et non un template : un `hosts:` templaté échoue quand l'inventaire # est vide — c'est ce qui cassait `ansible-lint`, qui s'exécute sans inventaire. hosts: hotes_actifs[0] become: false gather_facts: false tasks: - name: Sonder les destinations internes interdites ansible.builtin.script: cmd: >- "{{ playbook_dir }}/../../scripts/sonde_tcp.py" '{{ {"controle": (hostvars["localhost"].devis_frontiere_cibles.stdout | from_json).controle, "cibles": (hostvars["localhost"].devis_frontiere_cibles.stdout | from_json).tenant} | to_json }}' args: executable: python3 register: devis_frontiere_tenant changed_when: false - name: Déposer le relevé sur le contrôleur ansible.builtin.copy: dest: "{{ playbook_dir }}/../../instance/devis-frontiere.json" mode: "0600" content: >- {{ { 'admin': hostvars['localhost'].devis_frontiere_admin.stdout | from_json, 'tenant': devis_frontiere_tenant.stdout | from_json, 'sonde_depuis': inventory_hostname, } | to_nice_json }} delegate_to: localhost become: false