--- # Devis des certificats — RELEVE seul, sur toute la flotte. Ne modifie rien (D-23). # # Le defaut qu'il traque est connu et recurrent : un certificat renouvele SUR DISQUE # mais toujours servi PERIME depuis la memoire du service, tant que nginx / postfix / # dovecot / slapd n'ont pas ete recharges. Rien ne le signale — le fichier est bon, # `step` est content, et le navigateur voit un certificat expire. # # La methode ne suppose rien du service : on releve l'empreinte du certificat sur # disque, puis l'empreinte REELLEMENT SERVIE par chaque port TLS en ecoute. Un port # qui sert un certificat portant le meme sujet mais une autre empreinte tient une # copie perimee en memoire. # # make certificats-plan - name: Devis des certificats — disque contre mémoire hosts: client_pki become: true gather_facts: true vars: devis_certificats_sortie: "{{ playbook_dir }}/../../instance/devis-certificats.json" tasks: # PRECEDENCE — `include_vars` au niveau du play PRIME sur les `group_vars`. # Charger les defauts d'un role tels quels ferait donc mentir le devis : il # comparerait au defaut du depot au lieu de la valeur reellement declaree par # l'inventaire. Mesure du 2026-08-08 : le premier jet rapportait # `client_pki_reload_services: []` sur les 14 hotes alors que quatre groupes le # declarent. Le devis portait exactement le defaut qu'il est cense traquer. # # On retient donc ce que l'inventaire declare AVANT de charger les defauts, puis # on le reimpose. (Charger sous un `name:` ne marche pas : les defauts se citent # entre eux — `client_pki_cert` reference `client_pki_steppath` — et ces # references ne se resolvent plus une fois enfermees dans un dictionnaire.) - name: Retenir ce que l'inventaire déclare pour client_pki ansible.builtin.set_fact: devis_inventaire: >- {{ hostvars[inventory_hostname] | dict2items | selectattr('key', 'match', '^client_pki_') | items2dict }} - name: Charger les défauts du rôle client_pki ansible.builtin.include_vars: dir: "{{ playbook_dir }}/../../roles/client_pki/defaults" # Cle par cle : `set_fact` ne prend pas un dictionnaire entier en argument libre — # il l'accepte sans erreur et n'en fait rien. C'est ainsi que le premier correctif # a paru fonctionner tout en laissant le devis mentir. - name: Rendre le dernier mot à l'inventaire ansible.builtin.set_fact: "{{ item.key }}": "{{ item.value }}" loop: "{{ devis_inventaire | dict2items }}" loop_control: label: "{{ item.key }}" - name: Relever le certificat sur disque ansible.builtin.command: argv: - openssl - x509 - -in - "{{ client_pki_cert }}" - -noout - -subject - -fingerprint - -enddate - -issuer register: devis_cert_disque changed_when: false failed_when: false - name: Relever les ports TLS en écoute et le certificat que chacun sert ansible.builtin.shell: executable: /bin/bash cmd: | set -uo pipefail for P in $(ss -H -ltn 2>/dev/null | awk '{print $4}' | sed 's/.*://' | sort -un); do # STARTTLS n'est pas optionnel sur ces ports : sans le dialecte, la poignee # de main n'a pas lieu et on conclurait a tort que le port ne sert rien. case "$P" in 25|587) ARG="-starttls smtp" ;; 143) ARG="-starttls imap" ;; 110) ARG="-starttls pop3" ;; 389) ARG="-starttls ldap" ;; *) ARG="" ;; esac OUT=$(timeout 5 openssl s_client -connect 127.0.0.1:"$P" $ARG \ -servername {{ ansible_fqdn }} /dev/null \ | openssl x509 -noout -subject -fingerprint -enddate 2>/dev/null) if [ -n "$OUT" ]; then echo "PORT=$P" echo "$OUT" fi done register: devis_cert_servis changed_when: false - name: Déposer le relevé sur le contrôleur ansible.builtin.copy: dest: "{{ devis_certificats_sortie }}.{{ inventory_hostname }}" mode: "0600" content: >- {{ { 'hote': inventory_hostname, 'fqdn': ansible_fqdn, 'chemin': client_pki_cert, 'disque': devis_cert_disque.stdout | default(''), 'disque_rc': devis_cert_disque.rc | default(1), 'servis': devis_cert_servis.stdout | default(''), 'recharges_declares': client_pki_reload_services, } | to_nice_json }} delegate_to: localhost become: false