Set-OPS-Public/roles/serveur_ops_tenant/tasks/main.yml
Daniel Allaire 9af39b1b60 forge : chaque locataire publie la cle qui lira son depot, le site sait l'enregistrer
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>
2026-10-11 19:59:07 -04:00

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"