Compare commits

..

No commits in common. "2fde5c7ae7d852b6a99557115be2bca12a9d2244" and "2903d75831ec7b05f45595bd59d24a0fc6dfa789" have entirely different histories.

22 changed files with 165 additions and 400 deletions

View file

@ -1,5 +1,5 @@
--- ---
# Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main. # Ecrit par `make wiki-publier`, lu par la preuve P60. Ne pas editer a la main.
remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git remote: ssh://git@eregion.chezlepro.ca:2222/Alliance-Boreale/Set-OPS-Public.wiki.git
source: 3fe7e3c source: 1493059
date: 2026-09-29 date: 2026-09-29

View file

@ -122,11 +122,12 @@
| `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. | | `serveur_rspamd` | ingress | 11332 | tcp | localhost | clair | Protocole milter consommé par Postfix co-localisé (analyse + signature DKIM). Local uniquement. |
| `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. | | `serveur_rspamd` | ingress | 11334 | tcp | localhost | clair | Interface de contrôle rspamd (statistiques, apprentissage) en local. |
| `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. | | `serveur_step_ca` | ingress | 8443 | tcp | flotte | tls-requis | ACME + API step-ca : chaque nœud (client_pki) émet/renouvelle ses certificats et récupère la racine. |
| `serveur_web_dorsal` | ingress | 80 | tcp | edge, serveur_web_frontal | clair | Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics). | | `serveur_web_dorsal` | ingress | 80 | tcp | edge | clair | Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge). |
| `serveur_web_dorsal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot de chaque app (Forgejo souverain) au deploiement. | | `serveur_web_dorsal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot de chaque app (Forgejo souverain) au deploiement. |
| `serveur_web_frontal` | ingress | 80 | tcp | externe | clair | HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la. | | `serveur_web_frontal` | ingress | 80 | tcp | edge | clair | Contenu statique servi au navigateur via l'edge (TLS termine a l'edge). |
| `serveur_web_frontal` | ingress | 443 | tcp | externe | tls-requis | HTTPS public — les sites et services que le locataire publie sur l'Internet (attend son certificat Let's Encrypt). | | `serveur_web_frontal` | ingress | 80 | tcp | externe | clair | HTTP public — le premier contact d'un navigateur, et le defi HTTP d'ACME ; renvoie vers HTTPS. |
| `serveur_web_frontal` | egress | 80 | tcp | serveur_web_dorsal | clair | Relais vers le web dorsal, qui porte les sites et les applications publiques. | | `serveur_web_frontal` | ingress | 443 | tcp | externe | tls-requis | HTTPS public — les sites et services que le locataire publie sur l'Internet. |
| `serveur_web_frontal` | egress | 443 | tcp | flotte | tls | git clone/pull du depot du site (Forgejo souverain) au deploiement. |
## Synthèse chiffrement ## Synthèse chiffrement
@ -134,6 +135,6 @@
- **n-a** : 8 flux - **n-a** : 8 flux
- **ssh** : 8 flux - **ssh** : 8 flux
- **starttls** : 6 flux - **starttls** : 6 flux
- **tls** : 10 flux - **tls** : 11 flux
- **tls-cible** : 2 flux - **tls-cible** : 2 flux
- **tls-requis** : 36 flux - **tls-requis** : 36 flux

View file

@ -1,13 +1,9 @@
# serveur_web_dorsal # serveur_web_dorsal
**Le serveur qui porte les sites et les applications web du locataire** : webapps Hébergement de **webapps dynamiques** (runtime + service systemd), derrière l'edge.
dynamiques (runtime + service systemd) et, depuis le 2026-09-29, sites **statiques** Deuxième brique de la **plateforme webapp** de Set-OPS : **100 % natif** — venv/paquets
(repris du web frontal). **100 % natif** — venv/paquets + systemd + nginx, **zéro + systemd + nginx, **zéro conteneur**. Codifié depuis le spike du 2026-07-05
conteneur**. Codifié depuis le spike du 2026-07-05 (monregistraire : FastAPI + uvicorn + (monregistraire : FastAPI + uvicorn + SQLite).
SQLite ; site Alliance Boréale).
Il est relayé par **deux portes** : l'**edge** pour les noms internes (l'administration),
le **web frontal** (reverse proxy + WAF) pour les noms publics (l'Internet).
## Patron ## Patron
Pour chaque app déclarée dans `serveur_web_dorsal_apps` : Pour chaque app déclarée dans `serveur_web_dorsal_apps` :
@ -34,21 +30,6 @@ serveur_web_dorsal_apps:
``` ```
Et une app `groupe: serveur_web_dorsal, hote: <noeud>, port: 80, expose: [registraire.<domaine>]`. Et une app `groupe: serveur_web_dorsal, hote: <noeud>, port: 80, expose: [registraire.<domaine>]`.
## Sites statiques
Pour chaque site déclaré dans `serveur_web_dorsal_sites` : `git clone`/`pull` →
`/srv/web/<nom>`, puis un vhost nginx servant `<sous_dossier>` (ex. `public/`), avec
en-têtes de sécurité + CSP (défaut : `self` only, aucun tiers), gzip, cache.
```yaml
serveur_web_dorsal_sites:
- nom: alliance
server_name: "alliance-boreale.chezlepro.ca" # le FQDN que le plan expose
depot: "https://forge.{{ domaine_interne }}/alliance/site-alliance-boreale.git"
branche: main
sous_dossier: public
```
Et dans `applications.yml` : `groupe: serveur_web_dorsal, hote: <noeud>, port: 80,
expose: [<FQDN>]`. Sonde `sites-servis` : un GET par `server_name`.
## BD ## BD
Les apps à état **local** (SQLite) : `data_dir` persistant. Les apps à **BD réseau** Les apps à état **local** (SQLite) : `data_dir` persistant. Les apps à **BD réseau**
(PostgreSQL) : déclarer la base dans `bases-donnees.yml` et passer le DSN via `env` (PostgreSQL) : déclarer la base dans `bases-donnees.yml` et passer le DSN via `env`
@ -60,5 +41,4 @@ paquets de runtime (le patron systemd + nginx + git reste identique).
## Validation ## Validation
`ansible-lint` profil production + `--syntax-check`. Éprouvé sur web-frontal-01 `ansible-lint` profil production + `--syntax-check`. Éprouvé sur web-frontal-01
(monregistraire servi via l'edge, HTTPS step-ca, API fonctionnelle) — avant que le (monregistraire servi via l'edge, HTTPS step-ca, API fonctionnelle).
frontal ne devienne le reverse proxy public.

View file

@ -29,30 +29,3 @@ serveur_web_dorsal_apps: []
# `/api/...` est parfaitement saine sans rien servir a `/`. Ce qui reste exclu : les 5xx # `/api/...` est parfaitement saine sans rien servir a `/`. Ce qui reste exclu : les 5xx
# et l'absence de reponse, c'est-a-dire l'app coincee ou morte. # et l'absence de reponse, c'est-a-dire l'app coincee ou morte.
serveur_web_dorsal_sonde_codes: "HTTP/1.1 200,HTTP/1.1 302,HTTP/1.1 401,HTTP/1.1 403,HTTP/1.1 404" serveur_web_dorsal_sonde_codes: "HTTP/1.1 200,HTTP/1.1 302,HTTP/1.1 401,HTTP/1.1 403,HTTP/1.1 404"
# --- LES SITES STATIQUES (2026-09-29, repris du web frontal) -------------------------------
#
# LE DORSAL PORTE LES SITES ; LE FRONTAL NE SERT RIEN LUI-MEME. Le web frontal est le reverse
# proxy et le WAF devant les services web publics du locataire ; ce qui est servi — les
# applications comme les sites statiques — vit ici. Un site statique : 100 % natif, nginx,
# le contenu cloné d'un depot git souverain.
serveur_web_dorsal_sites_racine: "/srv/web"
# En-tetes de securite par defaut (sobriete + vie privee : tout local, pas de tiers).
serveur_web_dorsal_csp_defaut: >-
default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self';
object-src 'none'; base-uri 'self'; frame-ancestors 'self'
# Liste des sites statiques servis par ce noeud. Chaque entree :
# nom identifiant court (dossier + vhost)
# server_name FQDN servi — celui que le plan EXPOSE (applications.yml -> edge ou frontal)
# depot URL du depot git du site (souverain, ex. Forgejo)
# branche branche a servir (defaut: main)
# sous_dossier sous-dossier du depot a servir comme racine (ex: public)
# csp Content-Security-Policy specifique (optionnel)
serveur_web_dorsal_sites: []
# LE PORT DES VHOSTS LOCAUX DES SITES. En clair sur cette machine : c'est le frontal (ou
# l'edge, pour un nom interne) qui les recoit et les relaie. La sonde mesure ce qu'il ira
# chercher — si le frontal est vert et le site muet, la panne est ici.
serveur_web_dorsal_sites_port: 80

View file

@ -2,14 +2,12 @@
# Flux reseau du dorsal web (propriete du role). Voir docs/flux-conception.md. # Flux reseau du dorsal web (propriete du role). Voir docs/flux-conception.md.
# L'app ecoute en LOCALHOST (pas de flux inter-noeud) ; le nginx local la fronte. # L'app ecoute en LOCALHOST (pas de flux inter-noeud) ; le nginx local la fronte.
flux: flux:
# LE DORSAL EST RELAYE PAR DEUX PORTES (2026-09-29) : l'edge pour les noms internes
# (l'administration), le web frontal pour les noms publics (l'Internet, a travers le WAF).
- sens: ingress - sens: ingress
port: 80 port: 80
protocole: tcp protocole: tcp
pair: [edge, serveur_web_frontal] pair: edge
chiffrement: clair chiffrement: clair
raison: "Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics)." raison: "Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge)."
- sens: egress - sens: egress
port: 443 port: 443
protocole: tcp protocole: tcp

View file

@ -28,10 +28,3 @@ sondes:
jamais en « failed » — elle se coince : le processus vit, systemd la dit active, le jamais en « failed » — elle se coince : le processus vit, systemd la dit active, le
port est ouvert, et plus une requete n''aboutit. Le rapport d''unites en echec ne port est ouvert, et plus une requete n''aboutit. Le rapport d''unites en echec ne
verra jamais ca.' verra jamais ca.'
# SUR LE DORSAL DEPUIS LE 2026-09-29 : c'est lui qui porte les sites ; le frontal les relaie.
- nom: sites-servis
ttl: 5400
raison: 'Chacun des sites statiques declares se sert-il encore ? Le contenu vient
d''un depot git : une branche renommee, un sous-dossier deplace ou un clone vide
laissent nginx parfaitement vert avec un site perime, vide, ou en 404. Seul un GET
sur le `server_name` de chaque site le dit.'

View file

@ -107,49 +107,6 @@
label: "{{ item.nom }}" label: "{{ item.nom }}"
when: not ansible_check_mode when: not ansible_check_mode
# LES SITES STATIQUES (2026-09-29) — repris du web frontal, qui ne sert plus rien lui-meme.
- name: Creer la racine des sites statiques
ansible.builtin.file:
path: "{{ serveur_web_dorsal_sites_racine }}"
state: directory
owner: root
group: root
mode: "0755"
when: serveur_web_dorsal_sites | length > 0
- name: Cloner / mettre a jour le depot de chaque site (clone superficiel, contenu courant)
ansible.builtin.git:
repo: "{{ item.depot }}"
dest: "{{ serveur_web_dorsal_sites_racine }}/{{ item.nom }}"
version: "{{ item.branche | default('main') }}"
depth: 1
force: true
loop: "{{ serveur_web_dorsal_sites }}"
loop_control:
label: "{{ item.nom }}"
- name: Deployer le vhost de chaque site
ansible.builtin.template:
src: site.conf.j2
dest: "/etc/nginx/sites-available/site-{{ item.nom }}"
owner: root
group: root
mode: "0644"
loop: "{{ serveur_web_dorsal_sites }}"
loop_control:
label: "{{ item.nom }}"
notify: Recharger nginx serveur_web_dorsal
- name: Activer le vhost de chaque site
ansible.builtin.file:
src: "/etc/nginx/sites-available/site-{{ item.nom }}"
dest: "/etc/nginx/sites-enabled/site-{{ item.nom }}"
state: link
loop: "{{ serveur_web_dorsal_sites }}"
loop_control:
label: "{{ item.nom }}"
notify: Recharger nginx serveur_web_dorsal
# LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. `client_sante` la fait tourner et # LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. `client_sante` la fait tourner et
# pousse le resultat ; les greffons standard, eux, sont poses par le porteur. # pousse le resultat ; les greffons standard, eux, sont poses par le porteur.
# #
@ -170,14 +127,3 @@
owner: root owner: root
group: root group: root
mode: "0750" mode: "0750"
# LE CONTENU D'UN SITE N'EST PAS DANS CE ROLE : il arrive par git depuis une forge. Une
# branche renommee, un sous-dossier deplace ou un clone vide laissent nginx parfaitement
# vert. Seul un GET par `server_name` le dit.
- name: Deposer la sonde « sites-servis »
ansible.builtin.template:
src: sonde-sites-servis.sh.j2
dest: /usr/local/lib/setops/sondes/sites-servis.sh
owner: root
group: root
mode: "0750"

View file

@ -1,34 +1,37 @@
# serveur_web_frontal # serveur_web_frontal
**Reverse proxy et WAF devant les services web PUBLICS du locataire.** C'est la porte Hébergement de **sites web statiques** (nginx), derrière l'edge. Première brique de
publique : la frontière redirige l'adresse publique que le site attribue au locataire la **plateforme webapp** de Set-OPS : **100 % natif** — pas de conteneur, pas de
(80/443) vers lui ; il filtre et relaie. **Il ne sert rien lui-même** — les sites et les runtime, pas de base de données. Le contenu vient d'un **dépôt git souverain**
applications vivent sur le web **dorsal** (`serveur_web_dorsal`). (Forgejo). Codifié depuis le spike du 2026-07-05 (site Alliance Boréale).
L'**edge** (`serveur_nginx`) est l'autre porte : les services **internes** (`*.internal`),
pour la zone d'administration. Les deux ne se remplacent pas.
## Ce que le rôle fait ## Ce que le rôle fait
1. nginx + **ModSecurity v3** (`libnginx-mod-http-modsecurity`) + **OWASP CRS** Pour chaque site déclaré dans `serveur_web_frontal_sites` :
(`modsecurity-crs`), tous paquets Debian, tout libre ; 1. `git clone`/`pull` du dépôt → `/srv/web/<nom>` ;
2. compose le WAF (`/etc/nginx/setops-waf/main.conf`) : moteur, mode, CRS et ses 2. vhost nginx servant `<sous_dossier>` (ex. `public/`), avec en-têtes de sécurité
exclusions — l'inclusion livrée par Debian ne charge que le moteur ; + CSP (défaut : `self` only, aucun tiers, aucun pistage), gzip, cache, `try_files`.
3. un vhost par **exposition publique** : les FQDN que `applications.yml` expose dans un
domaine dont `plan/domaines.yml` désigne ce frontal (`edge: serveur_web_frontal`) —
la même dérivation que l'edge ;
4. un serveur par défaut qui **refuse tout nom inconnu** (`return 444`) ;
5. la sonde `frontal` : nginx actif et configuration valide, WAF chargé et CRS présent,
chaque exposition publique servie à travers lui.
## Le WAF Exposé via `applications.yml` (champ `expose`) → l'edge proxie en HTTPS (TLS step-ca).
`serveur_web_frontal_waf_mode: DetectionOnly` d'abord : le CRS journalise ce qu'il aurait Les SAN du cert edge et le plancher /etc/hosts sont **auto-dérivés du plan**.
bloqué (`/var/log/nginx/modsec_audit.log`) sans rien refuser. On lit, on ajuste les
exclusions pour les applications réelles, **puis** `On`. Éprouvé le 2026-09-29 (Debian 13,
nginx 1.26, CRS 3.3.7) : une injection SQL est journalisée (942100) en détection, refusée
(403) en blocage ; une requête saine passe dans les deux cas.
## TLS ## Déclaration (group_vars du groupe serveur_web_frontal)
En clair (80) tant que le certificat **Let's Encrypt** des noms publics n'est pas en place. ```yaml
serveur_web_frontal_sites:
- nom: alliance
server_name: "alliance-boreale.{{ domaine_interne }}"
depot: "https://forge.{{ domaine_interne }}/alliance/site-alliance-boreale.git"
branche: main
sous_dossier: public
# csp: "..." # optionnel, sinon serveur_web_frontal_csp_defaut
```
Et dans `applications.yml` : une app `groupe: serveur_web_frontal, hote: <noeud>, port: 80,
expose: [alliance-boreale.<domaine>]`.
## Portée
Statique seulement (le **frontal**). Les apps **dynamiques** (runtime + service
systemd + BD via resoudre_base) relèveront d'un rôle *dorsal* à venir — même éthos :
natif, systemd, zéro conteneur.
## Validation ## Validation
`ansible-lint` profil production + `--syntax-check` ; sonde `frontal`. `ansible-lint` profil production + `--syntax-check`. Éprouvé sur web-frontal-01
(site servi via l'edge, HTTPS step-ca vérifié, CSP en place).

View file

@ -1,40 +1,29 @@
--- ---
# ========================================================================= # =========================================================================
# serveur_web_frontal — REVERSE PROXY + WAF devant les services web PUBLICS du locataire. # serveur_web_frontal — hebergement de sites web STATIQUES (nginx), derriere l'edge.
# # Premiere brique de la « plateforme webapp » : 100% natif (pas de conteneur,
# L'EDGE ET LE FRONTAL SONT DEUX PORTES (2026-09-29). L'edge (`serveur_nginx`) expose les # pas de runtime, pas de BD). Le contenu vient d'un depot git souverain.
# services INTERNES (`*.internal`) a la zone d'administration. Le frontal expose ce que le # Les apps DYNAMIQUES (service systemd + BD) viendront via un r_le dorsal.
# locataire publie sur l'Internet : la frontiere redirige l'adresse publique du locataire # Codifie depuis le spike du 2026-07-05 (site Alliance Boreale).
# (80/443) vers lui, il filtre (WAF) et relaie vers le service — le plus souvent le web
# DORSAL, qui porte les sites et les applications. Le frontal ne sert rien lui-meme.
#
# SES VHOSTS NE SE DECLARENT PAS ICI : ils derivent des expositions dont le domaine PUBLIC
# le designe (`plan/domaines.yml`, champ `edge: serveur_web_frontal`) — la meme derivation
# que l'edge (`expositions_des_applications`), filtree sur ce groupe.
# ========================================================================= # =========================================================================
serveur_web_frontal_paquets: serveur_web_frontal_racine_base: "/srv/web"
- nginx
- libnginx-mod-http-modsecurity # le connecteur nginx de ModSecurity v3
- modsecurity-crs # OWASP Core Rule Set
serveur_web_frontal_groupe: "serveur_web_frontal" # En-tetes de securite par defaut (sobriete + vie privee : tout local, pas de tiers).
serveur_web_frontal_publier_expositions: true serveur_web_frontal_csp_defaut: >-
default-src 'self'; img-src 'self' data:; style-src 'self'; script-src 'self';
object-src 'none'; base-uri 'self'; frame-ancestors 'self'
# --- LE WAF -------------------------------------------------------------------------- # Liste des sites statiques servis par ce noeud. Chaque entree :
# # nom identifiant court (dossier + vhost)
# `DetectionOnly` D'ABORD : le CRS journalise ce qu'il aurait bloque # server_name FQDN servi (doit etre expose via applications.yml -> edge)
# (`/var/log/nginx/modsec_audit.log`) sans rien refuser. On lit, on ajuste les exclusions # depot URL du depot git du site (souverain, ex. Forgejo)
# pour les applications reelles, PUIS on passe a `On`. Un WAF bloquant pose a l'aveugle # branche branche a servir (defaut: main)
# casse les formulaires legitimes, et on finit par le desactiver. # sous_dossier sous-dossier du depot a servir comme racine (ex: public)
# # csp Content-Security-Policy specifique (optionnel)
# Eprouve le 2026-09-29 sur le vrai systeme (Debian 13, nginx 1.26, CRS 3.3.7) : une serveur_web_frontal_sites: []
# injection SQL en requete rend 200 et est journalisee (942100, 949110) en detection ;
# 403 en blocage ; une requete saine passe dans les deux cas.
serveur_web_frontal_waf_mode: "DetectionOnly" # ou "On"
serveur_web_frontal_waf_dir: "/etc/nginx/setops-waf"
# Taille maximale d'une requete relayee (televersements). # LE PORT DES VHOSTS LOCAUX. Les sites statiques ecoutent en clair sur cette machine ;
serveur_web_frontal_taille_max_corps: "16m" # c'est l'edge qui termine le TLS devant. La sonde mesure donc ce que l'edge ira chercher,
# pas ce que le visiteur voit — et c'est le bon endroit : si l'edge est vert et le site
# Un amont declare sur un de ces ports se parle en TLS (comme a l'edge). # muet, la panne est ici.
serveur_web_frontal_ports_tls: [443, 8443] serveur_web_frontal_sonde_port: 80

View file

@ -1,26 +1,31 @@
--- ---
# Flux reseau du frontal web (propriete du role). Voir docs/flux-conception.md. # Flux reseau du frontal web (propriete du role). Voir docs/flux-conception.md.
# flux:
- sens: ingress
port: 80
protocole: tcp
pair: edge
chiffrement: clair
raison: "Contenu statique servi au navigateur via l'edge (TLS termine a l'edge)."
# LE WEB FRONTAL EST LA PORTE PUBLIQUE DU LOCATAIRE (2026-09-29). L'edge expose les services # LE WEB FRONTAL EST LA PORTE PUBLIQUE DU LOCATAIRE (2026-09-29). L'edge expose les services
# INTERNES a l'administration ; ce qui est publie sur l'Internet l'est ici, par l'adresse # INTERNES a l'administration ; ce qui est publie sur l'Internet l'est ici, par l'adresse
# publique que le site attribue au locataire (redirection a la frontiere). Il filtre (WAF) # publique que le site attribue au locataire (redirection a la frontiere). Le 443 attend son
# et relaie : il ne sert rien lui-meme, et ne recoit plus rien de l'edge. # TLS (Let's Encrypt) : la redirection est prete avant lui.
flux:
- sens: ingress - sens: ingress
port: 80 port: 80
protocole: tcp protocole: tcp
pair: externe pair: externe
chiffrement: clair chiffrement: clair
raison: "HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la." raison: "HTTP public — le premier contact d'un navigateur, et le defi HTTP d'ACME ; renvoie vers HTTPS."
- sens: ingress - sens: ingress
port: 443 port: 443
protocole: tcp protocole: tcp
pair: externe pair: externe
chiffrement: tls-requis chiffrement: tls-requis
raison: "HTTPS public — les sites et services que le locataire publie sur l'Internet (attend son certificat Let's Encrypt)." raison: "HTTPS public — les sites et services que le locataire publie sur l'Internet."
- sens: egress - sens: egress
port: 80 port: 443
protocole: tcp protocole: tcp
pair: serveur_web_dorsal pair: flotte
chiffrement: clair chiffrement: tls
raison: "Relais vers le web dorsal, qui porte les sites et les applications publiques." raison: "git clone/pull du depot du site (Forgejo souverain) au deploiement."

View file

@ -1,14 +1,33 @@
--- ---
# Supervision derivee du role. Voir docs/supervision-conception.md. # Supervision derivee du role. Voir docs/supervision-conception.md.
# #
# LE FRONTAL EST LA PORTE PUBLIQUE : s'il tombe, tout ce que le locataire publie tombe avec # CE QUE `serveur_nginx` NE PEUT PAS DIRE. L'edge a sa sonde, et elle repond « nginx sert
# lui, pendant que les services derriere restent verts. Et un frontal peut tres bien # encore ». Ici la question est autre : CHACUN des sites declares se sert-il vraiment ?
# relayer SANS FILTRER — module WAF non charge, regles absentes — en restant « en ligne ». #
# `sites-servis` a suivi les sites sur le dorsal le 2026-09-29. # LA PANNE PROPRE A CE ROLE EST SILENCIEUSE, et elle vient du fait que le contenu n'est
# pas dans le role : il arrive par `git clone`/`pull` depuis une forge. Trois facons de
# perdre un site sans que nginx bronche :
#
# 1. LE DEPOT A CHANGE DE BRANCHE OU DE NOM — le pull echoue, le contenu reste celui de
# la derniere fois qui a marche. Le site est en ligne, et vieux.
# 2. LE SOUS-DOSSIER SERVI A BOUGE (`public/` renomme en amont) — nginx rend 404 ou 403
# sur un repertoire qui existe mais ne contient plus d'index.
# 3. LE CLONE EST VIDE — un depot neuf, ou une reconstruction qui n'a rien ramene. Meme
# lecture que la forge du site le 2026-09-14 : tout repond, il n'y a rien dedans.
#
# Dans les trois cas l'edge est vert, le vhost existe, le port ecoute. Seul un GET sur le
# `server_name` du site dit la verite.
#
# UNE SEULE SONDE POUR TOUS LES SITES, parce qu'il n'y a qu'une CAUSE D'ACTION : aller
# voir cet hebergeur. Elle nomme celui qui manque — c'est ce dont on a besoin pour agir,
# et une sonde par site ferait N services a definir pour un seul geste.
#
# TTL de 5400 s pour un porteur qui passe aux 15 min : trois passages manques avant la
# peremption. Le silence alerte autant que l'echec.
sondes: sondes:
- nom: frontal - nom: sites-servis
ttl: 5400 ttl: 5400
raison: 'Le reverse proxy public tient-il, son WAF (ModSecurity + OWASP CRS) est-il raison: 'Chacun des sites statiques declares se sert-il encore ? Le contenu vient
vraiment dans le chemin, et chaque exposition publique repond-elle a travers lui ? d''un depot git : une branche renommee, un sous-dossier deplace ou un clone vide
Un frontal qui relaie sans filtrer reste vert ; un frontal tombe rend tout le public laissent nginx parfaitement vert avec un site perime, vide, ou en 404. Seul un GET
injoignable pendant que les services derriere restent sains.' sur le `server_name` de chaque site le dit.'

View file

@ -1,103 +1,66 @@
--- ---
# serveur_web_frontal — reverse proxy + WAF des services web PUBLICS. Voir defaults/main.yml. - name: Installer nginx + git (git requis pour cloner les depots des sites)
- name: Installer nginx, ModSecurity et l'OWASP CRS
ansible.builtin.apt: ansible.builtin.apt:
name: "{{ serveur_web_frontal_paquets }}" name:
- nginx
- git
state: present state: present
update_cache: true update_cache: true
cache_valid_time: 3600 cache_valid_time: 3600
- name: Retirer le vhost nginx par defaut de Debian - name: Retirer le vhost nginx par defaut
ansible.builtin.file: ansible.builtin.file:
path: /etc/nginx/sites-enabled/default path: /etc/nginx/sites-enabled/default
state: absent state: absent
notify: Recharger nginx serveur_web_frontal notify: Recharger nginx serveur_web_frontal
- name: Deployer le durcissement de base nginx - name: Creer la racine des sites
ansible.builtin.template:
src: durcissement.conf.j2
dest: /etc/nginx/conf.d/99-setops-frontal.conf
owner: root
group: root
mode: "0644"
notify: Recharger nginx serveur_web_frontal
- name: Creer le repertoire du WAF
ansible.builtin.file: ansible.builtin.file:
path: "{{ serveur_web_frontal_waf_dir }}" path: "{{ serveur_web_frontal_racine_base }}"
state: directory state: directory
owner: root owner: root
group: root group: root
mode: "0755" mode: "0755"
- name: Deployer la composition du WAF (ModSecurity + OWASP CRS) - name: Cloner / mettre a jour le depot de chaque site (clone superficiel, contenu courant)
ansible.builtin.template: ansible.builtin.git:
src: waf.conf.j2 repo: "{{ item.depot }}"
dest: "{{ serveur_web_frontal_waf_dir }}/main.conf" dest: "{{ serveur_web_frontal_racine_base }}/{{ item.nom }}"
owner: root version: "{{ item.branche | default('main') }}"
group: root depth: 1
mode: "0644" force: true
notify: Recharger nginx serveur_web_frontal loop: "{{ serveur_web_frontal_sites }}"
# LES REGISTRES DU PLAN, COMME A L'EDGE : on releve d'abord ce qui existe, puis on charge.
# `domaines.yml` peut manquer (un plan sans domaine public) : les expositions sont alors vides.
- name: Quels registres du plan existent vraiment ?
ansible.builtin.stat:
path: "{{ item }}"
loop:
- "{{ setops_plan_dir }}/applications.yml"
- "{{ setops_plan_dir }}/domaines.yml"
delegate_to: localhost
become: false
register: serveur_web_frontal_registres
when: serveur_web_frontal_publier_expositions | bool
- name: Charger les registres applications et domaines
ansible.builtin.include_vars:
file: "{{ item.item }}"
loop: "{{ serveur_web_frontal_registres.results | default([]) }}"
loop_control: loop_control:
label: "{{ item.item | basename }}" label: "{{ item.nom }}"
when:
- serveur_web_frontal_publier_expositions | bool
- item.stat.exists | default(false)
- name: Lister les expositions publiques destinees a ce frontal - name: Deployer le vhost de chaque site
ansible.builtin.set_fact:
serveur_web_frontal_expositions: >-
{{ {'applications': applications | default({})}
| expositions_des_applications({'domaines_publics': domaines_publics | default({})}, serveur_web_frontal_groupe) }}
when: serveur_web_frontal_publier_expositions | bool
- name: Deployer les vhosts du frontal (derives des expositions publiques)
ansible.builtin.template: ansible.builtin.template:
src: frontal.conf.j2 src: site.conf.j2
dest: /etc/nginx/sites-available/frontal.conf dest: "/etc/nginx/sites-available/{{ item.nom }}"
owner: root owner: root
group: root group: root
mode: "0644" mode: "0644"
loop: "{{ serveur_web_frontal_sites }}"
loop_control:
label: "{{ item.nom }}"
notify: Recharger nginx serveur_web_frontal notify: Recharger nginx serveur_web_frontal
- name: Activer les vhosts du frontal - name: Activer le vhost de chaque site
ansible.builtin.file: ansible.builtin.file:
src: /etc/nginx/sites-available/frontal.conf src: "/etc/nginx/sites-available/{{ item.nom }}"
dest: /etc/nginx/sites-enabled/frontal.conf dest: "/etc/nginx/sites-enabled/{{ item.nom }}"
state: link state: link
loop: "{{ serveur_web_frontal_sites }}"
loop_control:
label: "{{ item.nom }}"
notify: Recharger nginx serveur_web_frontal notify: Recharger nginx serveur_web_frontal
- name: Valider la configuration nginx avant tout rechargement # LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. `client_sante` la fait tourner et
ansible.builtin.command: nginx -t # pousse le resultat ; les greffons standard, eux, sont poses par le porteur.
changed_when: false #
when: not ansible_check_mode # LE CONTENU DE CE ROLE N'EST PAS DANS CE ROLE : il arrive par git depuis une forge. Une
# branche renommee, un sous-dossier deplace ou un clone vide laissent nginx parfaitement
- name: Activer et demarrer nginx # vert. Seul un GET par `server_name` le dit.
ansible.builtin.systemd:
name: nginx
enabled: true
state: started
when: not ansible_check_mode
# LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE.
- name: Assurer le repertoire des sondes de supervision - name: Assurer le repertoire des sondes de supervision
ansible.builtin.file: ansible.builtin.file:
path: /usr/local/lib/setops/sondes path: /usr/local/lib/setops/sondes
@ -106,10 +69,10 @@
group: root group: root
mode: "0755" mode: "0755"
- name: Deposer la sonde « frontal » - name: Deposer la sonde « sites-servis »
ansible.builtin.template: ansible.builtin.template:
src: sonde-frontal.sh.j2 src: sonde-sites-servis.sh.j2
dest: /usr/local/lib/setops/sondes/frontal.sh dest: /usr/local/lib/setops/sondes/sites-servis.sh
owner: root owner: root
group: root group: root
mode: "0750" mode: "0750"

View file

@ -1,3 +0,0 @@
# Gere par Set-OPS (role serveur_web_frontal). Ne pas editer a la main.
server_tokens off;
client_max_body_size {{ serveur_web_frontal_taille_max_corps }};

View file

@ -1,41 +0,0 @@
# Gere par Set-OPS (role serveur_web_frontal). Ne pas editer a la main.
#
# LE SERVEUR PAR DEFAUT REFUSE TOUT NOM INCONNU (meme regle que l'edge) : sans lui, nginx
# confie un nom inconnu au premier vhost, et un balayage de l'adresse publique recevrait un
# service qu'il n'a pas nomme.
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
{% for expo in serveur_web_frontal_expositions | default([]) %}
{% if expo.hote in hostvars and hostvars[expo.hote].ansible_host is defined and expo.port %}
{% set tls = (expo.port | int) in serveur_web_frontal_ports_tls %}
# {{ expo.fqdn }} -> {{ expo.hote }}:{{ expo.port }} ({{ expo.application }})
# En clair en attendant son certificat Let's Encrypt.
server {
listen 80;
listen [::]:80;
server_name {{ expo.fqdn }};
modsecurity on;
modsecurity_rules_file {{ serveur_web_frontal_waf_dir }}/main.conf;
location / {
proxy_pass {{ 'https' if tls else 'http' }}://{{ hostvars[expo.hote].ansible_host }}:{{ expo.port }};
{% if tls %}
proxy_ssl_verify on;
proxy_ssl_trusted_certificate /etc/ssl/certs/ca-certificates.crt;
proxy_ssl_name {{ expo.hote }}.{{ domaine_interne }};
proxy_ssl_server_name on;
{% endif %}
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
{% endif %}
{% endfor %}

View file

@ -1,15 +1,15 @@
# Site statique {{ item.nom }} — genere par Set-OPS (role serveur_web_dorsal). Ne pas editer. # Site statique {{ item.nom }} — genere par Set-OPS (role serveur_web_frontal). Ne pas editer.
# Sert derriere l'edge (TLS termine a l'edge). Racine = depot git / sous-dossier. # Sert derriere l'edge (TLS termine a l'edge). Racine = depot git / sous-dossier.
server { server {
listen 80; listen 80;
server_name {{ item.server_name }}; server_name {{ item.server_name }};
root {{ serveur_web_dorsal_sites_racine }}/{{ item.nom }}{{ ('/' ~ item.sous_dossier) if (item.sous_dossier | default('')) else '' }}; root {{ serveur_web_frontal_racine_base }}/{{ item.nom }}{{ ('/' ~ item.sous_dossier) if (item.sous_dossier | default('')) else '' }};
index index.html; index index.html;
add_header X-Content-Type-Options "nosniff" always; add_header X-Content-Type-Options "nosniff" always;
add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Frame-Options "SAMEORIGIN" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "{{ (item.csp | default(serveur_web_dorsal_csp_defaut)) | trim }}" always; add_header Content-Security-Policy "{{ (item.csp | default(serveur_web_frontal_csp_defaut)) | trim }}" always;
gzip on; gzip on;
gzip_types text/css application/javascript text/html image/svg+xml; gzip_types text/css application/javascript text/html image/svg+xml;

View file

@ -1,40 +0,0 @@
#!/bin/bash
# GENERE par Set-OPS (role serveur_web_frontal). Ne pas editer a la main.
#
# SONDE « frontal » — le reverse proxy public tient, son WAF est charge, et chaque
# exposition publique repond a travers lui.
#
# Contrat : docs/supervision-conception.md (API des greffons Nagios) — une ligne, 0/1/2.
# Mise en defaut PAR PARAMETRE : `serveur_web_frontal_waf_mode` (une valeur que le moteur
# refuse fait echouer `nginx -t`).
set -uo pipefail
MODE='{{ serveur_web_frontal_waf_mode }}'
EXPOSITIONS="{{ serveur_web_frontal_expositions | default([]) | map(attribute='fqdn') | unique | sort | join(' ') }}"
systemctl is-active --quiet nginx || { echo "Le frontal (nginx) n'est pas actif : les services publics sont injoignables."; exit 2; }
nginx -t >/dev/null 2>&1 \
|| { echo "Le frontal sert, mais sa configuration sur disque est INVALIDE : le prochain demarrage echouera."; exit 2; }
# LE WAF EST-IL VRAIMENT DANS LE CHEMIN ? Un module non charge, ou des regles absentes,
# laissent passer tout le trafic sans rien dire : le frontal reste « vert ».
conf=$(nginx -T 2>/dev/null)
grep -q "ngx_http_modsecurity_module" <<<"${conf}" \
|| { echo "Le module ModSecurity n'est pas charge : le frontal relaie SANS filtrer."; exit 2; }
grep -q "modsecurity-crs/rules" '{{ serveur_web_frontal_waf_dir }}/main.conf' 2>/dev/null \
|| { echo "Les regles OWASP CRS ne sont pas chargees : le WAF n'a rien a appliquer."; exit 2; }
servies=0; en_defaut=()
for n in ${EXPOSITIONS}; do
code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 8 -H "Host: ${n}" http://127.0.0.1/ 2>/dev/null)
if [[ "${code}" =~ ^[234][0-9][0-9]$ ]]; then servies=$((servies + 1)); else en_defaut+=("${n}=${code:-000}"); fi
done
if [[ -n "${en_defaut[*]:-}" ]]; then
echo "Le frontal repond, mais des expositions publiques ne sont PAS servies : ${en_defaut[*]} (000 = silence, 5xx = amont en defaut). WAF ${MODE}."
exit 2
fi
if [[ -z "${EXPOSITIONS}" ]]; then
echo "Frontal pret (WAF CRS en ${MODE}) : aucune exposition publique declaree pour l'instant."
exit 0
fi
echo "Frontal : ${servies} exposition(s) publique(s) servie(s), WAF CRS en ${MODE}."

View file

@ -4,7 +4,7 @@
# la sequence `{#`, que Jinja lirait comme un debut de commentaire. Le shell en a # la sequence `{#`, que Jinja lirait comme un debut de commentaire. Le shell en a
# besoin ; c'est donc Jinja qui cede. Meme geste que `client_sante/setops-sante.sh.j2` # besoin ; c'est donc Jinja qui cede. Meme geste que `client_sante/setops-sante.sh.j2`
# et `serveur_nginx/sonde-edge.sh.j2`.) # et `serveur_nginx/sonde-edge.sh.j2`.)
# GENERE par Set-OPS (role serveur_web_dorsal). Ne pas editer a la main. # GENERE par Set-OPS (role serveur_web_frontal). Ne pas editer a la main.
# #
# SONDE « sites-servis » — chacun des sites declares se sert-il encore ? # SONDE « sites-servis » — chacun des sites declares se sert-il encore ?
# #
@ -18,9 +18,9 @@
# mesurerait le serveur par defaut, c'est-a-dire n'importe quoi d'autre. # mesurerait le serveur par defaut, c'est-a-dire n'importe quoi d'autre.
set -uo pipefail set -uo pipefail
PORT={{ serveur_web_dorsal_sites_port }} PORT={{ serveur_web_frontal_sonde_port }}
noms=({% for s in serveur_web_dorsal_sites | default([]) %}'{{ s.server_name }}' {% endfor %}) noms=({% for s in serveur_web_frontal_sites | default([]) %}'{{ s.server_name }}' {% endfor %})
etiquettes=({% for s in serveur_web_dorsal_sites | default([]) %}'{{ s.nom }}' {% endfor %}) etiquettes=({% for s in serveur_web_frontal_sites | default([]) %}'{{ s.nom }}' {% endfor %})
if (( ${#noms[@]} == 0 )); then if (( ${#noms[@]} == 0 )); then
# PAS UN VERT MUET. Un hebergeur sans site declare est un etat legitime — le dire vaut # PAS UN VERT MUET. Un hebergeur sans site declare est un etat legitime — le dire vaut

View file

@ -1,12 +0,0 @@
# Gere par Set-OPS (role serveur_web_frontal). Ne pas editer a la main.
#
# ModSecurity v3 + OWASP CRS. L'inclusion livree par Debian (`modsecurity_includes.conf`)
# ne charge QUE le moteur, pas les regles : on compose ici, dans l'ordre que le CRS exige
# — reglages du moteur, puis son mode, puis la configuration du CRS, ses exclusions
# d'avant, ses regles, ses exclusions d'apres.
Include /etc/nginx/modsecurity.conf
SecRuleEngine {{ serveur_web_frontal_waf_mode }}
Include /etc/modsecurity/crs/crs-setup.conf
Include /etc/modsecurity/crs/REQUEST-900-EXCLUSION-RULES-BEFORE-CRS.conf
Include /usr/share/modsecurity-crs/rules/*.conf
Include /etc/modsecurity/crs/RESPONSE-999-EXCLUSION-RULES-AFTER-CRS.conf

View file

@ -9,11 +9,8 @@
graph LR graph LR
R["<b>postfix</b>"] R["<b>postfix</b>"]
E0["externe"] -->|"25 · starttls"| R E0["externe"] -->|"25 · starttls"| R
E1["externe"] -->|"465 · tls-requis"| R E1["flotte"] -->|"587 · starttls"| R
E2["externe"] -->|"587 · starttls"| R E2["smtp"] -->|"25 · starttls"| R
E3["flotte"] -->|"465 · tls-requis"| R
E4["flotte"] -->|"587 · starttls"| R
E5["smtp"] -->|"25 · starttls"| R
R -.->|"sonde « file-courriel »"| ICINGA[["Icinga"]] R -.->|"sonde « file-courriel »"| ICINGA[["Icinga"]]
``` ```
@ -22,8 +19,7 @@ graph LR
| port | depuis | chiffrement | pourquoi | | port | depuis | chiffrement | pourquoi |
|---|---|---|---| |---|---|---|---|
| `25` | `externe`, `client_smtp` | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). | | `25` | `externe`, `client_smtp` | starttls | SMTP entrant : courrier externe (MX) et notifications internes (client_smtp). |
| `587` | `flotte`, `externe` | starttls | Soumission authentifiée (STARTTLS obligatoire) : agents internes et clients de courriel des utilisateurs. | | `587` | `flotte` | starttls | Soumission authentifiée (submission) pour les agents internes qui envoient du courrier. |
| `465` | `flotte`, `externe` | tls-requis | Soumission authentifiée en TLS direct (submissions, RFC 8314) : clients de courriel des utilisateurs. |
## Ce qu'il rend à la supervision ## Ce qu'il rend à la supervision

View file

@ -9,23 +9,20 @@
graph LR graph LR
R["<b>web_dorsal</b>"] R["<b>web_dorsal</b>"]
E0["edge"] -->|"80 · clair"| R E0["edge"] -->|"80 · clair"| R
E1["web_frontal"] -->|"80 · clair"| R
R -.->|"sonde « apps-servies »"| ICINGA[["Icinga"]] R -.->|"sonde « apps-servies »"| ICINGA[["Icinga"]]
R -.->|"sonde « sites-servis »"| ICINGA[["Icinga"]]
``` ```
## Qui lui parle ## Qui lui parle
| port | depuis | chiffrement | pourquoi | | port | depuis | chiffrement | pourquoi |
|---|---|---|---| |---|---|---|---|
| `80` | `edge`, `serveur_web_frontal` | clair | Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics). | | `80` | `edge` | clair | Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge). |
## Ce qu'il rend à la supervision ## Ce qu'il rend à la supervision
| sonde | TTL | ce qu'elle voit | | sonde | TTL | ce qu'elle voit |
|---|---|---| |---|---|---|
| `apps-servies` | 5400 s | Chacune des webapps declarees repond-elle encore ? Une app ne tombe presque jamais en « failed » — elle se coince : le processus vit, systemd la dit active, le port est ouvert, et plus une requete n'aboutit. Le rapport d'unites en echec ne verra jamais ca. | | `apps-servies` | 5400 s | Chacune des webapps declarees repond-elle encore ? Une app ne tombe presque jamais en « failed » — elle se coince : le processus vit, systemd la dit active, le port est ouvert, et plus une requete n'aboutit. Le rapport d'unites en echec ne verra jamais ca. |
| `sites-servis` | 5400 s | Chacun des sites statiques declares se sert-il encore ? Le contenu vient d'un depot git : une branche renommee, un sous-dossier deplace ou un clone vide laissent nginx parfaitement vert avec un site perime, vide, ou en 404. Seul un GET sur le `server_name` de chaque site le dit. |
## Ce qu'il expose en séries ## Ce qu'il expose en séries

View file

@ -8,23 +8,21 @@
```mermaid ```mermaid
graph LR graph LR
R["<b>web_frontal</b>"] R["<b>web_frontal</b>"]
E0["externe"] -->|"443 · tls-requis"| R E0["edge"] -->|"80 · clair"| R
E1["externe"] -->|"80 · clair"| R R -.->|"sonde « sites-servis »"| ICINGA[["Icinga"]]
R -.->|"sonde « frontal »"| ICINGA[["Icinga"]]
``` ```
## Qui lui parle ## Qui lui parle
| port | depuis | chiffrement | pourquoi | | port | depuis | chiffrement | pourquoi |
|---|---|---|---| |---|---|---|---|
| `80` | `externe` | clair | HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la. | | `80` | `edge` | clair | Contenu statique servi au navigateur via l'edge (TLS termine a l'edge). |
| `443` | `externe` | tls-requis | HTTPS public — les sites et services que le locataire publie sur l'Internet (attend son certificat Let's Encrypt). |
## Ce qu'il rend à la supervision ## Ce qu'il rend à la supervision
| sonde | TTL | ce qu'elle voit | | sonde | TTL | ce qu'elle voit |
|---|---|---| |---|---|---|
| `frontal` | 5400 s | Le reverse proxy public tient-il, son WAF (ModSecurity + OWASP CRS) est-il vraiment dans le chemin, et chaque exposition publique repond-elle a travers lui ? Un frontal qui relaie sans filtrer reste vert ; un frontal tombe rend tout le public injoignable pendant que les services derriere restent sains. | | `sites-servis` | 5400 s | Chacun des sites statiques declares se sert-il encore ? Le contenu vient d'un depot git : une branche renommee, un sous-dossier deplace ou un clone vide laissent nginx parfaitement vert avec un site perime, vide, ou en 404. Seul un GET sur le `server_name` de chaque site le dit. |
## Ce qu'il expose en séries ## Ce qu'il expose en séries

View file

@ -61,7 +61,7 @@ Chaque fiche est construite depuis ce que le rôle **déclare lui-même** — `m
| [`serveur_ops`](Rôle-serveur_ops) | 1 | 2 | — | | [`serveur_ops`](Rôle-serveur_ops) | 1 | 2 | — |
| [`serveur_ops_site`](Rôle-serveur_ops_site) | — | 3 | — | | [`serveur_ops_site`](Rôle-serveur_ops_site) | — | 3 | — |
| [`serveur_ops_tenant`](Rôle-serveur_ops_tenant) | 1 | 1 | — | | [`serveur_ops_tenant`](Rôle-serveur_ops_tenant) | 1 | 1 | — |
| [`serveur_postfix`](Rôle-serveur_postfix) | 3 | 1 | — | | [`serveur_postfix`](Rôle-serveur_postfix) | 2 | 1 | — |
| [`serveur_postgresql`](Rôle-serveur_postgresql) | 2 | 1 | oui | | [`serveur_postgresql`](Rôle-serveur_postgresql) | 2 | 1 | oui |
| [`serveur_powerdns`](Rôle-serveur_powerdns) | 4 | 1 | — | | [`serveur_powerdns`](Rôle-serveur_powerdns) | 4 | 1 | — |
| [`serveur_prometheus`](Rôle-serveur_prometheus) | 1 | 1 | — | | [`serveur_prometheus`](Rôle-serveur_prometheus) | 1 | 1 | — |
@ -70,8 +70,8 @@ Chaque fiche est construite depuis ce que le rôle **déclare lui-même** — `m
| [`serveur_resolveur_site`](Rôle-serveur_resolveur_site) | 2 | 1 | — | | [`serveur_resolveur_site`](Rôle-serveur_resolveur_site) | 2 | 1 | — |
| [`serveur_rspamd`](Rôle-serveur_rspamd) | 2 | 1 | — | | [`serveur_rspamd`](Rôle-serveur_rspamd) | 2 | 1 | — |
| [`serveur_step_ca`](Rôle-serveur_step_ca) | 1 | 1 | — | | [`serveur_step_ca`](Rôle-serveur_step_ca) | 1 | 1 | — |
| [`serveur_web_dorsal`](Rôle-serveur_web_dorsal) | 1 | 2 | — | | [`serveur_web_dorsal`](Rôle-serveur_web_dorsal) | 1 | 1 | — |
| [`serveur_web_frontal`](Rôle-serveur_web_frontal) | 2 | 1 | — | | [`serveur_web_frontal`](Rôle-serveur_web_frontal) | 1 | 1 | — |
| [`ssh_baseline`](Rôle-ssh_baseline) | — | — | — | | [`ssh_baseline`](Rôle-ssh_baseline) | — | — | — |
| [`ssh_hardening`](Rôle-ssh_hardening) | — | — | — | | [`ssh_hardening`](Rôle-ssh_hardening) | — | — | — |
| [`sudo_ansible`](Rôle-sudo_ansible) | — | — | — | | [`sudo_ansible`](Rôle-sudo_ansible) | — | — | — |