diff --git a/CHANGELOG.md b/CHANGELOG.md index 7653f23..a7bf917 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,60 @@ ## 2026-08-06 — le chemin nord-sud devient dérivable +### `make deployer` ignorait l'ordre des couches + +`client_metrique` échouait sur les deux premières VM : il exige le certificat TLS du +`node_exporter`, que seule l'AC peut émettre. Cause : `afficher_playbooks_hote()` triait les +groupes **alphabétiquement** après le socle. `client_journal`, `client_metrique`, +`client_unbound` passaient donc avant `serveur_step_ca` — sur l'hôte de l'AC lui-même. + +`docs/couches-deploiement.yml` existe précisément pour définir cet ordre, et sa dernière +couche dit en toutes lettres : *« intégrations déployées en dernier, quand leurs cibles sont +debout »*. `make site` le lit ; `make deployer` ne l'avait jamais lu. Deux chemins pour la +même question, un seul registre consulté. + +Le tri se fait désormais par couche — socle d'abord, alphabétique seulement **à l'intérieur** +d'une couche — depuis le même registre que l'orchestrateur. + +``` +infra-pki-01 socle → serveur_step_ca → client_pki → intégrations +infra-dns-01 socle → client_pki → serveur_powerdns → intégrations +``` + +### L'autorité n'est plus une exception, elle est un cas à part + +Deux politiques universelles se contredisaient. `client_pki` exemptait l'AC — *« elle EST la +source de la confiance »* — et `client_metrique` refuse toute exemption — *« un collecteur +muet sur son propre état est un angle mort »*. Les deux avaient raison séparément, et l'AC +restait la seule machine impossible à mesurer. + +L'exemption confondait **ne pas s'enrôler** et **ne pas avoir de certificat**. L'AC n'a pas à +aller chercher sa racine par le réseau, chez elle, en vérifiant une empreinte qu'elle vient de +produire — mais ses services ont besoin de certificats comme tous les autres. Elle est +précisément la machine qui peut se les signer, localement, sans réseau. + +`client_pki` distingue donc les deux chemins : bootstrap pour les autres, **émission locale** +sur l'AC. L'exemption disparaît, et la doctrine reste intacte. + +**Un effet de bord instructif.** `step ca bootstrap` écrit aussi le `defaults.json` qui porte +l'URL de l'AC : en sautant le bootstrap, on perdait l'information sans le voir — +`flag '--ca-url' is required`. `--ca-url` et `--root` sont désormais explicites pour **tous** +les hôtes. Dépendre d'un fichier écrit par une étape qu'on saute volontairement, c'était +reconstruire le même piège. + +### Deux services souverains debout + +``` +step-ca active, :8443 `step ca health` → ok +powerdns active, 10.27.19.11:53 (plus 0.0.0.0 — la restriction d'écoute a pris) + dig @10.27.19.11 infra-pki-01.chezlepro.internal → 10.27.19.21 +``` + +`infra-dns-01` est la première VM tenant **entièrement** déployée : huit playbooks, aucun +échec — socle, durcissement, PKI cliente, PowerDNS, sauvegarde, journaux, métriques, courriel. +La zone souveraine résout, et l'AC a émis son premier certificat à un tiers. + + ### L'ICMP n'a pas de port — et la seconde barrière n'avait jamais démarré `nftables.service` refusait de démarrer sur la première VM déployée : diff --git a/roles/client_pki/meta/integration.yml b/roles/client_pki/meta/integration.yml index 99ee1e4..45e0a92 100644 --- a/roles/client_pki/meta/integration.yml +++ b/roles/client_pki/meta/integration.yml @@ -7,7 +7,13 @@ integration: Tout hote fait confiance a l'AC interne et porte un certificat : c'est le prerequis du chiffrement est-ouest (voir docs/zero-confiance.md). Un hote sans certificat ne peut ni presenter ni verifier — il retombe en clair en silence. - # L'AC ne s'enrole pas aupres d'elle-meme : elle EST la source de la confiance. - # L'exemption se derive du service rendu, jamais d'un nom d'hote — elle suit donc - # step-ca si on le deplace. - sauf_role: serveur_step_ca + # AUCUNE exemption, et l'autorite non plus. Elle ne s'ENROLE pas aupres d'elle-meme — + # sa racine est deja sur son disque — mais elle a besoin de CERTIFICATS comme tout le + # monde : sans eux, ses propres services restent en clair et `client_metrique` echoue + # sur la seule machine qu'on ne peut pas se permettre de ne pas mesurer. + # + # Le role distingue donc les deux : bootstrap pour les autres, EMISSION LOCALE sur + # l'AC (voir tasks/main.yml). Exempter l'hote entier confondait « ne pas s'enroler » + # avec « ne pas avoir de certificat », et laissait un angle mort au coeur du systeme. + # Constate le 2026-08-06 : `client_pki` exemptait l'AC, `client_metrique` exigeait un + # certificat, et les deux politiques avaient raison separement. diff --git a/roles/client_pki/tasks/main.yml b/roles/client_pki/tasks/main.yml index 9a6708e..9e7a57d 100644 --- a/roles/client_pki/tasks/main.yml +++ b/roles/client_pki/tasks/main.yml @@ -70,6 +70,19 @@ 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: >- @@ -80,6 +93,17 @@ 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: @@ -97,9 +121,15 @@ # ajoutee au plan -> client_pki_sans mis a jour). Plus de garde 'creates' aveugle. - name: Obtenir / re-emettre le certificat d'hote (absent 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 %} diff --git a/scripts/inventory_host.py b/scripts/inventory_host.py index 154561e..196a81b 100644 --- a/scripts/inventory_host.py +++ b/scripts/inventory_host.py @@ -249,6 +249,28 @@ def parametres_proxmox_hote(data: dict, host: str, return lignes +def _rang_des_couches() -> dict[str, int]: + """Rang de chaque groupe dans `docs/couches-deploiement.yml`. {} si le fichier manque. + + SOURCE UNIQUE avec `make site` : l'ordre d'un hote seul et celui de la reconstruction + complete doivent decouler du meme registre. Les tenir separement, c'est se garantir + qu'ils divergeront — et la divergence ne se voit qu'au deploiement, trop tard. + """ + fichier = Path(__file__).resolve().parent.parent / "docs" / "couches-deploiement.yml" + if not fichier.is_file(): + return {} + try: + contenu = yaml.safe_load(fichier.read_text(encoding="utf-8")) or {} + except (OSError, yaml.YAMLError): + return {} + couches = contenu.get("couches") if isinstance(contenu, dict) else contenu + rangs: dict[str, int] = {} + for rang, couche in enumerate(couches or []): + for groupe in (couche or {}).get("groupes") or []: + rangs.setdefault(str(groupe), rang) + return rangs + + def afficher_playbooks_hote(data: dict, host: str, playbook_dir: Path) -> None: groups = groupes_hote(data, host) if not groups: @@ -257,8 +279,20 @@ def afficher_playbooks_hote(data: dict, host: str, playbook_dir: Path) -> None: # Le SOCLE passe toujours en premier : il pose /etc/hosts (resolution interne), # le durcissement, etc. Les integrations client_* et les services en dependent # (ex. client_pki doit resoudre l'AC via /etc/hosts avant de bootstrapper). + # + # ENSUITE, l'ordre des COUCHES (docs/couches-deploiement.yml) — le meme registre que + # `make site`. Trier alphabetiquement mettait `client_metrique` avant + # `serveur_step_ca` : l'integration exigeait un certificat que l'autorite, pas encore + # deployee, ne pouvait pas avoir emis. Constate le 2026-08-06 sur les deux premieres + # VM, dont l'hote de l'AC lui-meme. La couche « integrations » dit en toutes lettres + # « deployees en dernier, quand leurs cibles sont debout » : `make deployer` l'ignorait. socle = ["serveur_debian", "serveur_durci"] - groups = sorted(groups, key=lambda g: (socle.index(g) if g in socle else len(socle), g)) + couches = _rang_des_couches() + groups = sorted(groups, key=lambda g: ( + socle.index(g) if g in socle else len(socle), + couches.get(g, len(couches)), # groupe hors registre : apres les couches connues + g, + )) missing_playbooks: list[str] = [] playbooks: list[Path] = []