devis_expositions pose la question positive (une exposition declaree repond-elle). Celle-ci manquait, et ce n'est pas la meme : un pare-feu peut servir tout ce qu'on lui demande ET laisser passer tout le reste. Rien n'y est saisi. Les cibles sont les ports REELLEMENT en ecoute (sonder un port ferme ne prouverait rien du pare-feu) ; la politique attendue est lue dans devis_opnsense.py, la source qui configure la frontiere. scripts/sonde_tcp.py refuse de conclure : un CONTROLE (adresse ou personne n'ecoute) avant tout verdict — s'il repond, le releve est declare NUL. Et etablir n'est pas livrer : banniere, sinon requete HTTP, sinon poignee TLS ; sans reponse le verdict est AMBIGU, jamais « ouvert ». Deux pieges rencontres en le construisant, corriges et commentes sur place : la sonde « tenant » s'executait sur le poste (dans un play connection: local, delegate_to herite de cette connexion) ; et 2 s de lecture faisaient ressortir un Postfix sain en AMBIGU (postscreen retarde sa banniere expres) — porte a 8 s, compense par du parallelisme. Premier verdict, frontiere en l'etat : 38 ecarts, dont collab-01:9980 qui livre vraiment un HTTP 200 depuis le poste. Releve sortant declare NUL. Verifie : ansible-lint Passed, prouver.py 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
118 lines
4.9 KiB
YAML
118 lines
4.9 KiB
YAML
---
|
|
# 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
|