diff --git a/CHANGELOG.md b/CHANGELOG.md index d14b492..c97bbc1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,50 @@ # CHANGELOG — Set-OPS +## 2026-08-13 — Reconstruction complète en `10.17.x.x` : sept défauts, tous invisibles avant + +Chezlepro reconstruit **depuis zéro** sur l'adressage dérivé sans décalage. Sept défauts +sont tombés, et **aucun n'était détectable sur une flotte déjà debout** — c'est tout +l'argument de la reconstruction comme preuve. + +| # | Défaut | Pourquoi il était invisible | +|---|---|---| +| 1 | aucune route vers le tenant sur le poste | posée à la main un jour, jamais persistée ; disparue à la réactivation de l'interface | +| 2 | `backup-01` exigeait l'AC d'Icinga | l'AC existait déjà sur l'ancienne flotte | +| 3 | ma correction inversait les couches | **refusée par P08** avant d'entrer au dépôt | +| 4 | base Forgejo à moitié initialisée | séquelle de l'arrêt du défaut n°2 | +| 5 | **`/etc/hosts` : nom court avant le FQDN** | `hostname -f` faux sur **les quatorze machines, depuis toujours** | +| 6 | API Icinga jamais activée | la garde `creates:` d'`api setup` la saute dès que le fichier existe | +| 7 | restic refuse tout le lot si un chemin manque | `/srv/web` n'existe qu'après la première webapp | + +### Le cinquième dépassait Icinga + +`/etc/hosts` déclarait `10.17.18.21 backup-01 backup-01.chezlepro.internal`. Or +`hostname -f` rend le **premier** nom : toute la flotte se croyait appelée `mon-01`, jamais +`mon-01.chezlepro.internal`. Conséquence visible sur Icinga (certificat `CN=mon-01` +invérifiable en appelant par le FQDN) — mais le même piège attendait Postfix (`myhostname`), +les journaux et tout ce qui se nomme ainsi. **FQDN d'abord, nom court en alias.** + +### Ce qu'on ne corrige pas : la convention d'Icinga + +`icinga2 api setup` **réécrit** `NodeName` d'après le nom court et nomme ses certificats +d'après lui. Aligné avant, il est écrasé ; aligné après, les certificats portent le mauvais +nom. On a essayé les deux. **On adopte donc sa convention** : le dépôt appelle l'API par le +nom court, celui que le certificat porte, résolu par le plancher `/etc/hosts`. + +`serveur_icinga_node_name` dérive désormais de l'**inventaire** et non d'`ansible_fqdn` — +un fait qui dépend du résolveur de la machine, et qui a rendu `mon-01` alors que le FQDN +était correct. + +> **Une leçon presque comique.** Le commentaire écrit pour avertir qu'une séquence +> accolade-dièse casse les gabarits Jinja… contenait cette séquence, et cassait le gabarit. +> Neuf hôtes en échec pour un avertissement mal rédigé. + +### Verdict + +**Sept devis CONFORME**, `prouver` 36/36, `make test` 0. Neuf sauvegardes réussies, cinq +sans objet, et la supervision reçoit les rapports du dépôt avec vérification du pair. + + ## 2026-08-12 — D-79 : l'hyperviseur filtre encore en iptables *legacy* Question de l'exploitant : *« pourquoi les règles de sécurité au pare-feu global, alors diff --git a/docs/audit/preuve-2026-08-13.md b/docs/audit/preuve-2026-08-13.md new file mode 100644 index 0000000..03a49e0 --- /dev/null +++ b/docs/audit/preuve-2026-08-13.md @@ -0,0 +1,72 @@ +# Preuve de conformite — Set-OPS — 2026-08-13 + +> Genere par `make prouver` (`scripts/prouver.py`). **Rejouable** : relancer +> reproduit ce rapport. Chaque preuve rejoue l'outillage existant du depot ; +> aucune validation n'est reimplementee ici. Voir le mode d'emploi : +> [`docs/audit/README.md`](README.md), et le registre trace : +> [`docs/audit/affirmations.md`](affirmations.md). + +- **Instance** : `instance` — inventaire `instance/inventories/principal/hosts.yml` +- **Verdict** : ✅ CONFORME (36 OK · 0 echec · 0 saute) + +## Preuves + +| # | Preuve | Affirmations | Statut | Detail | +|---|---|---|---|---| +| P01 | Lint (ansible-lint) | AFF-006 | ✅ OK |  | +| P02 | Tests unitaires (inventory_host) | — | ✅ OK | >>> le verrou tient : aucune VM n'aurait ete touchee | +| P03 | Diff-vide du plan — TOUTES les instances | AFF-001, AFF-004, AFF-030, AFF-031, AFF-032 | ✅ OK | 3 instance(s) verifiee(s) — OPS-Chezlepro-lab, OPS-Chezlepro, OPS-Technolibre : plan et inventaire applique coincident. | +| P04 | Groupes <-> playbooks homonymes | AFF-008 | ✅ OK | | +| P05 | Dependances causales de groupes | AFF-009, AFF-084 | ✅ OK | | +| P06 | Validateurs de registres (serveurs/apps/bases/domaines) | AFF-003 | ✅ OK | Registre des domaines valide. | +| P07 | GUI (node --check) | AFF-033 | ✅ OK | JS du GUI : syntaxe valide (node --check). | +| P08 | Orchestration (couches + graphe) | AFF-070 | ✅ OK | Orchestration coherente : 30 groupes classes, aucun cycle, aucune arete en arriere. | +| P09 | Flux reseau (schema + matrice) | AFF-071 | ✅ OK | Flux coherents : 29 rôles, 77 flux, schéma + matrice OK. | +| P10 | Handlers <-> notify | AFF-034, AFF-035 | ✅ OK | Tout notify pointe vers un handler du meme role (49 roles). | +| P11 | Syntaxe des playbooks (--syntax-check) | AFF-083 | ✅ OK | playbook: playbooks/proxmox/cloner_vm_debian.yml | +| P12 | Existence des runbooks cites | AFF-010, AFF-011, AFF-012, AFF-083 | ✅ OK | 17/17 runbooks/registres cites presents. | +| P13 | Invariants structurels/doctrinaux | AFF-015, AFF-022, AFF-037, AFF-038, AFF-062 | ✅ OK | LICENSE, socle dossier, pas de couches paralleles, SSH clef-only, nftables off : OK. | +| P14 | Pas de chemin lab/ code en dur | AFF-097 | ✅ OK | Aucun chemin instance/inventories/lab/group_vars code en dur. | +| P15 | Modele public socle valide | AFF-022, AFF-099 | ✅ OK | Modele public socle : domaines/serveurs/applications/bases valides. | +| P16 | Inventaire Ansible complet (--list) | AFF-030 | ✅ OK | 14 hotes, 31 groupes (inventaire dechiffre et parse). | +| P17 | Tous les modeles valident (registres + underlay) | AFF-022, AFF-099 | ✅ OK | Les 1 modele(s) decouvert(s) valident. | +| P18 | Gabarit de voute complet | AFF-026 | ✅ OK | Gabarit de voute complet : 26 secret(s) exige(s), tous presents. Voute reelle : 29 cle(s), aucun manque. | +| P19 | Le GUI couvre le schema du plan | AFF-002, AFF-095 | ✅ OK | GUI : les 28 champ(s) des plans reels sont editables (2 plan(s) inspecte(s)), registres toleres : nomenclature. | +| P20 | Adressage 100% derive du seed (aucun stocke) | AFF-001, AFF-003 | ✅ OK | 2 nomenclature(s) : adressage 100% derive du seed index. | +| P21 | Federation : aucun index en collision | AFF-102 | ✅ OK | Federation coherente : 2 instance(s) federee(s), aucun index en collision. | +| P22 | Plan de recette a jour (genere du wiki) | AFF-002 | ✅ OK | Plan de recette à jour (20 sections). | +| P23 | Underlay sans collision avec la plage tenant | AFF-103 | ✅ OK | Underlay conforme : 6 reseau(x), aucune collision avec la plage tenant. | +| P24 | Frontiere nord/sud : acces d'administration declare | AFF-104 | ✅ OK | CONFORME : frontiere nord/sud, 39 regles, 12 routes, admin=10.0.0.0/24,192.168.254.2/32,192.168.255.2/32. | +| P25 | Pare-feu Proxmox : est-ouest intra-tenant derive | AFF-107 | ✅ OK | CONFORME : pare-feu Proxmox, 2 tenant(s), 40 groupe(s), 66 regle(s). | +| P26 | Integrations universelles : aucun hote laisse de cote | AFF-108 | ✅ OK | 14 hote(s) x 4 integration(s) universelle(s) : aucune lacune, aucune recopie (1 exemption(s) derivee(s) du service rendu). | +| P27 | Propriete des intrants : hebergeur et tenant separes | AFF-109 | ✅ OK | 0 cle(s) de cluster chez l'hebergeur, aucune recopiee dans les group_vars du tenant. | +| P28 | Pools Proxmox : un par tenant, sans collision | AFF-110 | ✅ OK | CONFORME : 2 pool(s) Proxmox, 28 VM placee(s), aucun nom ni VMID en collision. | +| P29 | Authentification : chaque role declare sa position | AFF-111 | ✅ OK | 23 role(s) serveur declares (interne-sans-auth 2, ldap-direct 2, sans-auth-humaine 12, socle-identite 2, web-sso 5) ; 2 lacune(s) nommee(s) : serveur_loki, serv | +| P30 | SDN EVPN : zones, VNets et sous-reseaux derives | AFF-112 | ✅ OK | CONFORME : SDN EVPN, 2 zone(s), 12 VNet(s), 12 sous-reseau(x), aucune collision. | +| P31 | Documentation : tout ce que le depot FAIT est nomme | — | ✅ OK | 44 scripts expliques et atteignables, 92 cibles make documentees, 54 roles avec README. | +| P32 | Intrants exiges par les roles : tous fournis | — | ✅ OK | CONFORME : 35 exigence(s) de role, toutes satisfaites (126 cle(s) declaree(s) par l'instance). | +| P33 | Aucune collision de port entre roles co-localises | — | ✅ OK | CONFORME : 32 revendication(s) de port, aucune collision entre roles co-localises (33 groupes). | +| P34 | Chaque document declare son lecteur | — | ✅ OK | 39 document(s) declarent leur lecteur (16 genere(s) exempte(s)). | +| P35 | Toute application exigeant une base en a une au plan | — | ✅ OK | 5 application(s) exigeant une base l'ont toutes (4 entree(s) au registre). | +| P36 | Tout detenteur d'etat porte une sauvegarde | — | ✅ OK | 9 hote(s) detiennent de l'etat, tous porteurs de `client_backup` (9 groupe(s) au catalogue). | + +## Couverture des affirmations ✅ du registre + +Chaque affirmation ✅ automatisable est couverte par la preuve indiquee ci-dessus. +Les ✅ **structurelles/doctrinales** non rejouables par une commande (ex. AFF-005 +`make`=aide, AFF-014 ciblage groupe, AFF-024 `instancier-appliquer`, AFF-051 autorite +d'AGENTS.md, AFF-073/075 gardes `make`, AFF-090 wiki) ont ete verifiees a l'audit ; +elles restent hors du harnais recurrent (rien d'executable a rejouer). + +## Declarations d'intention (⚪ invérifiables localement — assumees) + +Ces affirmations ne sont pas rejouables hors production ; elles sont **assumees** +comme declarations d'intention, non comme preuves : + +- **AFF-036** — « testables avec `--check` autant que possible » : verifiable seulement + contre une flotte vivante. +- **AFF-091** — contenu pedagogique du wiki : affirmations conceptuelles. +- **AFF-096** — « GUI 100 % francais » : revue exhaustive des libelles rendus, non automatisee. +- **AFF-007** — hote d'exemple `web-frontal-01` : placeholder assume. + +_Rapport genere le 2026-08-13._ diff --git a/roles/client_backup/templates/sauvegarder.sh.j2 b/roles/client_backup/templates/sauvegarder.sh.j2 index 498f40d..2ac0974 100644 --- a/roles/client_backup/templates/sauvegarder.sh.j2 +++ b/roles/client_backup/templates/sauvegarder.sh.j2 @@ -24,7 +24,30 @@ CHEMINS=( {% endfor %} {% endfor %} ) -restic backup --host "{{ inventory_hostname }}" --tag setops "${CHEMINS[@]}" +# Un chemin DECLARE peut ne pas exister encore : `/srv/web` avant la premiere webapp, +# le stockage de Nextcloud avant son installation. Restic refuse alors TOUT le lot +# (« all source directories/files do not exist ») et la sauvegarde entiere echoue — +# constate le 2026-08-13 sur trois hotes d'une flotte neuve. +# On ne sauvegarde donc que ce qui existe, et on le DIT quand il n'y a rien : c'est a la +# supervision de signaler un depot vide, pas au script de mourir. +# NOTE : ne JAMAIS ecrire la longueur d'un tableau bash dans ce gabarit — la sequence +# accolade-diese y ouvre un commentaire Jinja et casse le rendu. On compte a la main. +# (Ce commentaire a lui-meme casse le gabarit une fois, en citant la sequence.) +PRESENTS=() +N_PRESENTS=0 +for c in "${CHEMINS[@]}"; do + if [[ -e "$c" ]]; then + PRESENTS+=("$c") + N_PRESENTS=$((N_PRESENTS + 1)) + else + echo "absent, ignore : $c" >&2 + fi +done +if (( N_PRESENTS == 0 )); then + echo "aucun chemin declare n'existe encore sur cet hote — rien a sauvegarder." >&2 + exit 0 +fi +restic backup --host "{{ inventory_hostname }}" --tag setops "${PRESENTS[@]}" # 4. Rétention. restic forget {{ client_backup_retention }} --prune diff --git a/roles/hosts_statiques/templates/hosts.j2 b/roles/hosts_statiques/templates/hosts.j2 index 7ccdae0..37e884d 100644 --- a/roles/hosts_statiques/templates/hosts.j2 +++ b/roles/hosts_statiques/templates/hosts.j2 @@ -6,9 +6,15 @@ ff02::1 ip6-allnodes ff02::2 ip6-allrouters # --- Écosystème (dérivé du plan ; chaque hôte → son IP réelle) --- +# LE FQDN EN PREMIER, le nom court en alias. `hostname -f` rend le PREMIER nom trouve +# pour l'adresse de la machine : avec le nom court en tete, toute la flotte se croyait +# appelee « mon-01 » et non « mon-01.chezlepro.internal ». Consequence mesuree le +# 2026-08-12 : `icinga2 api setup` emettait un certificat CN=mon-01/SAN=mon-01, que +# personne ne pouvait verifier en appelant par le FQDN. Le meme piege attend tout ce qui +# se nomme par `hostname -f` — Postfix (myhostname), les journaux, les certificats. {% set fleet = (groups.get('hotes_actifs', []) + groups.get('hotes_planifies', [])) | unique | sort %} {% for h in fleet if hostvars[h].ansible_host is defined %} -{{ "%-15s" | format(hostvars[h].ansible_host) }} {{ h }}{{ (' ' ~ h ~ '.' ~ hosts_statiques_domaine) if hosts_statiques_domaine else '' }} +{{ "%-15s" | format(hostvars[h].ansible_host) }} {{ (h ~ '.' ~ hosts_statiques_domaine ~ ' ') if hosts_statiques_domaine else '' }}{{ h }} {% endfor %} {% if hosts_statiques_publier_expositions | default(false) and hosts_statiques_expositions %} diff --git a/roles/serveur_backup/tasks/main.yml b/roles/serveur_backup/tasks/main.yml index e093d4d..ec848e0 100644 --- a/roles/serveur_backup/tasks/main.yml +++ b/roles/serveur_backup/tasks/main.yml @@ -82,16 +82,29 @@ mode: "0600" no_log: true -# L'API Icinga presente un certificat emis par l'AC d'Icinga (il refuse d'en servir un -# autre : il renouvelle tout ce qui expire sous 30 jours, or nos certificats vivent 24 h). -# On recupere donc CETTE AC pour verifier le pair — plutot que de sauter la verification. +# L'AC d'Icinga sert a VERIFIER le pair en rapportant (il refuse de servir un certificat +# qu'il n'a pas emis). Mais le depot est un SERVICE et la supervision une APPLICATION : +# la couche `services` precede `apps`, donc Icinga n'existe pas encore au premier +# deploiement. Exiger son AC ici inversait le graphe — P08 l'a refuse. +# +# On la prend donc SI ELLE EXISTE, et c'est `serveur_icinga` qui la POUSSE quand il monte +# (roles/serveur_icinga/tasks/main.yml). Le rapporteur echoue bruyamment tant qu'elle +# manque — et il n'y a de toute facon personne a qui rapporter. +- name: L'AC d'Icinga est-elle deja disponible ? + ansible.builtin.stat: + path: "{{ serveur_backup_icinga_ca_source }}" + delegate_to: "{{ serveur_backup_icinga_hote }}" + register: serveur_backup_ca_presente + - name: Récupérer l'AC d'Icinga depuis l'hôte de supervision + when: serveur_backup_ca_presente.stat.exists ansible.builtin.slurp: src: "{{ serveur_backup_icinga_ca_source }}" delegate_to: "{{ serveur_backup_icinga_hote }}" register: serveur_backup_ca_icinga - name: Déposer l'AC d'Icinga pour la vérification du pair + when: serveur_backup_ca_presente.stat.exists ansible.builtin.copy: content: "{{ serveur_backup_ca_icinga.content | b64decode }}" dest: "{{ serveur_backup_ca_verification }}" diff --git a/roles/serveur_backup/templates/verifier-sauvegardes.sh.j2 b/roles/serveur_backup/templates/verifier-sauvegardes.sh.j2 index 7800026..042ab71 100644 --- a/roles/serveur_backup/templates/verifier-sauvegardes.sh.j2 +++ b/roles/serveur_backup/templates/verifier-sauvegardes.sh.j2 @@ -18,7 +18,7 @@ set -uo pipefail RACINE="{{ serveur_backup_racine }}" -API="https://{{ serveur_backup_icinga_hote }}.{{ domaine_interne }}:5665" +API="https://{{ serveur_backup_icinga_hote }}:5665" # nom COURT : c'est celui du certificat TTL={{ serveur_backup_ttl_icinga }} AGE_WARN={{ serveur_backup_age_warn_h }} AGE_CRIT={{ serveur_backup_age_crit_h }} diff --git a/roles/serveur_icinga/defaults/main.yml b/roles/serveur_icinga/defaults/main.yml index 8789d75..380e3b7 100644 --- a/roles/serveur_icinga/defaults/main.yml +++ b/roles/serveur_icinga/defaults/main.yml @@ -42,8 +42,15 @@ serveur_icinga_db_ca: "/etc/step/certs/root_ca.crt" # NodeName aligne sur le FQDN : le certificat auto-emis porte NodeName en CN et en SAN, # et c'est par le FQDN qu'on appelle l'API. Sans cet alignement, aucune verification ne # peut reussir. -serveur_icinga_node_name: "{{ ansible_fqdn | default(ansible_hostname) }}" +# DERIVE DE L'INVENTAIRE, pas de `ansible_fqdn`. Le fait depend du resolveur de la +# machine et de la forme de /etc/hosts — il a rendu « mon-01 » alors que le FQDN etait +# correct (2026-08-12), produisant un certificat SAN=mon-01 que personne ne pouvait +# verifier en appelant par le nom complet. L'inventaire, lui, est la meme source que +# celle dont `serveur_backup` compose son URL : les deux ne peuvent pas diverger. +serveur_icinga_node_name: "{{ inventory_hostname }}.{{ domaine_interne }}" serveur_icinga_ca: "/var/lib/icinga2/ca/ca.crt" +# Ou le depot de sauvegarde attend cette AC (cf. serveur_backup_ca_verification). +serveur_icinga_ca_destination_depot: "/etc/setops/icinga-ca.crt" # --- Supervision des sauvegardes (resultats passifs pousses par le depot) --- # Le controle porte sur la VERITE DE TERRAIN (l'instantane cote depot), pas sur l'unite diff --git a/roles/serveur_icinga/tasks/main.yml b/roles/serveur_icinga/tasks/main.yml index 3f9f526..25a89df 100644 --- a/roles/serveur_icinga/tasks/main.yml +++ b/roles/serveur_icinga/tasks/main.yml @@ -50,6 +50,16 @@ serveur_icinga_db_host: "{{ resoudre_base_db_host }}" no_log: true +# --- Identite TLS de l'API (5665) --- +# ON NE TOUCHE PAS A `NodeName`, et c'est une lecon payee cher (2026-08-12). +# `icinga2 api setup` REECRIT lui-meme NodeName d'apres le nom COURT de la machine, puis +# nomme ses certificats d'apres lui : CN=mon-01, SAN=DNS:mon-01. Toute tentative de +# l'aligner sur le FQDN est effacee au passage suivant — on l'a essaye avant et apres +# `api setup`, sans succes. +# +# Plutot que de lutter contre sa convention, on l'ADOPTE : le consommateur appelle l'API +# par le nom que le certificat porte (`serveur_backup` compose son URL sur le nom court). +# Le plancher `/etc/hosts` deploye par `hosts_statiques` le resout sur chaque machine. - name: Configurer l'API Icinga 2 ansible.builtin.command: cmd: icinga2 api setup @@ -89,30 +99,15 @@ no_log: true notify: Redemarrer icingadb -# --- Identite TLS de l'API (5665) --- -# ARBITRAGE DU 2026-08-11, apres mesure. On a voulu servir ici le certificat step-ca, pour -# que les consommateurs verifient le pair au lieu de faire `curl -k`. Icinga le REFUSE -# structurellement, et le dit : -# -# information/ApiListener: Our certificate will expire soon, but we own the CA. Renewing. -# -# Les certificats Set-OPS vivent 24 H ; Icinga renouvelle tout ce qui expire sous 30 jours -# et, possedant une AC, re-emet avec la sienne. Il ecrasait donc le certificat step-ca a -# chaque demarrage. Ce n'est pas reparable par configuration. -# -# CE QU'ON FAIT A LA PLACE : Icinga garde son AC — un domaine de confiance FERME, ce qui -# est legitime — et le consommateur (serveur_backup) VERIFIE le pair contre cette AC-la. -# Le pair est authentifie ; seule la racine differe. Le `-k` disparait, ce qui etait le -# vrai probleme. -# -# NodeName reste aligne sur le FQDN : le certificat qu'Icinga s'emet porte NodeName en CN -# ET en SAN, et c'est par le FQDN qu'on l'appelle. Sans cet alignement, le SAN vaudrait -# « mon-01 » et toute verification echouerait. -- name: TLS — aligner NodeName sur le FQDN (identite de l'API) - ansible.builtin.lineinfile: - path: /etc/icinga2/constants.conf - regexp: '^const NodeName\s*=' - line: 'const NodeName = "{{ serveur_icinga_node_name }}"' +# `icinga2 api setup` active la fonctionnalite EN MEME TEMPS qu'il pose les certificats, +# et sa garde `creates:` la saute des que le fichier existe. Consequence mesuree le +# 2026-08-12 : apres un nettoyage des certificats, l'API restait DESACTIVEE — icinga2 +# demarrait, se declarait `active`, et n'ecoutait sur rien. Exiger l'activation +# separement rend l'etat independant de l'ordre des nettoyages. +- name: API — exiger que la fonctionnalite soit activee + ansible.builtin.command: + cmd: icinga2 feature enable api + creates: /etc/icinga2/features-enabled/api.conf notify: Redemarrer icinga2 - name: API — durcir l'ApiListener (aucune config ni commande acceptee) @@ -179,3 +174,25 @@ loop: - "{{ serveur_icinga_service_icinga2 }}" - "{{ serveur_icinga_service_icingadb }}" + +# --- Publier l'AC vers le depot de sauvegarde --- +# Le depot rapporte l'etat des instantanes a cette API et doit VERIFIER le pair. Il lui +# faut donc cette AC — mais lui est un SERVICE et nous une APPLICATION : il se deploie +# AVANT nous, et exiger son attente inverserait le graphe des couches (refuse par P08 le +# 2026-08-12). C'est donc a NOUS de la lui porter, une fois que `icinga2 api setup` l'a +# creee. Sens correct : l'application rejoint le service, jamais l'inverse. +- name: Publier l'AC d'Icinga vers le depot de sauvegarde + when: groups['serveur_backup'] | default([]) | length > 0 + ansible.builtin.slurp: + src: "{{ serveur_icinga_ca }}" + register: serveur_icinga_ca_contenu + +- name: Deposer l'AC sur le depot de sauvegarde + when: groups['serveur_backup'] | default([]) | length > 0 + ansible.builtin.copy: + content: "{{ serveur_icinga_ca_contenu.content | b64decode }}" + dest: "{{ serveur_icinga_ca_destination_depot }}" + owner: root + group: root + mode: "0644" + delegate_to: "{{ groups['serveur_backup'] | first }}"