LE CONSTAT. 39 roles declarent leurs flux, 32 leur empreinte, 32 leur authentification — tous derives. Et 19 groupes sur 19 declaraient une surveillance en prose que RIEN n executait ; Icinga en surveillait deux. La carte disait ce qui etait surveille, et personne ne surveillait. LE MECANISME. Un role declare ses sondes dans meta/supervision.yml et depose lui-meme son script dans /usr/local/lib/setops/sondes/. Le porteur client_sante les fait toutes tourner et pousse un resultat passif par sonde, sans savoir ce qu elles mesurent. serveur_icinga derive les objets Service ET le filtre de permission d API des memes declarations. Ajouter une sonde ne demande de toucher ni au porteur ni a Icinga. PREMIERE SONDE : client_pki/certificat. Heures restantes sur le certificat reellement pose, chaine verifiee, et empreinte SERVIE comparee au disque quand un service le consomme. 14/14 au tenant, 7/7 au site. QUATRE OBSTACLES, ET TROIS SONT LA MEME LECON. La sonde a rendu 14/14 en CRITIQUE sur une PKI saine : openssl verify -CAfile racine ne trouve pas l intermediaire qui signe nos certificats. step certificate verify, lui, repond VALIDE. Deployee au site, elle a rendu 5/7 : le seuil d avertissement (12 h) etait AU-DESSUS du point de renouvellement (8 h, le tiers restant). Elle criait avant que le mecanisme ne soit cense agir. Seuils ramenes a 6 h et 3 h. Un seuil se DERIVE du moment ou le mecanisme surveille agit. Une alarme toujours allumee ne vaut pas mieux qu une alarme jamais allumee : elle apprend a ne plus regarder. Une sonde se prouve DEUX FOIS, verte sur le sain et rouge sur le casse. Le filtre d API etait ecrit avant la lecture des declarations : les services auraient existe et Icinga aurait refuse leurs resultats. Et mon controle negatif a casse un service reel : substituer le certificat d hote a fait propager un cert sans sa clef vers node_exporter. Un controle negatif se fait sur une COPIE. P64 tient les deux bouts : declaree sans etre deposee, ou deposee sans etre declaree. Trois controles negatifs rejoues. make prouver : CONFORME, 64 OK, 0 echec, 0 saute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
301 lines
13 KiB
YAML
301 lines
13 KiB
YAML
---
|
|
# SANS AUTORITÉ DÉCLARÉE, ON NE DEVINE PAS SON NOM.
|
|
#
|
|
# L'URL de l'AC se dérivait d'un littéral (`infra-pki-01`), vrai chez tout tenant par
|
|
# coïncidence de nomenclature. Le premier écosystème à nommer sa PKI autrement a vu ses
|
|
# cinq machines s'enrôler auprès d'un hôte inexistant, et le message accusait le DNS.
|
|
#
|
|
# Elle se dérive maintenant de `groups['serveur_step_ca']`. Si ce groupe est vide, il n'y
|
|
# a pas d'autorité du tout : le dire ici vaut mieux que de laisser `step ca bootstrap`
|
|
# échouer sur un nom tronqué, trois tâches plus loin, en parlant de résolution.
|
|
- name: Une autorité de certification est-elle déclarée ?
|
|
ansible.builtin.assert:
|
|
that:
|
|
- client_pki_ca_hote | length > 0
|
|
fail_msg: >-
|
|
Aucun hôte ne porte `serveur_step_ca` dans cet inventaire : il n'y a pas d'autorité
|
|
interne à qui demander un certificat. Déclarer une PKI, ou exempter cet écosystème
|
|
de `client_pki` — mais avec une raison, jamais en silence.
|
|
|
|
# L'empreinte du root CA est la SOURCE DE VÉRITÉ de l'autorité elle-même : on la
|
|
# dérive à chaud (robuste au from-zero — une AC régénérée a une empreinte neuve).
|
|
# `client_pki_ca_fingerprint_override` permet d'épingler explicitement si besoin.
|
|
- name: Dériver l'empreinte du root CA depuis l'autorité
|
|
ansible.builtin.command:
|
|
cmd: "step certificate fingerprint {{ serveur_step_ca_steppath | default('/etc/step-ca') }}/certs/root_ca.crt"
|
|
delegate_to: "{{ groups['serveur_step_ca'][0] }}"
|
|
changed_when: false
|
|
check_mode: false
|
|
register: client_pki_fingerprint_ac
|
|
|
|
- name: Retenir l'empreinte (dérivée de l'AC, sauf override explicite)
|
|
ansible.builtin.set_fact:
|
|
client_pki_ca_fingerprint: "{{ client_pki_ca_fingerprint_override | default(client_pki_fingerprint_ac.stdout | trim, true) }}"
|
|
|
|
- name: Exiger l'empreinte AC et le mot de passe provisioner (Vault)
|
|
ansible.builtin.assert:
|
|
that:
|
|
- client_pki_ca_fingerprint | length > 0
|
|
- client_pki_provisioner_password | length > 0
|
|
fail_msg: >-
|
|
Empreinte du root CA indisponible (step-ca joignable et déployé ?) ;
|
|
client_pki_provisioner_password requis (via Ansible Vault).
|
|
|
|
- name: Assurer le repertoire des trousseaux apt
|
|
ansible.builtin.file:
|
|
path: /etc/apt/keyrings
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
# Recuperee UNE FOIS. Sans garde, chaque deploiement recontactait le serveur du
|
|
# fournisseur : cinq cles x quatorze hotes = soixante-dix allers-retours externes pour
|
|
# des cles deja installees, et autant d'occasions qu'un tiers lent fasse tomber le
|
|
# deploiement. Arbitrage rendu le 2026-08-09 : une plateforme souveraine ne depend pas
|
|
# de six serveurs etrangers pour redeployer ce qu'elle possede deja.
|
|
#
|
|
# CONSEQUENCE ASSUMEE : une rotation de cle amont n'est plus recuperee toute seule. Elle
|
|
# ne passe pas inapercue pour autant — `apt` refuse alors le depot, bruyamment. Pour
|
|
# forcer le rafraichissement : supprimer le fichier et rejouer le role.
|
|
- name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Sm)
|
|
ansible.builtin.stat:
|
|
path: "{{ client_pki_depot_cle_fichier }}"
|
|
register: telecharger_la_cle_de_signature_smallste_present
|
|
|
|
# Un serveur tiers intermittent ne doit pas arreter un deploiement de quarante
|
|
# minutes (mesure du 2026-08-23 : la meme URL pend, puis rend 200 en 0,48 s au
|
|
# second essai). Defaut de `get_url` : 10 s et aucune reprise.
|
|
- name: Telecharger la cle de signature Smallstep
|
|
when: not telecharger_la_cle_de_signature_smallste_present.stat.exists
|
|
ansible.builtin.get_url:
|
|
url: "{{ client_pki_depot_cle_url }}"
|
|
dest: "{{ client_pki_depot_cle_fichier }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
timeout: 30
|
|
register: client_pki_cle_depot_telechargee
|
|
retries: 5
|
|
delay: 6
|
|
until: client_pki_cle_depot_telechargee is succeeded
|
|
|
|
- name: Ajouter le depot apt Smallstep
|
|
ansible.builtin.template:
|
|
src: smallstep.sources.j2
|
|
dest: /etc/apt/sources.list.d/smallstep.sources
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
|
|
# LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT ENSUITE (2026-09-03).
|
|
#
|
|
# `client_pki` est une integration UNIVERSELLE : chaque machine de chaque ecosysteme
|
|
# installe `step-cli` ici, a sa naissance. Le depot Smallstep est en HTTPS, et
|
|
# `client_artefacts` pose `Acquire::https::Proxy "DIRECT"` — il CONTOURNE donc le cache
|
|
# du site et sort sur Internet. Sans lien, une VM neuve n'obtenait pas son client
|
|
# d'autorite, donc pas de certificat, donc n'entrait dans aucun flux chiffre.
|
|
#
|
|
# C'etait le dernier obstacle a une reconstruction hors ligne, et il tenait dans un mot :
|
|
# `DIRECT`, pose a juste titre pour une autre raison.
|
|
- name: Poser step-cli depuis le cache du controleur, s'il y est
|
|
ansible.builtin.include_role:
|
|
name: paquets_tiers
|
|
vars:
|
|
paquets_tiers_noms: "{{ client_pki_paquets }}"
|
|
|
|
# FILET, ET SEULEMENT SI LE CACHE N'A RIEN DONNE.
|
|
#
|
|
# `update_cache: true` interroge TOUS les depots configures, Smallstep compris. Hors
|
|
# ligne, cette tache echouerait donc APRES que le cache ait deja pose le paquet — le
|
|
# deploiement tomberait sur un travail deja fait. Le repli ne doit exister que quand il
|
|
# y a quelque chose a rattraper.
|
|
#
|
|
# Cache vide — poste jamais connecte, paquet retire de la declaration — on retombe sur le
|
|
# depot distant comme avant. Degrader, jamais deviner.
|
|
- name: Installer depuis le depot distant ce que le cache n'a pas fourni
|
|
ansible.builtin.apt:
|
|
name: "{{ client_pki_paquets
|
|
| difference((paquets_tiers_disponibles | default({})).keys() | list) }}"
|
|
state: present
|
|
update_cache: true
|
|
when: (client_pki_paquets
|
|
| difference((paquets_tiers_disponibles | default({})).keys() | list)) | length > 0
|
|
|
|
- name: Creer le repertoire STEPPATH des certificats
|
|
ansible.builtin.file:
|
|
path: "{{ client_pki_steppath }}/certs"
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: Deployer le mot de passe du provisioner
|
|
ansible.builtin.copy:
|
|
content: "{{ client_pki_provisioner_password }}"
|
|
dest: "{{ client_pki_steppath }}/provisioner.pass"
|
|
owner: root
|
|
group: root
|
|
mode: "0600"
|
|
no_log: true
|
|
|
|
# L'AUTORITE EST UN CAS A PART, mais pas une exception. Elle n'a pas a « s'enroler »
|
|
# aupres d'elle-meme : sa racine est deja sur son disque, et un bootstrap la ferait
|
|
# aller la chercher par le reseau, chez elle, en verifiant une empreinte qu'elle vient
|
|
# de produire. Elle a en revanche besoin de CERTIFICATS comme tout le monde — sans quoi
|
|
# ses propres services (node_exporter) restent en clair, et `client_metrique` echoue.
|
|
#
|
|
# La distinction est donc : pas d'enrolement, mais emission locale. C'est ce qui permet
|
|
# de retirer l'exemption de `client_pki` sans contredire « l'AC est la source de la
|
|
# confiance » — elle l'est, et c'est precisement pourquoi elle peut se signer elle-meme.
|
|
- name: Reconnaitre l'hote qui PORTE l'autorite
|
|
ansible.builtin.set_fact:
|
|
client_pki_est_autorite: "{{ inventory_hostname in (groups['serveur_step_ca'] | default([])) }}"
|
|
|
|
- name: Etablir la confiance dans l'AC interne (bootstrap + installation racine)
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
step ca bootstrap
|
|
--ca-url {{ client_pki_ca_url }}
|
|
--fingerprint {{ client_pki_ca_fingerprint }}
|
|
--install --force
|
|
creates: "{{ client_pki_steppath }}/certs/root_ca.crt"
|
|
environment:
|
|
STEPPATH: "{{ client_pki_steppath }}"
|
|
when: not client_pki_est_autorite | bool
|
|
|
|
- name: Poser la racine depuis le disque local (l'autorite ne s'enrole pas aupres d'elle-meme)
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_step_ca_steppath | default('/etc/step-ca') }}/certs/root_ca.crt"
|
|
dest: "{{ client_pki_steppath }}/certs/root_ca.crt"
|
|
remote_src: true
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
when: client_pki_est_autorite | bool
|
|
|
|
- name: Rendre le certificat racine lisible par tous (cert public, requis par les clients TLS)
|
|
ansible.builtin.file:
|
|
path: "{{ client_pki_steppath }}/certs/root_ca.crt"
|
|
mode: "0644"
|
|
|
|
# La validite, pas seulement la forme. Sans ce controle, un certificat expire mais
|
|
# portant les bons SAN ne declenchait AUCUNE re-emission : le role ne savait pas se
|
|
# soigner, et sur l'hote de l'autorite — ou le renouvellement automatique etait casse —
|
|
# rien ne pouvait plus le rattraper. Constate le 2026-08-08.
|
|
- name: Verifier que le certificat d hote est encore valide
|
|
ansible.builtin.command:
|
|
cmd: "openssl x509 -in {{ client_pki_cert }} -noout -checkend {{ client_pki_marge_renouvellement }}"
|
|
register: client_pki_validite
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
- name: Lire les SAN du certificat d'hote existant (detection de derive)
|
|
ansible.builtin.command:
|
|
cmd: "openssl x509 -in {{ client_pki_cert }} -noout -ext subjectAltName"
|
|
register: client_pki_san_actuels
|
|
changed_when: false
|
|
failed_when: false
|
|
|
|
# Re-emet si le cert est absent OU si un SAN voulu manque (ex: nouvelle exposition
|
|
# ajoutee au plan -> client_pki_sans mis a jour). Plus de garde 'creates' aveugle.
|
|
- name: Obtenir / re-emettre le certificat d'hote (absent, perime ou SAN derives)
|
|
ansible.builtin.command:
|
|
# `--ca-url` et `--root` EXPLICITES : sur un hote ordinaire ils sont redondants avec
|
|
# le `defaults.json` qu'ecrit `step ca bootstrap`, mais l'autorite ne bootstrape pas
|
|
# — elle n'aurait donc aucune de ces deux valeurs. Les nommer ici vaut mieux que de
|
|
# dependre d'un fichier ecrit par une etape qu'on saute volontairement.
|
|
cmd: >-
|
|
step ca certificate {{ client_pki_nom_cert }}
|
|
{{ client_pki_cert }} {{ client_pki_cle }}
|
|
--ca-url {{ client_pki_ca_url }}
|
|
--root {{ client_pki_steppath }}/certs/root_ca.crt
|
|
--provisioner {{ client_pki_provisioner }}
|
|
--provisioner-password-file {{ client_pki_steppath }}/provisioner.pass
|
|
{% for s in client_pki_sans | select | unique %}--san {{ s }} {% endfor %}
|
|
--force
|
|
environment:
|
|
STEPPATH: "{{ client_pki_steppath }}"
|
|
when: >-
|
|
client_pki_san_actuels.rc != 0
|
|
or client_pki_validite.rc != 0
|
|
or (client_pki_sans | select | unique | reject('equalto', '')
|
|
| reject('in', client_pki_san_actuels.stdout | default(''))
|
|
| list | length > 0)
|
|
changed_when: true
|
|
notify: Recharger les consommateurs du cert
|
|
|
|
# POSÉ À CHAQUE PASSAGE, PAS SEULEMENT À L'ÉMISSION.
|
|
#
|
|
# `step ca certificate` réécrit la clé avec ses propres droits, et le renouvellement
|
|
# automatique aussi. Ne régler les droits qu'au moment où le certificat change les
|
|
# perdrait au premier renouvellement — une panne qui surviendrait des semaines plus tard,
|
|
# sans rapport visible avec cette tâche.
|
|
- name: Donner accès à la clé privée au service qui doit la lire
|
|
ansible.builtin.file:
|
|
path: "{{ client_pki_cle }}"
|
|
owner: root
|
|
group: "{{ client_pki_cle_groupe }}"
|
|
mode: "{{ client_pki_cle_mode }}"
|
|
when: client_pki_cle_groupe != 'root' or client_pki_cle_mode != '0600'
|
|
notify: Recharger les consommateurs du cert
|
|
|
|
# Le certificat est public. `step` l'ecrit en 0600 comme la cle ; on le rend lisible, sans
|
|
# quoi un service non-root echoue sur le CERT apres avoir obtenu la CLE — et le message
|
|
# parle de permission sur un fichier que rien ne justifie de proteger.
|
|
- name: Rendre le certificat d'hôte lisible (il est public par nature)
|
|
ansible.builtin.file:
|
|
path: "{{ client_pki_cert }}"
|
|
owner: root
|
|
group: root
|
|
mode: "{{ client_pki_cert_mode }}"
|
|
notify: Recharger les consommateurs du cert
|
|
|
|
- name: Deployer l'unite systemd de renouvellement
|
|
ansible.builtin.template:
|
|
src: cert-renewer@.service.j2
|
|
dest: /etc/systemd/system/cert-renewer@.service
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Recharger systemd
|
|
|
|
- name: Deployer le minuteur de renouvellement
|
|
ansible.builtin.template:
|
|
src: cert-renewer@.timer.j2
|
|
dest: /etc/systemd/system/cert-renewer@.timer
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Recharger systemd
|
|
|
|
- name: Activer le renouvellement automatique du certificat d'hote
|
|
ansible.builtin.systemd:
|
|
name: "cert-renewer@{{ client_pki_nom_cert }}.timer"
|
|
enabled: true
|
|
state: started
|
|
daemon_reload: true
|
|
|
|
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
|
|
#
|
|
# LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. Il connait ses chemins, ses
|
|
# seuils, son consommateur — le superviseur, lui, ne connait que le verdict. C'est la
|
|
# meme repartition que pour les flux : le role declare, le moteur derive.
|
|
#
|
|
# Le porteur (`client_sante`) fait tourner tout ce qui vit dans ce repertoire et pousse
|
|
# un resultat passif par sonde. Il n'a pas a savoir ce que celle-ci mesure.
|
|
- name: Assurer le repertoire des sondes de supervision
|
|
ansible.builtin.file:
|
|
path: /usr/local/lib/setops/sondes
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: Deposer la sonde du certificat d'hote
|
|
ansible.builtin.template:
|
|
src: sonde-certificat.sh.j2
|
|
dest: /usr/local/lib/setops/sondes/certificat.sh
|
|
owner: root
|
|
group: root
|
|
mode: "0750"
|