deployer : l'ordre des couches, et l'autorite qui se signe elle-meme
`make deployer` triait les groupes alphabetiquement apres le socle : `client_metrique` passait avant `serveur_step_ca`, donc exigeait un certificat que l'autorite, pas encore deployee, ne pouvait pas avoir emis — sur l'hote de l'AC lui-meme. `docs/couches-deploiement.yml` existe pour definir cet ordre et dit « integrations deployees en dernier » ; `make site` le lit, `deployer` ne l'avait jamais lu. Meme registre desormais. Deux politiques universelles se contredisaient : `client_pki` exemptait l'AC, `client_metrique` refuse toute exemption. L'exemption confondait « ne pas s'enroler » et « ne pas avoir de certificat ». `client_pki` distingue les deux chemins : bootstrap pour les autres, EMISSION LOCALE sur l'AC. L'exemption disparait, la doctrine reste. Effet de bord instructif : `step ca bootstrap` ecrit aussi le defaults.json qui porte l'URL de l'AC. En sautant le bootstrap on perdait l'information sans le voir. `--ca-url` et `--root` sont explicites pour tous les hotes. Mesure : infra-pki-01 et infra-dns-01 entierement deployees, aucun echec, step-ca health=ok, powerdns resout la zone souveraine, et les deux hotes portent un certificat de l'AC interne. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
3786cc055b
commit
f18715bc78
4 changed files with 129 additions and 5 deletions
54
CHANGELOG.md
54
CHANGELOG.md
|
|
@ -2,6 +2,60 @@
|
||||||
|
|
||||||
## 2026-08-06 — le chemin nord-sud devient dérivable
|
## 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é
|
### 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 :
|
`nftables.service` refusait de démarrer sur la première VM déployée :
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,13 @@ integration:
|
||||||
Tout hote fait confiance a l'AC interne et porte un certificat : c'est le
|
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
|
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.
|
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.
|
# AUCUNE exemption, et l'autorite non plus. Elle ne s'ENROLE pas aupres d'elle-meme —
|
||||||
# L'exemption se derive du service rendu, jamais d'un nom d'hote — elle suit donc
|
# sa racine est deja sur son disque — mais elle a besoin de CERTIFICATS comme tout le
|
||||||
# step-ca si on le deplace.
|
# monde : sans eux, ses propres services restent en clair et `client_metrique` echoue
|
||||||
sauf_role: serveur_step_ca
|
# 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.
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,19 @@
|
||||||
mode: "0600"
|
mode: "0600"
|
||||||
no_log: true
|
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)
|
- name: Etablir la confiance dans l'AC interne (bootstrap + installation racine)
|
||||||
ansible.builtin.command:
|
ansible.builtin.command:
|
||||||
cmd: >-
|
cmd: >-
|
||||||
|
|
@ -80,6 +93,17 @@
|
||||||
creates: "{{ client_pki_steppath }}/certs/root_ca.crt"
|
creates: "{{ client_pki_steppath }}/certs/root_ca.crt"
|
||||||
environment:
|
environment:
|
||||||
STEPPATH: "{{ client_pki_steppath }}"
|
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)
|
- name: Rendre le certificat racine lisible par tous (cert public, requis par les clients TLS)
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
|
|
@ -97,9 +121,15 @@
|
||||||
# ajoutee au plan -> client_pki_sans mis a jour). Plus de garde 'creates' aveugle.
|
# 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)
|
- name: Obtenir / re-emettre le certificat d'hote (absent ou SAN derives)
|
||||||
ansible.builtin.command:
|
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: >-
|
cmd: >-
|
||||||
step ca certificate {{ client_pki_nom_cert }}
|
step ca certificate {{ client_pki_nom_cert }}
|
||||||
{{ client_pki_cert }} {{ client_pki_cle }}
|
{{ 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 {{ client_pki_provisioner }}
|
||||||
--provisioner-password-file {{ client_pki_steppath }}/provisioner.pass
|
--provisioner-password-file {{ client_pki_steppath }}/provisioner.pass
|
||||||
{% for s in client_pki_sans | select | unique %}--san {{ s }} {% endfor %}
|
{% for s in client_pki_sans | select | unique %}--san {{ s }} {% endfor %}
|
||||||
|
|
|
||||||
|
|
@ -249,6 +249,28 @@ def parametres_proxmox_hote(data: dict, host: str,
|
||||||
return lignes
|
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:
|
def afficher_playbooks_hote(data: dict, host: str, playbook_dir: Path) -> None:
|
||||||
groups = groupes_hote(data, host)
|
groups = groupes_hote(data, host)
|
||||||
if not groups:
|
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 SOCLE passe toujours en premier : il pose /etc/hosts (resolution interne),
|
||||||
# le durcissement, etc. Les integrations client_* et les services en dependent
|
# le durcissement, etc. Les integrations client_* et les services en dependent
|
||||||
# (ex. client_pki doit resoudre l'AC via /etc/hosts avant de bootstrapper).
|
# (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"]
|
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] = []
|
missing_playbooks: list[str] = []
|
||||||
playbooks: list[Path] = []
|
playbooks: list[Path] = []
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue