Debut du chantier des depots prives (docs/depots-prives-conception.md). La face reseau
publie genome: {depot, cle_lecture} ; P95 exige que la cle soit connue de la flotte, et
l'armement refuse une voute qui en remettrait une autre. make forge-cles enregistre
chaque cle en lecture seule sur le seul depot que la face nomme, retire une cle
perimee, ne touche jamais celle du runner du site. Flux SSH vers la forge du site
depuis les seuls runners des locataires (paire runners_locataires).
Rien n'est encore applique sur le terrain ; les depots restent publics.
95 preuves.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
271 lines
11 KiB
YAML
271 lines
11 KiB
YAML
---
|
|
- name: Exiger les intrants du runner de tenant
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_ops_tenant_depot | length > 0
|
|
- (not (serveur_ops_tenant_voute_deposer | bool))
|
|
or (serveur_ops_tenant_voute_source | length > 0)
|
|
fail_msg: >-
|
|
serveur_ops_tenant exige `serveur_ops_tenant_depot` (le dossier de CET écosystème
|
|
chez le runner, cloné par serveur_ops — il vaut `serveur_ops_instance` par défaut)
|
|
et, si le dépôt de la voûte est demandé, `serveur_ops_tenant_voute_source`
|
|
(le chemin de son `vault.yml` sur le contrôleur).
|
|
|
|
- name: Le plan de l'écosystème est-il bien là ?
|
|
ansible.builtin.stat:
|
|
path: "{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/plan/serveurs.yml"
|
|
register: serveur_ops_tenant_plan
|
|
|
|
- name: Refuser si le plan de l'écosystème manque
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_ops_tenant_plan.stat.exists
|
|
fail_msg: >-
|
|
{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/plan/serveurs.yml est
|
|
absent. Déclarer ce dépôt dans `serveur_ops_depots` (role: instance) et rejouer
|
|
serveur_ops : donner sa voûte à un runner qui n'a pas le plan qu'elle ouvre ne
|
|
configure rien — ça ne fait que déposer un secret.
|
|
when: not ansible_check_mode
|
|
|
|
# LE DOSSIER D'INVENTAIRE SE DÉCOUVRE. Deux noms existent dans le dépôt (`principal`,
|
|
# `production`) et l'écrire en dur le ferait mentir pour l'écosystème qui emploie l'autre.
|
|
- name: Chercher le dossier d'inventaire de cet écosystème
|
|
ansible.builtin.stat:
|
|
path: "{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/{{ item }}"
|
|
register: serveur_ops_tenant_candidats
|
|
loop: "{{ serveur_ops_tenant_inventaires_connus }}"
|
|
loop_control:
|
|
label: "{{ item }}"
|
|
|
|
- name: Retenir le premier qui existe
|
|
ansible.builtin.set_fact:
|
|
serveur_ops_tenant_inventaire: >-
|
|
{{ (serveur_ops_tenant_candidats.results | selectattr('stat.exists')
|
|
| map(attribute='item') | list | first) | default('') }}
|
|
|
|
- name: Refuser si aucun inventaire n'est reconnu
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_ops_tenant_inventaire | length > 0
|
|
fail_msg: >-
|
|
Aucun dossier d'inventaire trouvé sous
|
|
{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/ parmi
|
|
{{ serveur_ops_tenant_inventaires_connus | join(', ') }}. Sans lui, on ne saurait pas
|
|
où déposer la voûte — et la déposer au mauvais endroit revient à ne pas la déposer,
|
|
en laissant croire le contraire.
|
|
when: not ansible_check_mode
|
|
|
|
- name: Préparer le dossier des variables communes
|
|
ansible.builtin.file:
|
|
path: >-
|
|
{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/{{
|
|
serveur_ops_tenant_inventaire }}/group_vars/all
|
|
state: directory
|
|
owner: "{{ serveur_ops_tenant_utilisateur }}"
|
|
group: "{{ serveur_ops_tenant_utilisateur }}"
|
|
mode: "0700"
|
|
when:
|
|
- serveur_ops_tenant_voute_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
# LES SECRETS DE L'ÉCOSYSTÈME, DE DROIT ET NON PAR EMPRUNT.
|
|
#
|
|
# `decrypt: false` : voir defaults/main.yml. Le fichier reste chiffré ; seul le mot de
|
|
# passe, tapé à l'exécution, l'ouvre.
|
|
- name: Déposer la voûte de l'écosystème (chiffrée)
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_ops_tenant_voute_source }}"
|
|
dest: >-
|
|
{{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot }}/inventories/{{
|
|
serveur_ops_tenant_inventaire }}/group_vars/all/vault.yml
|
|
owner: "{{ serveur_ops_tenant_utilisateur }}"
|
|
group: "{{ serveur_ops_tenant_utilisateur }}"
|
|
mode: "0600"
|
|
decrypt: false
|
|
when:
|
|
- serveur_ops_tenant_voute_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
# ÉCRIRE, PUIS RELIRE (D-68). Une voûte déchiffrée par accident est une fuite silencieuse :
|
|
# le fichier existe, le rôle se dit satisfait, et les secrets de l'écosystème dorment en
|
|
# clair sur une machine.
|
|
- name: Relire l'en-tête de la voûte déposée
|
|
ansible.builtin.command:
|
|
cmd: >-
|
|
head -c 21 {{ serveur_ops_tenant_racine }}/{{ serveur_ops_tenant_depot
|
|
}}/inventories/{{ serveur_ops_tenant_inventaire }}/group_vars/all/vault.yml
|
|
register: serveur_ops_tenant_entete
|
|
changed_when: false
|
|
when:
|
|
- serveur_ops_tenant_voute_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
- name: Exiger que la voûte déposée soit CHIFFRÉE
|
|
ansible.builtin.assert:
|
|
that:
|
|
- "'$ANSIBLE_VAULT' in serveur_ops_tenant_entete.stdout"
|
|
fail_msg: >-
|
|
La voûte déposée n'est PAS chiffrée — elle a été déchiffrée en transit.
|
|
Vérifier `decrypt: false` sur la copie. Détruire le fichier au shred et considérer
|
|
les secrets de cet écosystème comme exposés.
|
|
when:
|
|
- serveur_ops_tenant_voute_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
# --- ARMER LE RUNNER ---------------------------------------------------------
|
|
#
|
|
# Ce qui suit ne se declenche que si un humain l'a demande (serveur_ops_tenant_cle_deposer).
|
|
|
|
- name: Exiger une source pour la cle de la voute
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_ops_tenant_cle_source | length > 0
|
|
fail_msg: >-
|
|
serveur_ops_tenant_cle_deposer est vrai, mais aucune
|
|
`serveur_ops_tenant_cle_source` n'est nommee. Armer un runner sans dire
|
|
avec QUELLE cle n'a pas de sens — et poser un fichier vide le laisserait croire
|
|
arme.
|
|
when: serveur_ops_tenant_cle_deposer | bool
|
|
|
|
- name: Preparer le dossier des cles du runner
|
|
ansible.builtin.file:
|
|
path: "{{ serveur_ops_tenant_racine }}/.config"
|
|
state: directory
|
|
owner: "{{ serveur_ops_tenant_utilisateur }}"
|
|
group: "{{ serveur_ops_tenant_utilisateur }}"
|
|
mode: "0700"
|
|
when:
|
|
- serveur_ops_tenant_cle_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
- name: Deposer la cle de la voute
|
|
ansible.builtin.copy:
|
|
src: "{{ serveur_ops_tenant_cle_source }}"
|
|
dest: "{{ serveur_ops_tenant_cle_destination }}"
|
|
owner: "{{ serveur_ops_tenant_utilisateur }}"
|
|
group: "{{ serveur_ops_tenant_utilisateur }}"
|
|
mode: "0600"
|
|
no_log: true
|
|
when:
|
|
- serveur_ops_tenant_cle_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
# ECRIRE, PUIS RELIRE — comme pour la voute, et pour la faute SYMETRIQUE : ici on ne
|
|
# craint pas un chiffre devenu clair, mais une cle qui serait en fait une VOUTE. Copier le
|
|
# mauvais fichier laisse un runner qui se croit arme et echoue au premier secret, tres
|
|
# loin d'ici. On mesure aussi qu'elle n'est pas vide : un fichier de mot de passe vide est
|
|
# accepte par Ansible et n'ouvre rien.
|
|
- name: Relire la cle deposee
|
|
ansible.builtin.stat:
|
|
path: "{{ serveur_ops_tenant_cle_destination }}"
|
|
checksum_algorithm: sha256
|
|
register: serveur_ops_tenant_cle_posee
|
|
when:
|
|
- serveur_ops_tenant_cle_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
- name: Exiger une cle non vide, en 0600, et qui ne soit pas une voute
|
|
ansible.builtin.assert:
|
|
that:
|
|
- serveur_ops_tenant_cle_posee.stat.exists
|
|
- serveur_ops_tenant_cle_posee.stat.size > 0
|
|
- serveur_ops_tenant_cle_posee.stat.mode == '0600'
|
|
fail_msg: >-
|
|
La cle deposee est absente, vide, ou lisible au-dela de son proprietaire.
|
|
Un runner arme d'une cle vide se croit arme et echoue au premier secret,
|
|
tres loin d'ici.
|
|
when:
|
|
- serveur_ops_tenant_cle_deposer | bool
|
|
- not ansible_check_mode
|
|
|
|
# --- L'IDENTITE SSH DU RUNNER (voir defaults/main.yml, 2026-09-30) ----------------------
|
|
- name: Remettre au runner sa cle SSH (conservee dans la voute)
|
|
ansible.builtin.copy:
|
|
content: "{{ serveur_ops_tenant_ssh_cle | trim }}\n"
|
|
dest: "{{ serveur_ops_tenant_ssh_chemin }}"
|
|
owner: "{{ serveur_ops_tenant_utilisateur }}"
|
|
group: "{{ serveur_ops_tenant_utilisateur }}"
|
|
mode: "0600"
|
|
no_log: true
|
|
when:
|
|
- serveur_ops_tenant_ssh_cle | length > 0
|
|
- not ansible_check_mode
|
|
|
|
- name: Relire la cle publique qui en decoule
|
|
ansible.builtin.command: ssh-keygen -y -f {{ serveur_ops_tenant_ssh_chemin }}
|
|
changed_when: false
|
|
register: serveur_ops_tenant_ssh_publique
|
|
when:
|
|
- serveur_ops_tenant_ssh_cle | length > 0
|
|
- not ansible_check_mode
|
|
|
|
- name: Poser la cle publique a cote de la privee
|
|
ansible.builtin.copy:
|
|
content: "{{ serveur_ops_tenant_ssh_publique.stdout.split()[:2] | join(' ') }} setops@{{ inventory_hostname }}.{{ domaine_interne }}\n"
|
|
dest: "{{ serveur_ops_tenant_ssh_chemin }}.pub"
|
|
owner: "{{ serveur_ops_tenant_utilisateur }}"
|
|
group: "{{ serveur_ops_tenant_utilisateur }}"
|
|
mode: "0644"
|
|
when:
|
|
- serveur_ops_tenant_ssh_cle | length > 0
|
|
- not ansible_check_mode
|
|
|
|
# LA GARDE QUI AURAIT VU L'ECART AVANT LE DEPLOIEMENT : la cle du runner doit etre une de
|
|
# celles que le plan declare, puisque c'est celle-la que chaque machine du locataire recoit
|
|
# a sa naissance. Sinon, le runner arme se verrait refuser toute la flotte, tres loin d'ici.
|
|
- name: Exiger que la cle du runner soit declaree au plan
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (serveur_ops_tenant_ssh_publique.stdout.split()[:2] | join(' '))
|
|
in (ssh_baseline_cles_admin | default([]) | map(attribute='cle')
|
|
| map('split') | map('batch', 2) | map('first') | map('join', ' ') | list)
|
|
fail_msg: >-
|
|
La cle SSH du runner (issue de la voute) n'est PAS declaree dans
|
|
`ssh_baseline_cles_admin` : les machines du locataire, nees avec la cle declaree, le
|
|
refuseront toutes. Declarer sa cle publique au plan, ou corriger la voute.
|
|
when:
|
|
- serveur_ops_tenant_ssh_cle | length > 0
|
|
- not ansible_check_mode
|
|
|
|
# LA CLE QUI LIT LE DEPOT DU GENOME (2026-10-11). Le site l'enregistre sur le depot de ce
|
|
# locataire d'apres sa face (`serveur_ops_tenant_cle_publique`) : si la voute en remettait une
|
|
# autre, le runner arme ne pourrait plus tirer son plan, et l'echec viendrait bien plus tard.
|
|
- name: Exiger que la cle du runner soit celle que la face publie pour lire le genome
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (serveur_ops_tenant_ssh_publique.stdout.split()[:2] | join(' '))
|
|
== (serveur_ops_tenant_cle_publique.split()[:2] | join(' '))
|
|
fail_msg: >-
|
|
La cle SSH du runner (issue de la voute) n'est PAS `serveur_ops_tenant_cle_publique` :
|
|
la forge du site n'autorise que celle-ci a lire le depot de ce locataire. Corriger la
|
|
declaration (puis republier la face), ou la voute.
|
|
when:
|
|
- serveur_ops_tenant_ssh_cle | length > 0
|
|
- serveur_ops_tenant_cle_publique | default('') | length > 0
|
|
- not ansible_check_mode
|
|
|
|
- name: Dire qu'aucune identite n'est conservee
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
Aucune `vault_runner_cle_ssh` dans la voute : le runner garde la paire qu'il s'est
|
|
fabriquee. Sa reconstruction lui en donnera une autre, que la flotte refusera.
|
|
when: serveur_ops_tenant_ssh_cle | length == 0
|
|
|
|
# --- Sonde de supervision (docs/supervision-conception.md) --------------------------
|
|
# Le role qui possede la verite depose sa propre sonde ; le porteur (`client_sante`) la
|
|
# fait tourner et pousse le verdict, sans savoir ce qu'elle 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 « voute »
|
|
ansible.builtin.template:
|
|
src: sonde-voute.sh.j2
|
|
dest: /usr/local/lib/setops/sondes/voute.sh
|
|
owner: root
|
|
group: root
|
|
mode: "0750"
|