Metriques et journaux (CHANGELOG 142). Le site pose les quatre interrupteurs TLS que chaque locataire pose dans ses group_vars : node_exporter et Loki servaient en HTTP sur les neuf VM, le certificat etait la, inutilise. test_parite_tls_site.py compare desormais le site aux locataires. Les hyperviseurs poussent leurs journaux en HTTPS verifie, avec la racine du site comme simple fichier. L'exportateur PostgreSQL sert en HTTPS partout, sur l'interrupteur de PostgreSQL lui-meme, que Prometheus lit pour choisir son schema. La frontiere (CHANGELOG 143). Une autorite subordonnee signee par la racine du site, bornee (pathlen 0, noms sous le domaine et adresses du site, contrainte critique), cle nee sur la boite (scripts/frontiere_pki.py). Syslog vers Loki en TLS mutuel (seule la frontiere peut ecrire), exportateur en HTTPS, interface web sur le certificat de services ; les clients d'API verifient la frontiere contre la racine du site. La lecture du magasin passe par config.xml : l'API de recherche renvoie les cles privees. Deploye et mesure au site, chez les deux locataires et sur la frontiere ; 95 preuves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
348 lines
15 KiB
YAML
348 lines
15 KiB
YAML
---
|
|
- 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.
|
|
# UN FICHIER VIDE EXISTE (mesure du 2026-09-10).
|
|
#
|
|
# Cette garde demandait « ce fichier est-il la ? ». Un telechargement interrompu — un 503,
|
|
# un delai depasse, une coupure — laisse un fichier de ZERO octet, qui existe. La tache
|
|
# est donc sautee a tous les passages suivants, et la machine garde une ressource morte
|
|
# pour toujours, en silence.
|
|
#
|
|
# CE QUE CA A COUTE : `infra-mail-01` a garde une cle smallstep de 0 octet apres une
|
|
# epreuve hors ligne. Treize machines portaient 1022 octets, elle portait le vide :
|
|
#
|
|
# E: Le depot http://packages.smallstep.com/... n'est pas signe.
|
|
#
|
|
# Le meme jour, l'index `dists/trixie` du cache du site etait tombe de 140 416 a 4 332
|
|
# octets — meme famille, autre endroit. Un telechargement partiel ne se signale pas : il
|
|
# se fait passer pour un succes.
|
|
#
|
|
# On demande donc « est-il ENTIER ? » — a defaut de pouvoir demander « est-il JUSTE ? »,
|
|
# ce qui exigerait une empreinte de reference que l'amont ne publie pas toujours.
|
|
- name: Cette ressource est-elle deja recuperee ? (Telecharger la cle de signature Gr)
|
|
ansible.builtin.stat:
|
|
path: "{{ client_journal_depot_cle_fichier }}"
|
|
register: telecharger_la_cle_de_signature_grafana_present
|
|
|
|
# ... ET IL FAUT LE RETIRER, PAS SEULEMENT LE REDEMANDER (mesure du 2026-09-10).
|
|
#
|
|
# Laisser la garde autoriser la tache ne suffit PAS. `get_url` sur une destination qui
|
|
# existe emet une requete CONDITIONNELLE : l'amont repond « non modifie », le module rend
|
|
# `ok`, et le fichier corrompu reste en place. Mesure : la tache s'executait bien
|
|
# (`ok: [infra-mail-01]`), et la cle faisait toujours 0 octet au passage suivant. Un
|
|
# `force: yes` manuel l'a reparee en une seconde.
|
|
#
|
|
# On efface donc la ruine avant de reconstruire. `state: absent` ne mord que sur un
|
|
# fichier vide : une cle valide n'est jamais retiree, et la doctrine « on ne retelecharge
|
|
# pas ce qu'on possede deja » reste intacte.
|
|
- name: Retirer une ressource VIDE avant de la redemander
|
|
ansible.builtin.file:
|
|
path: "{{ client_journal_depot_cle_fichier }}"
|
|
state: absent
|
|
when:
|
|
- telecharger_la_cle_de_signature_grafana_present.stat.exists
|
|
- (telecharger_la_cle_de_signature_grafana_present.stat.size | default(0)) == 0
|
|
|
|
- name: Telecharger la cle de signature Grafana
|
|
when: not (telecharger_la_cle_de_signature_grafana_present.stat.exists and telecharger_la_cle_de_signature_grafana_present.stat.size | default(0) > 0)
|
|
# `force` PARCE QU'UNE REPRISE CONDITIONNELLE NE REPREND RIEN (2026-09-12).
|
|
#
|
|
# Sans lui, `get_url` envoie un `If-Modified-Since` des que le fichier existe. Le cache
|
|
# repond `304 Not Modified`, la tache reussit sans rien ecrire, et la boucle `until`
|
|
# — qui exige une taille non nulle — retente cinq fois contre un serveur qui repondra
|
|
# toujours 304 :
|
|
#
|
|
# HTTP Error 304: Not Modified size: 0 attempts: 5
|
|
#
|
|
# C'est la boucle de reprise qui se bat contre elle-meme. Le `when:` ci-dessus garantit
|
|
# deja qu'on ne retelecharge pas une cle valide ; `force` ne concerne donc que le cas
|
|
# ou l'on a DECIDE d'aller chercher.
|
|
ansible.builtin.get_url:
|
|
force: true
|
|
url: "{{ client_journal_depot_cle_url }}"
|
|
dest: "{{ client_journal_depot_cle_fichier }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
# LA CLE PASSE PAR LE CACHE, ELLE AUSSI (mesure du 2026-09-10).
|
|
#
|
|
# `get_url` ignore la configuration d'apt : le mandataire pose dans
|
|
# `/etc/apt/apt.conf.d/` ne vaut que pour apt. Les cles de signature sortaient donc
|
|
# TOUJOURS en direct, meme apres que tous les depots soient passes par le cache — un
|
|
# trou reste ouvert derriere une porte qu'on croyait fermee.
|
|
#
|
|
# Et ce n'est pas theorique : depuis `collab-01`, la route directe vers
|
|
# `www.collaboraoffice.com` EXPIRE, quand le cache l'atteint sans peine.
|
|
#
|
|
# en direct grafana 200 collabora TIMEOUT
|
|
# via le cache collabora 200
|
|
#
|
|
# Le meme geste corrige donc les deux : plus rien ne sort, et la machine qui n'avait
|
|
# pas de route en trouve une.
|
|
#
|
|
# DEGRADE, JAMAIS DEVINE : sans cache d'amorcage declare, la valeur est VIDE et
|
|
# `get_url` sort en direct comme avant. On n'invente pas un mandataire.
|
|
environment:
|
|
http_proxy: >-
|
|
{{ ('http://' ~ artefacts_amorcage) if (artefacts_amorcage | default('') | string | length > 0) else '' }}
|
|
https_proxy: ""
|
|
register: client_journal_cle_depot
|
|
retries: 5
|
|
delay: 6
|
|
until: client_journal_cle_depot is succeeded and (client_journal_cle_depot.size | default(0)) > 0
|
|
|
|
# `apt_repository` AJOUTE, IL NE REMPLACE PAS (mesure du 2026-09-10).
|
|
#
|
|
# Passer une source de `https://` a `http://` y ecrit une SECONDE ligne au lieu de
|
|
# corriger la premiere. apt interroge alors les deux — et l'ancienne continue de sortir
|
|
# en direct sur Internet, ce que le passage au cache visait justement a supprimer. Le
|
|
# symptome le disait, sur les quatorze machines :
|
|
#
|
|
# W: La cible Packages est specifiee plusieurs fois dans grafana.list:1 et :2
|
|
#
|
|
# On retire donc explicitement la forme precedente. `state: absent` ne mord que si la
|
|
# ligne existe : sur une machine neuve, cette tache ne fait rien.
|
|
- name: Mesurer la cle de signature Grafana qui vient d'etre posee
|
|
ansible.builtin.stat:
|
|
path: "{{ client_journal_depot_cle_fichier }}"
|
|
register: cle_grafana_posee
|
|
|
|
- name: Exiger une cle NON VIDE avant de s'en servir
|
|
# UNE REUSSITE N'EST PAS UN CONTENU (mesure du 2026-09-11). Sur une reconstruction a
|
|
# froid, `get_url` a rendu 0 octet sans erreur sur UNE machine des quatorze : le cache
|
|
# a servi un 200 au corps vide. La garde « retirer une ressource VIDE » ne mord qu'au
|
|
# passage SUIVANT, quand le fichier existe deja — au premier, elle n'a rien a retirer.
|
|
#
|
|
# Le defaut ne se lisait pas ici. Il se lisait deux cents lignes plus loin, dans un
|
|
# `apt` qui accusait la SIGNATURE : « Missing key ..., which is needed to verify
|
|
# signature ». Un message qui envoie chercher une cle revoquee chez le fournisseur,
|
|
# alors que le fichier local fait zero octet.
|
|
ansible.builtin.assert:
|
|
that:
|
|
- (cle_grafana_posee.stat.size | default(0)) > 0
|
|
fail_msg: >-
|
|
La cle de signature Grafana ({{ client_journal_depot_cle_fichier }}) fait 0 octet.
|
|
Le depot a repondu sans corps — cache vide, ou amont injoignable. apt echouera
|
|
plus loin en accusant la signature : c'est ICI qu'il faut regarder.
|
|
|
|
- name: Retirer la forme HTTPS du depot (elle contournait le cache)
|
|
ansible.builtin.apt_repository:
|
|
repo: "{{ client_journal_depot_source | replace('http://', 'https://') }}"
|
|
filename: grafana
|
|
state: absent
|
|
when: client_journal_depot_schema == 'http'
|
|
|
|
- name: Ajouter le depot apt Grafana
|
|
ansible.builtin.apt_repository:
|
|
repo: "{{ client_journal_depot_source }}"
|
|
filename: grafana
|
|
state: present
|
|
|
|
# LE CACHE DU CONTROLEUR D'ABORD, LE DEPOT DISTANT POUR LE RESTE (2026-09-03).
|
|
#
|
|
# Les depots tiers sont en HTTPS, et `client_artefacts` pose
|
|
# `Acquire::https::Proxy "DIRECT"` — ils CONTOURNENT donc le cache du site et sortent sur
|
|
# Internet a chaque construction de VM. `make cacher-paquets` les tire une fois, versions
|
|
# epinglees et empreintes verifiees ; ce role les depose depuis ce cache.
|
|
- name: Poser les paquets tiers depuis le cache du controleur
|
|
ansible.builtin.include_role:
|
|
name: paquets_tiers
|
|
vars:
|
|
paquets_tiers_noms: "{{ client_journal_paquets }}"
|
|
|
|
- name: Installer Grafana Alloy
|
|
ansible.builtin.apt:
|
|
# CE QUE LE CACHE N'A PAS FOURNI, ET RIEN D'AUTRE. Les dependances Debian passent
|
|
# par le cache du site en HTTP ; seuls les paquets tiers non caches exigent Internet.
|
|
# Reinstaller ce que le cache vient de poser ferait un `update_cache` inutile — et
|
|
# hors ligne, il echouerait APRES un travail deja fait.
|
|
name: "{{ client_journal_paquets | difference((paquets_tiers_disponibles | default({})).keys() | list) }}"
|
|
state: present
|
|
update_cache: true
|
|
|
|
when: (client_journal_paquets
|
|
| difference((paquets_tiers_disponibles | default({})).keys() | list)) | length > 0
|
|
- name: Autoriser Alloy a lire le journal systeme
|
|
ansible.builtin.user:
|
|
name: "{{ client_journal_utilisateur }}"
|
|
groups: systemd-journal
|
|
append: true
|
|
notify: Redemarrer alloy
|
|
|
|
# --- RECEPTEUR SYSLOG EN TLS (2026-10-12) --------------------------------------------
|
|
# Meme pont que node_exporter, Loki, Dovecot : le certificat de la machine, recopie la ou le
|
|
# service le lit, recopie a chaque renouvellement. Avant la configuration : Alloy redemarre
|
|
# avec des fichiers deja en place.
|
|
- name: Syslog TLS — repertoire des certificats (alloy)
|
|
ansible.builtin.file:
|
|
path: "{{ client_journal_syslog_tls_dir }}"
|
|
state: directory
|
|
owner: "{{ client_journal_utilisateur }}"
|
|
group: "{{ client_journal_utilisateur }}"
|
|
mode: "0700"
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
|
|
- name: Syslog TLS — script de synchronisation du certificat
|
|
ansible.builtin.template:
|
|
src: setops-alloy-cert-sync.sh.j2
|
|
dest: /usr/local/sbin/setops-alloy-cert-sync
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
|
|
- name: Syslog TLS — unites de synchronisation (service + path)
|
|
ansible.builtin.template:
|
|
src: "{{ item.s }}"
|
|
dest: "{{ item.d }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
loop:
|
|
- { s: "setops-alloy-cert-sync.service.j2", d: "/etc/systemd/system/setops-alloy-cert-sync.service" }
|
|
- { s: "setops-alloy-cert-sync.path.j2", d: "/etc/systemd/system/setops-alloy-cert-sync.path" }
|
|
loop_control:
|
|
label: "{{ item.d | basename }}"
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
|
|
- name: Syslog TLS — activer la surveillance du certificat (path)
|
|
ansible.builtin.systemd:
|
|
name: setops-alloy-cert-sync.path
|
|
enabled: true
|
|
state: started
|
|
daemon_reload: true
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
- not ansible_check_mode
|
|
|
|
- name: Syslog TLS — synchroniser le certificat maintenant
|
|
ansible.builtin.command: /usr/local/sbin/setops-alloy-cert-sync
|
|
changed_when: false
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
- not ansible_check_mode
|
|
|
|
# SEULS CES EMETTEURS PEUVENT ECRIRE. Sans certificat d'autorite, le recepteur n'accepterait
|
|
# aucun client : on refuse de deployer plutot que d'eteindre la journalisation de la
|
|
# frontiere en silence. Un CERTIFICAT, pas seulement une valeur non vide — et cette forme-la
|
|
# echappe a P32, qui ne lit pas les `when:` et aurait exige la variable de chaque locataire.
|
|
- name: Syslog TLS — exiger les autorites des emetteurs admis
|
|
ansible.builtin.assert:
|
|
that:
|
|
- client_journal_syslog_clients_ca_contenu is search('BEGIN CERTIFICATE')
|
|
fail_msg: >-
|
|
client_journal_syslog_tls est vrai, mais aucune autorite d'emetteur n'est fournie
|
|
(client_journal_syslog_clients_ca_contenu) : le recepteur refuserait tout le monde.
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
|
|
- name: Syslog TLS — poser les autorites des emetteurs admis
|
|
ansible.builtin.copy:
|
|
content: "{{ client_journal_syslog_clients_ca_contenu }}"
|
|
dest: "{{ client_journal_syslog_tls_dir }}/clients.crt"
|
|
owner: "{{ client_journal_utilisateur }}"
|
|
group: "{{ client_journal_utilisateur }}"
|
|
mode: "0644"
|
|
notify: Redemarrer alloy
|
|
when:
|
|
- client_journal_syslog_ecoute | length > 0
|
|
- client_journal_syslog_tls | bool
|
|
|
|
- name: Poser l'autorite qui signe Loki (machine sans PKI)
|
|
ansible.builtin.copy:
|
|
content: "{{ client_journal_loki_ca_contenu }}"
|
|
dest: "{{ client_journal_loki_ca }}"
|
|
owner: root
|
|
group: root
|
|
mode: "0644"
|
|
notify: Redemarrer alloy
|
|
when: client_journal_loki_ca_contenu | length > 0
|
|
|
|
- name: Deployer la configuration Alloy
|
|
ansible.builtin.template:
|
|
src: config.alloy.j2
|
|
dest: "{{ client_journal_config }}"
|
|
owner: root
|
|
group: "{{ client_journal_utilisateur }}"
|
|
mode: "0640"
|
|
notify: Redemarrer alloy
|
|
|
|
# Le port d'ecoute est IMPOSE ici plutot que subi : voir defaults/main.yml.
|
|
#
|
|
# AUCUN RAPPORT D'USAGE (mesure du 2026-10-10). Par defaut, Alloy envoie a
|
|
# stats.grafana.org la liste de ses composants actifs : relevé a la frontiere depuis
|
|
# CHAQUE machine des trois ecosystemes, ~70 connexions par jour et par locataire. Il
|
|
# passait par la sortie 443 generale du socle, qui n'existe plus.
|
|
- name: Imposer le port de l interface locale d Alloy, sans rapport d usage
|
|
ansible.builtin.lineinfile:
|
|
path: /etc/default/alloy
|
|
regexp: "^CUSTOM_ARGS="
|
|
line: >-
|
|
CUSTOM_ARGS="--server.http.listen-addr={{ client_journal_http_adresse }}:{{ client_journal_http_port }} --disable-reporting"
|
|
create: false
|
|
notify: Redemarrer alloy
|
|
|
|
- name: Activer et demarrer Alloy
|
|
ansible.builtin.systemd:
|
|
name: "{{ client_journal_service }}"
|
|
enabled: true
|
|
state: started
|
|
|
|
# LA SONDE VIT AVEC LE ROLE QU'ELLE MESURE (docs/supervision-conception.md). Le porteur
|
|
# `client_sante` fait tourner tout ce qui vit dans ce repertoire ; 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"
|
|
|
|
# La sonde compare le compteur de lignes jetees a celui du passage precedent : il lui
|
|
# faut un endroit ou s'en souvenir. `/var/lib`, parce que ce n'est pas un cache — le
|
|
# perdre ferait passer une cicatrice ancienne pour une perte en cours.
|
|
- name: Assurer le repertoire d etat des sondes
|
|
ansible.builtin.file:
|
|
# 0755, COMME PARTOUT AILLEURS (2026-10-01) : quatre roles tenaient ce repertoire, trois
|
|
# en 0750 et `serveur_artefacts` en 0755 — apt-cacher-ng doit le traverser pour servir
|
|
# le depot de binaires du site. Le dernier deploye gagnait : au site, le 2026-10-01, il
|
|
# rendait 403, et chaque runner allait chercher 500 Mo sur Internet. Le repertoire ne
|
|
# tient que des etats de sondes, deja lisibles. Garde : test_repertoires_partages.py.
|
|
path: /var/lib/setops
|
|
state: directory
|
|
owner: root
|
|
group: root
|
|
mode: "0755"
|
|
|
|
- name: Deposer la sonde d expedition des journaux
|
|
ansible.builtin.template:
|
|
src: sonde-journaux.sh.j2
|
|
dest: /usr/local/lib/setops/sondes/journaux.sh
|
|
owner: root
|
|
group: root
|
|
mode: "0750"
|