Renommer une exposition touche cinq choses. Quatre suivent au deploiement ; la cinquieme est celle que le navigateur regarde. serveur_nginx pose le vhost et laisse le SAN en arriere — c est client_pki qui reemet, et rien ne le reclame. Mesure deux fois le 2026-09-10. Le controle compare trois listes : les expositions du plan, les noms du certificat servi, et le code que rend le vhost. Dans les deux sens : un nom reste dans le SAN apres avoir quitte le plan continue d etre authentifie. Piege trouve en l ecrivant : client_pki met toujours le FQDN et le nom court de la machine dans le SAN. Les compter comme vestiges faisait crier le controle a chaque execution — et un controle qui crie toujours ne se lit plus. Deux formes de terminaison, une seule question : le locataire termine sur un edge, le site sur la machine elle-meme. Le porteur se derive de l une ou de l autre. Ce n est pas une preuve : rien de statique ne peut lire un certificat servi. Les quatre controles a la demande n etaient nommes nulle part comme famille ; ils ont maintenant leur section dans les runbooks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Crgis8CxCWkAGFA1ecBz3q
328 lines
16 KiB
Markdown
328 lines
16 KiB
Markdown
# Runbooks d'exploitation — Set-OPS
|
|
|
|
> **Pour qui :** l'exploitant, face à une situation. Pas le mainteneur, pas l'apprenant.
|
|
> Le *pourquoi* vit dans les autres `docs/` ; la version pédagogique, dans le wiki. Ici,
|
|
> uniquement le *comment faire*.
|
|
>
|
|
> Tu viens de reprendre l'écosystème et tu ne sais pas par où entrer ?
|
|
> Wiki → **Reprendre l'écosystème**.
|
|
|
|
---
|
|
|
|
## 1. Un certificat a expiré / le login SSO est cassé
|
|
|
|
**Symptôme** : connexion à une app via le SSO échoue *après* le login ; log Grafana/app :
|
|
`tls: failed to verify certificate: x509: certificate has expired`.
|
|
|
|
**Cause fréquente** : le cert step-ca a été **renouvelé sur disque** mais le service (nginx sur
|
|
l'edge) **n'a pas été rechargé** → il sert l'ancien cert en mémoire.
|
|
|
|
**Diagnostic-réflexe** — comparer le cert *servi* au cert *fichier* :
|
|
```bash
|
|
# SERVI (en mémoire par nginx)
|
|
echo | openssl s_client -connect infra-edge-01.chezlepro.internal:443 -servername keycloak.chezlepro.internal 2>/dev/null \
|
|
| openssl x509 -noout -enddate
|
|
# FICHIER (sur disque)
|
|
openssl x509 -in /etc/step/certs/infra-edge-01.chezlepro.internal.crt -noout -enddate
|
|
```
|
|
Dates différentes (servi < fichier) ⇒ nginx sert un cert périmé.
|
|
|
|
**Fix immédiat** : `systemctl reload nginx` sur l'edge (idem `postfix`/`dovecot`/`slapd` selon le
|
|
service touché).
|
|
|
|
**Fix permanent (déjà en place)** : `client_pki_reload_services` recharge les vrais consommateurs
|
|
après chaque renouvellement — edge→nginx, mail→postfix/dovecot, annuaire→slapd. Vérifier :
|
|
`systemctl cat cert-renewer@$(hostname -f).service | grep ExecStartPost`.
|
|
Voir aussi l'unité wiki *PKI & confiance* (⑤ le renouvellement).
|
|
|
|
---
|
|
|
|
## 2. Donner Explore/Editor à un opérateur dans Grafana (RBAC)
|
|
|
|
Par défaut, un utilisateur SSO est **Viewer** (dashboards seulement, pas Explore). Pour l'élever :
|
|
|
|
1. Déclarer l'assignation dans l'inventaire de l'instance (group_vars `serveur_keycloak`) :
|
|
```yaml
|
|
serveur_keycloak_role_assignments:
|
|
- { user: <uid>, role: grafana-editor } # ou grafana-admin
|
|
```
|
|
2. Redéployer Keycloak (`make deployer` sur le nœud SSO) — kcadm **à chaud, sans coupure**.
|
|
3. L'utilisateur doit **se déconnecter/reconnecter** (Grafana applique le rôle à la connexion).
|
|
|
|
Mapping (défaut du rôle grafana) : `grafana-admin`→Admin, `grafana-editor`→Editor, sinon Viewer
|
|
(`serveur_grafana_oidc_role_path`).
|
|
|
|
> **Préférer le groupe à la personne — et c'est construit, pas un idéal.** Les groupes LDAP
|
|
> sont projetés dans Keycloak et émis en claim (`tasks/groupes-ldap.yml`, `claim-groupes.yml`),
|
|
> et `roles/serveur_grafana/meta/acces.yml` déclare déjà `sysadmin ⇒ Admin`,
|
|
> `personnel ⇒ Viewer`. Ajouter quelqu'un au **groupe** lui ouvre Grafana, Forgejo, Icinga et
|
|
> le courriel d'un seul geste — alors qu'une assignation nominative crée une dette qu'on
|
|
> découvre le jour du départ, service par service. L'assignation explicite ci-dessus reste
|
|
> le geste de dépannage, pas la façon normale d'accorder un accès. Voir `docs/autorisation.md` §5.
|
|
|
|
---
|
|
|
|
## 3. « Connection timed out during banner exchange » — le message qui accuse le réseau
|
|
|
|
**Symptôme** : Ansible marque un ou plusieurs hôtes injoignables, avec ce message. On soupçonne
|
|
le réseau, la route ou le pare-feu. **C'est presque toujours autre chose.**
|
|
|
|
**Pourquoi le message trompe.** À travers la frontière, la poignée TCP aboutit *toujours* — le
|
|
pare-feu y répond lui-même sans relayer (voir `frontiere-opnsense.md`). L'échec ne peut donc
|
|
pas se présenter comme un « connection refused » franc : il se manifeste plus tard, au moment
|
|
où le serveur devrait annoncer sa bannière SSH. D'où un message qui parle de délai réseau pour
|
|
un hôte qui, souvent, n'a jamais rien reçu.
|
|
|
|
**Les trois causes, par fréquence :**
|
|
|
|
1. **La VM n'est pas encore là.** `make creer-vm` rend la main dès que Proxmox a *démarré* la
|
|
VM, pas quand elle répond. C'est le cas normal, quotidien, et aucun correctif ne l'abolira :
|
|
les attentes actives du `Makefile` existent pour ça.
|
|
2. **`sshd` refuse une rafale.** `MaxStartups` / `MaxSessions` trop serrés coupent des
|
|
connexions parfaitement légitimes — deux déploiements interrompus le 2026-08-09, sur des
|
|
hôtes qui n'avaient ni redémarré ni perdu leur réseau. Réglés par
|
|
`ssh_hardening_max_startups` (`10:30:60`) et `ssh_hardening_max_sessions` (`10`), avec
|
|
l'arbitrage expliqué dans `roles/ssh_hardening/defaults/main.yml`.
|
|
3. **Le réseau, vraiment** — le cas le plus rare, et le dernier à examiner.
|
|
|
|
**Ce qui tranche, dans l'ordre :**
|
|
|
|
```
|
|
make hote-afficher HOTE=<nom> # l'hôte existe-t-il au plan ?
|
|
ssh -v ansible@<ip> 2>&1 | tail -20 # où la négociation s'arrête exactement
|
|
```
|
|
|
|
Une bannière (`SSH-2.0-OpenSSH…`) prouve que le chemin est bon et que le problème est
|
|
applicatif. **Aucun `connect()` ne prouvera quoi que ce soit ici** — il réussit vers le vide.
|
|
|
|
---
|
|
|
|
## 4. Brander une instance Forgejo (identité visuelle)
|
|
|
|
Activer dans l'inventaire (group_vars `serveur_forgejo`) :
|
|
```yaml
|
|
serveur_forgejo_branding: true
|
|
serveur_forgejo_app_name: "Forge Chezlepro"
|
|
serveur_forgejo_theme: "forgejo-dark"
|
|
serveur_forgejo_meta_description: "…"
|
|
```
|
|
Puis redéployer. Le rôle déploie le dossier `custom/` officiel (logo/favicon aurore, accent CSS par
|
|
variables, page d'accueil brandée) — léger, résistant aux MAJ (aucune classe interne touchée).
|
|
Note : ne s'applique qu'aux Forgejo **gérées par Set-OPS**.
|
|
|
|
## 5. Restaurer — et d'abord : prouver qu'on peut
|
|
|
|
> Éprouvé le 2026-08-12 sur Chezlepro. `make valider` rejoue la partie automatisable ;
|
|
> la restauration d'une base reste manuelle, et porte un piège décrit plus bas.
|
|
|
|
**Ce que `make valider` prouve tout seul**, pour chaque nœud détenteur d'état : le dernier
|
|
instantané se restaure, il en sort des fichiers, et — pour l'annuaire — qu'il est
|
|
**rejouable** (`slapadd -u`, essai à blanc, rien n'est écrit). Verdicts possibles :
|
|
|
|
| Verdict | Sens |
|
|
|---|---|
|
|
| `OK` | restauré, et non vide |
|
|
| `À CONFIRMER` | restauré, mais l'instantané n'emporte **aucun fichier** — légitime si ce nœud n'a pas encore de données, à trancher par un humain |
|
|
| `SANS OBJET` | ce nœud ne détient rien de non régénérable |
|
|
| `ÉCHEC` | la restauration elle-même a échoué |
|
|
|
|
### Restaurer les clés de l'autorité (`infra-pki-01`)
|
|
|
|
```
|
|
export RESTIC_REPOSITORY=$(grep -oP 'RESTIC_REPOSITORY="\K[^"]+' /usr/local/sbin/setops-sauvegarder.sh)
|
|
export RESTIC_PASSWORD_FILE=/etc/setops/restic.pass
|
|
t=$(mktemp -d); restic restore latest --target "$t"
|
|
diff -r "$t/etc/step-ca" /etc/step-ca
|
|
```
|
|
|
|
Mesuré : **12 fichiers, 11 identiques octet pour octet**, `root_ca_key` et
|
|
`intermediate_ca_key` compris. Le seul écart attendu est `db/000000.vlog` — le journal de
|
|
la base badger de step-ca, qui avance à **chaque** émission de certificat.
|
|
|
|
### Rejouer une base depuis `pg_dumpall` — LE PIÈGE
|
|
|
|
`pg_dumpall` écrit `CREATE DATABASE <suivante>` **avant** le `\connect` correspondant.
|
|
Découper « du `\connect X` au `\connect` suivant » emporte donc un ordre qui vise une
|
|
**autre** base. Couper aussi sur `CREATE DATABASE`, et **vérifier avant de rejouer** :
|
|
|
|
```
|
|
awk '/^\\connect forgejo$/{f=1;next} f && (/^\\connect /||/^CREATE DATABASE /){exit} f' \
|
|
toutes-bases.sql > section.sql
|
|
|
|
grep -qE '^(DROP|CREATE|ALTER) DATABASE|^\\connect' section.sql \
|
|
&& { echo "REFUS : ordre hors-perimetre"; exit 1; }
|
|
|
|
runuser -u postgres -- createdb epreuve_restauration
|
|
runuser -u postgres -- psql -q -d epreuve_restauration -f section.sql
|
|
runuser -u postgres -- psql -tAd epreuve_restauration -c "select count(*) from information_schema.tables where table_schema='public'"
|
|
runuser -u postgres -- dropdb epreuve_restauration
|
|
```
|
|
|
|
Mesuré sur `forgejo` : rejeu en **0 erreur**, **130 tables**, et les comptes réels
|
|
(`forgejo-admin`, `sysadmin`). Production vérifiée intacte après coup.
|
|
|
|
**Ne jamais rejouer un `pg_dumpall` entier sur un cluster vivant** : il contient les
|
|
`DROP DATABASE` de toutes les bases. Une restauration réelle se fait sur un cluster neuf.
|
|
|
|
## 6. La bascule d'adressage de la fabric (D-77) — FAITE
|
|
|
|
> **Cette section décrivait une transition en cours jusqu'au 2026-09-06.** Elle est
|
|
> terminée : mesuré le 2026-08-22, **`10.0.0.0/24` n'existe plus** — ni `10.0.0.1`, ni
|
|
> `10.0.0.41` ne répondent. Le plan d'administration est `10.17.0.0/24` : la frontière y
|
|
> répond en `10.17.0.1` sur un port physique à elle, les commutateurs sont en `10.17.0.3`
|
|
> et `.4` avec cette passerelle par défaut, le poste de l'exploitant en `10.17.0.17`. Le
|
|
> renumérotage du tenant (`10.27` → `10.17`) est fait lui aussi.
|
|
>
|
|
> On garde la **méthode**, parce qu'elle est ce qui a permis de le faire sans coupure, et
|
|
> qu'un autre site la rejouera :
|
|
>
|
|
> **Ajouter avant de retirer, jamais l'inverse.** Un point de routage qui change d'adresse
|
|
> d'un coup coupe simultanément l'exploitant, les commutateurs qui l'ont en passerelle par
|
|
> défaut, et l'outil qui devait faire la bascule. La seconde adresse a donc vécu à côté de
|
|
> l'ancienne (`ipalias`), et l'ancienne n'est tombée qu'en **dernier**.
|
|
>
|
|
> **Distinguer une destination d'un chemin (D-78).** Un réseau qui n'est **jamais** une
|
|
> destination — seulement un chemin — n'a aucune raison d'être unique entre deux hébergeurs :
|
|
> il sort de l'espace dérivé, vers `192.168.<vlan>.0/24`. Seule la **gestion** doit rester
|
|
> unique d'un site à l'autre, parce que le poste de l'exploitant, un VPN et demain un lien
|
|
> inter-sites doivent l'atteindre.
|
|
>
|
|
> **Appliqué pour le transport VXLAN seulement, à ce jour** (mesuré le 2026-09-06) :
|
|
> `underlay-vxlan` est bien en `192.168.50.0/24`. Le **transit** (`10.0.4.0/24`, VLAN 40) et
|
|
> le **stockage** (`10.11.5-7.x`, VLAN 5/6/7) sont encore dans l'ancien espace. Ce n'est pas
|
|
> une urgence — ces réseaux ne quittent jamais leur site — mais la carte doit dire ce qui est,
|
|
> pas ce qui a été décidé. Un **site neuf** se monte directement au schéma final : il n'a
|
|
> aucune transition à subir.
|
|
>
|
|
> **Un plan d'adressage ne doit pas dépendre de l'ordre d'une migration.** Le VLAN de
|
|
> transport est passé de 11 à 50 — non parce que 11 était mauvais, mais parce que
|
|
> `192.168.11.0/24` est occupé par le contrôle de la grappe. Faire dépendre un plan
|
|
> d'adressage de l'**ordre** d'une migration est exactement la dette qui se paie un an
|
|
> plus tard.
|
|
|
|
### Ce qui reste, et qui n'est PAS un reliquat de la bascule
|
|
|
|
Les hyperviseurs gardent **deux** plans, et c'est voulu :
|
|
|
|
| Plan | Réseau | Ce qu'il porte |
|
|
|---|---|---|
|
|
| administration | `10.17.0.0/24` (`vmbr3`, segment physique) | les **équipements** et l'exploitant ; **aucune VM ne peut y naître** (aucun pont ne le touche, et le validateur refuse qu'on y déclare une machine) |
|
|
| contrôle de la grappe | `192.168.11.0/24` (`vmbr0`, carte dédiée) | l'**interface web Proxmox** et le dialogue entre nœuds — c'est par là qu'on atteint `ansible@192.168.11.4x` |
|
|
|
|
> **Le `/24` de gestion vit à l'intérieur du `/16` du tenant, et ce n'est pas un conflit.**
|
|
> Les zones d'un tenant commencent au 3ᵉ octet 16 ; la bande 0-15 est libre pour la fabric,
|
|
> et la route connectée du `/24` est plus spécifique que celle du `/16` — la règle du
|
|
> préfixe le plus long, pas une coïncidence. Il faut cependant le **déclarer**
|
|
> (`bande_basse_de:` dans `underlay.yml`), sinon le validateur ne peut pas distinguer ce
|
|
> chevauchement voulu d'un chevauchement accidentel.
|
|
|
|
## 7. Le nœud qui porte le gabarit tombe — la reproduction s'arrête
|
|
|
|
**Symptôme.** Aucune VM nouvelle ne peut naître. `make creer-vm`, `make flotte-creer` et
|
|
`make reconstruire` échouent au clonage. Les machines existantes, elles, ne bronchent pas.
|
|
|
|
**Ce qui se passe.** Le gabarit doré vit sur un nœud nommé — `vishnu` chez l'hébergeur de
|
|
référence — et sa configuration porte ce nom dans son chemin même :
|
|
|
|
```
|
|
/etc/pve/nodes/vishnu/qemu-server/9006.conf
|
|
```
|
|
|
|
Le clonage appelle `nodes/vishnu/qemu/9006/clone` : c'est l'API de **ce nœud-là** qui doit
|
|
répondre. Nœud éteint, API muette, aucune naissance.
|
|
|
|
**Ce qui ne se passe PAS, et qu'il faut savoir avant de paniquer.** Les données du gabarit
|
|
ne sont pas perdues. Mesure du 2026-09-10 :
|
|
|
|
```
|
|
pool CephNVMe size=3 min_size=2
|
|
OSD sur les trois hôtes : asgard, gandalf, vishnu
|
|
base-9006-disk-0, base-9006-disk-1 présentes dans le pool
|
|
```
|
|
|
|
L'image est répliquée trois fois et reste lisible avec un nœud en moins. Et les quatorze VM
|
|
d'un écosystème tournent ailleurs, sur leurs propres disques Ceph : elles ne s'aperçoivent
|
|
de rien.
|
|
|
|
> **`vishnu` n'est pas un point unique de défaillance pour l'exploitation. Il l'est pour la
|
|
> reproduction** — et la reproduction est ce que ce dépôt existe pour garantir.
|
|
|
|
**La manœuvre.** Deux gestes, quelques minutes, aucun mouvement de données :
|
|
|
|
```bash
|
|
# 1. re-héberger la configuration sur un nœud debout
|
|
mv /etc/pve/nodes/vishnu/qemu-server/9006.conf \
|
|
/etc/pve/nodes/asgard/qemu-server/9006.conf
|
|
|
|
# 2. le déclarer, sinon la garde refusera
|
|
# SITE-Chezlepro/plan/10-intrants.yml : gabarit.noeud: asgard
|
|
```
|
|
|
|
Puis vérifier :
|
|
|
|
```bash
|
|
make gabarit-etat # doit rendre « Conforme »
|
|
```
|
|
|
|
**Pourquoi c'est aussi court.** Parce que le disque du gabarit vit sur un stockage
|
|
**partagé** depuis le 2026-09-10 : le déplacement ne bouge qu'un fichier de configuration.
|
|
Sur un stockage local, il aurait fallu recopier 16 Go — ou refabriquer le gabarit.
|
|
|
|
**L'ordre compte.** Déplacer sans déclarer laisse `gabarit_etat` en écart ; déclarer sans
|
|
déplacer fait échouer le clonage sur un nœud qui ne détient pas le modèle. Faire les deux,
|
|
dans cet ordre.
|
|
|
|
**Ce que cette section ne fait pas.** Elle ne supprime pas la dépendance : elle la rend
|
|
connue et courte. Deux remèdes de fond existent, aucun n'est appliqué :
|
|
|
|
- **déplacer le gabarit là où vivent déjà les VM** — ça ne supprime pas le point unique,
|
|
ça cesse d'en avoir *deux* (le nœud des VM et celui du modèle) ;
|
|
- **une garde** qui refuse quand le nœud du gabarit n'héberge aucune machine de la flotte,
|
|
c'est-à-dire quand la reproduction dépend d'un nœud qui ne porte rien d'autre.
|
|
|
|
*Une dépendance qu'on documente sans la mesurer reste une dépendance qu'on découvrira au
|
|
mauvais moment.*
|
|
|
|
## 8. Les contrôles à la demande — ce que `make prouver` ne peut pas voir
|
|
|
|
`make prouver` est **statique** : il lit le dépôt, zéro appel réseau. C'est ce qui le rend
|
|
rejouable partout, par n'importe qui, et présentable comme pièce justificative. Le prix de
|
|
cette propriété : il ne peut rien dire de ce qui ne s'observe qu'en ouvrant une connexion.
|
|
|
|
Quatre contrôles comblent ce creux. Aucun ne corrige quoi que ce soit — ils regardent, et
|
|
rendent `0` si tout concorde.
|
|
|
|
| Contrôle | Ce qu'il compare | Le piège qu'il attrape |
|
|
| --- | --- | --- |
|
|
| `make expositions-etat` | Expositions du plan ↔ **SAN du certificat servi** ↔ code du vhost | Un renommage déployé partout **sauf** dans le certificat |
|
|
| `make gabarit-etat` | Gabarit déclaré ↔ VM modèle réelle | Le modèle a dérivé de ce que le plan promet |
|
|
| `make routes-fabric-etat` | Zones déclarées ↔ routes déclarées ↔ routes vivantes | Une route **vivante mais non déclarée** — elle part au redémarrage |
|
|
| `make frontiere-plan` | Registre des flux ↔ règles de la frontière | Une règle posée à la main, qu'aucune déclaration ne porte |
|
|
|
|
Ajouter `SITE=1` à `expositions-etat` pour interroger l'écosystème du SITE plutôt que
|
|
l'instance montée.
|
|
|
|
### Pourquoi `expositions-etat` existe
|
|
|
|
Renommer une exposition touche cinq choses. Quatre suivent au déploiement ; la cinquième,
|
|
non :
|
|
|
|
```
|
|
serveur_powerdns la zone publie le nouveau nom ✓
|
|
serveur_keycloak le client OIDC accepte le retour ✓
|
|
le service il fabrique ses URL avec le bon nom ✓ (P67)
|
|
serveur_nginx le vhost répond sur le nouveau nom ✓
|
|
client_pki le SAN du certificat porte le nom ✗ il faut le rejouer
|
|
```
|
|
|
|
Le symptôme est trompeur : le site répond, la page s'affiche, et c'est le **navigateur**
|
|
qui refuse — avec une erreur de certificat que personne ne relie à un renommage fait la
|
|
veille. Mesuré deux fois le 2026-09-10, sur `grafana → observatoire` puis `icinga → vigie`.
|
|
|
|
Le contrôle regarde **dans les deux sens**. Un nom resté dans le SAN après avoir quitté le
|
|
plan est un nom que le certificat continue d'authentifier : c'est exactement ce qu'avait
|
|
laissé le premier renommage, jusqu'au passage de `client_pki`.
|
|
|
|
> Un `INJOIGNABLE` ne condamne pas le service : il dit que **ce poste** n'a pas pu ouvrir
|
|
> la connexion. Les zones du SITE ne sont pas routées depuis le plan d'administration du
|
|
> locataire — le mur est la frontière, pas le vhost.
|