web frontal = reverse proxy + WAF ; le dorsal porte les sites
Le frontal derive ses vhosts des expositions publiques (meme derivation que l edge), filtre par ModSecurity v3 + OWASP CRS (DetectionOnly d abord, eprouve), refuse les noms inconnus, et ne sert plus rien lui-meme. Les sites statiques passent au dorsal, relaye par l edge et par le frontal. Sondes : frontal (nouvelle), sites-servis suit le dorsal. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2903d75831
commit
3fe7e3cea3
21 changed files with 401 additions and 166 deletions
|
|
@ -122,12 +122,11 @@
|
||||||
| `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 | clair | Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge). |
|
| `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` | 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 | edge | clair | Contenu statique servi au navigateur via l'edge (TLS termine a l'edge). |
|
| `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 | externe | clair | HTTP public — le premier contact d'un navigateur, et le defi HTTP d'ACME ; renvoie vers HTTPS. |
|
| `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 | 443 | tcp | externe | tls-requis | HTTPS public — les sites et services que le locataire publie sur l'Internet. |
|
| `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` | egress | 443 | tcp | flotte | tls | git clone/pull du depot du site (Forgejo souverain) au deploiement. |
|
|
||||||
|
|
||||||
## Synthèse chiffrement
|
## Synthèse chiffrement
|
||||||
|
|
||||||
|
|
@ -135,6 +134,6 @@
|
||||||
- **n-a** : 8 flux
|
- **n-a** : 8 flux
|
||||||
- **ssh** : 8 flux
|
- **ssh** : 8 flux
|
||||||
- **starttls** : 6 flux
|
- **starttls** : 6 flux
|
||||||
- **tls** : 11 flux
|
- **tls** : 10 flux
|
||||||
- **tls-cible** : 2 flux
|
- **tls-cible** : 2 flux
|
||||||
- **tls-requis** : 36 flux
|
- **tls-requis** : 36 flux
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
# serveur_web_dorsal
|
# serveur_web_dorsal
|
||||||
|
|
||||||
Hébergement de **webapps dynamiques** (runtime + service systemd), derrière l'edge.
|
**Le serveur qui porte les sites et les applications web du locataire** : webapps
|
||||||
Deuxième brique de la **plateforme webapp** de Set-OPS : **100 % natif** — venv/paquets
|
dynamiques (runtime + service systemd) et, depuis le 2026-09-29, sites **statiques**
|
||||||
+ systemd + nginx, **zéro conteneur**. Codifié depuis le spike du 2026-07-05
|
(repris du web frontal). **100 % natif** — venv/paquets + systemd + nginx, **zéro
|
||||||
(monregistraire : FastAPI + uvicorn + SQLite).
|
conteneur**. Codifié depuis le spike du 2026-07-05 (monregistraire : FastAPI + uvicorn +
|
||||||
|
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` :
|
||||||
|
|
@ -30,6 +34,21 @@ 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`
|
||||||
|
|
@ -41,4 +60,5 @@ 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).
|
(monregistraire servi via l'edge, HTTPS step-ca, API fonctionnelle) — avant que le
|
||||||
|
frontal ne devienne le reverse proxy public.
|
||||||
|
|
|
||||||
|
|
@ -29,3 +29,30 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,14 @@
|
||||||
# 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
|
pair: [edge, serveur_web_frontal]
|
||||||
chiffrement: clair
|
chiffrement: clair
|
||||||
raison: "Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge)."
|
raison: "Front nginx local des webapps et des sites statiques, relaye par l'edge (noms internes) et par le web frontal (noms publics)."
|
||||||
- sens: egress
|
- sens: egress
|
||||||
port: 443
|
port: 443
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
|
|
|
||||||
|
|
@ -28,3 +28,10 @@ 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.'
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,49 @@
|
||||||
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.
|
||||||
#
|
#
|
||||||
|
|
@ -127,3 +170,14 @@
|
||||||
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"
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
# Site statique {{ item.nom }} — genere par Set-OPS (role serveur_web_frontal). Ne pas editer.
|
# Site statique {{ item.nom }} — genere par Set-OPS (role serveur_web_dorsal). 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_frontal_racine_base }}/{{ item.nom }}{{ ('/' ~ item.sous_dossier) if (item.sous_dossier | default('')) else '' }};
|
root {{ serveur_web_dorsal_sites_racine }}/{{ 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_frontal_csp_defaut)) | trim }}" always;
|
add_header Content-Security-Policy "{{ (item.csp | default(serveur_web_dorsal_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;
|
||||||
|
|
@ -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_frontal). Ne pas editer a la main.
|
# GENERE par Set-OPS (role serveur_web_dorsal). 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_frontal_sonde_port }}
|
PORT={{ serveur_web_dorsal_sites_port }}
|
||||||
noms=({% for s in serveur_web_frontal_sites | default([]) %}'{{ s.server_name }}' {% endfor %})
|
noms=({% for s in serveur_web_dorsal_sites | default([]) %}'{{ s.server_name }}' {% endfor %})
|
||||||
etiquettes=({% for s in serveur_web_frontal_sites | default([]) %}'{{ s.nom }}' {% endfor %})
|
etiquettes=({% for s in serveur_web_dorsal_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
|
||||||
|
|
@ -1,37 +1,34 @@
|
||||||
# serveur_web_frontal
|
# serveur_web_frontal
|
||||||
|
|
||||||
Hébergement de **sites web statiques** (nginx), derrière l'edge. Première brique de
|
**Reverse proxy et WAF devant les services web PUBLICS du locataire.** C'est la porte
|
||||||
la **plateforme webapp** de Set-OPS : **100 % natif** — pas de conteneur, pas de
|
publique : la frontière redirige l'adresse publique que le site attribue au locataire
|
||||||
runtime, pas de base de données. Le contenu vient d'un **dépôt git souverain**
|
(80/443) vers lui ; il filtre et relaie. **Il ne sert rien lui-même** — les sites et les
|
||||||
(Forgejo). Codifié depuis le spike du 2026-07-05 (site Alliance Boréale).
|
applications vivent sur le web **dorsal** (`serveur_web_dorsal`).
|
||||||
|
|
||||||
|
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
|
||||||
Pour chaque site déclaré dans `serveur_web_frontal_sites` :
|
1. nginx + **ModSecurity v3** (`libnginx-mod-http-modsecurity`) + **OWASP CRS**
|
||||||
1. `git clone`/`pull` du dépôt → `/srv/web/<nom>` ;
|
(`modsecurity-crs`), tous paquets Debian, tout libre ;
|
||||||
2. vhost nginx servant `<sous_dossier>` (ex. `public/`), avec en-têtes de sécurité
|
2. compose le WAF (`/etc/nginx/setops-waf/main.conf`) : moteur, mode, CRS et ses
|
||||||
+ CSP (défaut : `self` only, aucun tiers, aucun pistage), gzip, cache, `try_files`.
|
exclusions — l'inclusion livrée par Debian ne charge que le moteur ;
|
||||||
|
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.
|
||||||
|
|
||||||
Exposé via `applications.yml` (champ `expose`) → l'edge proxie en HTTPS (TLS step-ca).
|
## Le WAF
|
||||||
Les SAN du cert edge et le plancher /etc/hosts sont **auto-dérivés du plan**.
|
`serveur_web_frontal_waf_mode: DetectionOnly` d'abord : le CRS journalise ce qu'il aurait
|
||||||
|
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.
|
||||||
|
|
||||||
## Déclaration (group_vars du groupe serveur_web_frontal)
|
## TLS
|
||||||
```yaml
|
En clair (80) tant que le certificat **Let's Encrypt** des noms publics n'est pas en place.
|
||||||
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`. Éprouvé sur web-frontal-01
|
`ansible-lint` profil production + `--syntax-check` ; sonde `frontal`.
|
||||||
(site servi via l'edge, HTTPS step-ca vérifié, CSP en place).
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,40 @@
|
||||||
---
|
---
|
||||||
# =========================================================================
|
# =========================================================================
|
||||||
# serveur_web_frontal — hebergement de sites web STATIQUES (nginx), derriere l'edge.
|
# serveur_web_frontal — REVERSE PROXY + WAF devant les services web PUBLICS du locataire.
|
||||||
# Premiere brique de la « plateforme webapp » : 100% natif (pas de conteneur,
|
#
|
||||||
# pas de runtime, pas de BD). Le contenu vient d'un depot git souverain.
|
# L'EDGE ET LE FRONTAL SONT DEUX PORTES (2026-09-29). L'edge (`serveur_nginx`) expose les
|
||||||
# Les apps DYNAMIQUES (service systemd + BD) viendront via un r_le dorsal.
|
# services INTERNES (`*.internal`) a la zone d'administration. Le frontal expose ce que le
|
||||||
# Codifie depuis le spike du 2026-07-05 (site Alliance Boreale).
|
# locataire publie sur l'Internet : la frontiere redirige l'adresse publique du locataire
|
||||||
|
# (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_racine_base: "/srv/web"
|
serveur_web_frontal_paquets:
|
||||||
|
- nginx
|
||||||
|
- libnginx-mod-http-modsecurity # le connecteur nginx de ModSecurity v3
|
||||||
|
- modsecurity-crs # OWASP Core Rule Set
|
||||||
|
|
||||||
# En-tetes de securite par defaut (sobriete + vie privee : tout local, pas de tiers).
|
serveur_web_frontal_groupe: "serveur_web_frontal"
|
||||||
serveur_web_frontal_csp_defaut: >-
|
serveur_web_frontal_publier_expositions: true
|
||||||
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 :
|
# --- LE WAF --------------------------------------------------------------------------
|
||||||
# nom identifiant court (dossier + vhost)
|
#
|
||||||
# server_name FQDN servi (doit etre expose via applications.yml -> edge)
|
# `DetectionOnly` D'ABORD : le CRS journalise ce qu'il aurait bloque
|
||||||
# depot URL du depot git du site (souverain, ex. Forgejo)
|
# (`/var/log/nginx/modsec_audit.log`) sans rien refuser. On lit, on ajuste les exclusions
|
||||||
# branche branche a servir (defaut: main)
|
# pour les applications reelles, PUIS on passe a `On`. Un WAF bloquant pose a l'aveugle
|
||||||
# sous_dossier sous-dossier du depot a servir comme racine (ex: public)
|
# casse les formulaires legitimes, et on finit par le desactiver.
|
||||||
# csp Content-Security-Policy specifique (optionnel)
|
#
|
||||||
serveur_web_frontal_sites: []
|
# Eprouve le 2026-09-29 sur le vrai systeme (Debian 13, nginx 1.26, CRS 3.3.7) : une
|
||||||
|
# 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"
|
||||||
|
|
||||||
# LE PORT DES VHOSTS LOCAUX. Les sites statiques ecoutent en clair sur cette machine ;
|
# Taille maximale d'une requete relayee (televersements).
|
||||||
# c'est l'edge qui termine le TLS devant. La sonde mesure donc ce que l'edge ira chercher,
|
serveur_web_frontal_taille_max_corps: "16m"
|
||||||
# pas ce que le visiteur voit — et c'est le bon endroit : si l'edge est vert et le site
|
|
||||||
# muet, la panne est ici.
|
# Un amont declare sur un de ces ports se parle en TLS (comme a l'edge).
|
||||||
serveur_web_frontal_sonde_port: 80
|
serveur_web_frontal_ports_tls: [443, 8443]
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,26 @@
|
||||||
---
|
---
|
||||||
# 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.
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# publique que le site attribue au locataire (redirection a la frontiere). Il filtre (WAF)
|
||||||
|
# et relaie : il ne sert rien lui-meme, et ne recoit plus rien de l'edge.
|
||||||
flux:
|
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
|
|
||||||
# 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). Le 443 attend son
|
|
||||||
# TLS (Let's Encrypt) : la redirection est prete avant lui.
|
|
||||||
- sens: ingress
|
- sens: ingress
|
||||||
port: 80
|
port: 80
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
pair: externe
|
pair: externe
|
||||||
chiffrement: clair
|
chiffrement: clair
|
||||||
raison: "HTTP public — le premier contact d'un navigateur, et le defi HTTP d'ACME ; renvoie vers HTTPS."
|
raison: "HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la."
|
||||||
- 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."
|
raison: "HTTPS public — les sites et services que le locataire publie sur l'Internet (attend son certificat Let's Encrypt)."
|
||||||
- sens: egress
|
- sens: egress
|
||||||
port: 443
|
port: 80
|
||||||
protocole: tcp
|
protocole: tcp
|
||||||
pair: flotte
|
pair: serveur_web_dorsal
|
||||||
chiffrement: tls
|
chiffrement: clair
|
||||||
raison: "git clone/pull du depot du site (Forgejo souverain) au deploiement."
|
raison: "Relais vers le web dorsal, qui porte les sites et les applications publiques."
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,14 @@
|
||||||
---
|
---
|
||||||
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
# Supervision derivee du role. Voir docs/supervision-conception.md.
|
||||||
#
|
#
|
||||||
# CE QUE `serveur_nginx` NE PEUT PAS DIRE. L'edge a sa sonde, et elle repond « nginx sert
|
# LE FRONTAL EST LA PORTE PUBLIQUE : s'il tombe, tout ce que le locataire publie tombe avec
|
||||||
# encore ». Ici la question est autre : CHACUN des sites declares se sert-il vraiment ?
|
# lui, pendant que les services derriere restent verts. Et un frontal peut tres bien
|
||||||
#
|
# relayer SANS FILTRER — module WAF non charge, regles absentes — en restant « en ligne ».
|
||||||
# LA PANNE PROPRE A CE ROLE EST SILENCIEUSE, et elle vient du fait que le contenu n'est
|
# `sites-servis` a suivi les sites sur le dorsal le 2026-09-29.
|
||||||
# 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: sites-servis
|
- nom: frontal
|
||||||
ttl: 5400
|
ttl: 5400
|
||||||
raison: 'Chacun des sites statiques declares se sert-il encore ? Le contenu vient
|
raison: 'Le reverse proxy public tient-il, son WAF (ModSecurity + OWASP CRS) est-il
|
||||||
d''un depot git : une branche renommee, un sous-dossier deplace ou un clone vide
|
vraiment dans le chemin, et chaque exposition publique repond-elle a travers lui ?
|
||||||
laissent nginx parfaitement vert avec un site perime, vide, ou en 404. Seul un GET
|
Un frontal qui relaie sans filtrer reste vert ; un frontal tombe rend tout le public
|
||||||
sur le `server_name` de chaque site le dit.'
|
injoignable pendant que les services derriere restent sains.'
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,103 @@
|
||||||
---
|
---
|
||||||
- name: Installer nginx + git (git requis pour cloner les depots des sites)
|
# serveur_web_frontal — reverse proxy + WAF des services web PUBLICS. Voir defaults/main.yml.
|
||||||
|
- name: Installer nginx, ModSecurity et l'OWASP CRS
|
||||||
ansible.builtin.apt:
|
ansible.builtin.apt:
|
||||||
name:
|
name: "{{ serveur_web_frontal_paquets }}"
|
||||||
- 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
|
- name: Retirer le vhost nginx par defaut de Debian
|
||||||
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: Creer la racine des sites
|
- name: Deployer le durcissement de base nginx
|
||||||
|
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_racine_base }}"
|
path: "{{ serveur_web_frontal_waf_dir }}"
|
||||||
state: directory
|
state: directory
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Cloner / mettre a jour le depot de chaque site (clone superficiel, contenu courant)
|
- name: Deployer la composition du WAF (ModSecurity + OWASP CRS)
|
||||||
ansible.builtin.git:
|
|
||||||
repo: "{{ item.depot }}"
|
|
||||||
dest: "{{ serveur_web_frontal_racine_base }}/{{ item.nom }}"
|
|
||||||
version: "{{ item.branche | default('main') }}"
|
|
||||||
depth: 1
|
|
||||||
force: true
|
|
||||||
loop: "{{ serveur_web_frontal_sites }}"
|
|
||||||
loop_control:
|
|
||||||
label: "{{ item.nom }}"
|
|
||||||
|
|
||||||
- name: Deployer le vhost de chaque site
|
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: site.conf.j2
|
src: waf.conf.j2
|
||||||
dest: "/etc/nginx/sites-available/{{ item.nom }}"
|
dest: "{{ serveur_web_frontal_waf_dir }}/main.conf"
|
||||||
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 le vhost de chaque site
|
# 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:
|
||||||
|
label: "{{ item.item | basename }}"
|
||||||
|
when:
|
||||||
|
- serveur_web_frontal_publier_expositions | bool
|
||||||
|
- item.stat.exists | default(false)
|
||||||
|
|
||||||
|
- name: Lister les expositions publiques destinees a ce frontal
|
||||||
|
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:
|
||||||
|
src: frontal.conf.j2
|
||||||
|
dest: /etc/nginx/sites-available/frontal.conf
|
||||||
|
owner: root
|
||||||
|
group: root
|
||||||
|
mode: "0644"
|
||||||
|
notify: Recharger nginx serveur_web_frontal
|
||||||
|
|
||||||
|
- name: Activer les vhosts du frontal
|
||||||
ansible.builtin.file:
|
ansible.builtin.file:
|
||||||
src: "/etc/nginx/sites-available/{{ item.nom }}"
|
src: /etc/nginx/sites-available/frontal.conf
|
||||||
dest: "/etc/nginx/sites-enabled/{{ item.nom }}"
|
dest: /etc/nginx/sites-enabled/frontal.conf
|
||||||
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
|
||||||
|
|
||||||
# LE ROLE QUI POSSEDE LA VERITE DEPOSE SA PROPRE SONDE. `client_sante` la fait tourner et
|
- name: Valider la configuration nginx avant tout rechargement
|
||||||
# pousse le resultat ; les greffons standard, eux, sont poses par le porteur.
|
ansible.builtin.command: nginx -t
|
||||||
#
|
changed_when: false
|
||||||
# LE CONTENU DE CE ROLE N'EST PAS DANS CE ROLE : il arrive par git depuis une forge. Une
|
when: not ansible_check_mode
|
||||||
# branche renommee, un sous-dossier deplace ou un clone vide laissent nginx parfaitement
|
|
||||||
# vert. Seul un GET par `server_name` le dit.
|
- name: Activer et demarrer nginx
|
||||||
|
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
|
||||||
|
|
@ -69,10 +106,10 @@
|
||||||
group: root
|
group: root
|
||||||
mode: "0755"
|
mode: "0755"
|
||||||
|
|
||||||
- name: Deposer la sonde « sites-servis »
|
- name: Deposer la sonde « frontal »
|
||||||
ansible.builtin.template:
|
ansible.builtin.template:
|
||||||
src: sonde-sites-servis.sh.j2
|
src: sonde-frontal.sh.j2
|
||||||
dest: /usr/local/lib/setops/sondes/sites-servis.sh
|
dest: /usr/local/lib/setops/sondes/frontal.sh
|
||||||
owner: root
|
owner: root
|
||||||
group: root
|
group: root
|
||||||
mode: "0750"
|
mode: "0750"
|
||||||
|
|
|
||||||
3
roles/serveur_web_frontal/templates/durcissement.conf.j2
Normal file
3
roles/serveur_web_frontal/templates/durcissement.conf.j2
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 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 }};
|
||||||
41
roles/serveur_web_frontal/templates/frontal.conf.j2
Normal file
41
roles/serveur_web_frontal/templates/frontal.conf.j2
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
# 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 %}
|
||||||
40
roles/serveur_web_frontal/templates/sonde-frontal.sh.j2
Normal file
40
roles/serveur_web_frontal/templates/sonde-frontal.sh.j2
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
#!/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}."
|
||||||
12
roles/serveur_web_frontal/templates/waf.conf.j2
Normal file
12
roles/serveur_web_frontal/templates/waf.conf.j2
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
# 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
|
||||||
|
|
@ -9,8 +9,11 @@
|
||||||
graph LR
|
graph LR
|
||||||
R["<b>postfix</b>"]
|
R["<b>postfix</b>"]
|
||||||
E0["externe"] -->|"25 · starttls"| R
|
E0["externe"] -->|"25 · starttls"| R
|
||||||
E1["flotte"] -->|"587 · starttls"| R
|
E1["externe"] -->|"465 · tls-requis"| R
|
||||||
E2["smtp"] -->|"25 · starttls"| R
|
E2["externe"] -->|"587 · 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"]]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -19,7 +22,8 @@ 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` | starttls | Soumission authentifiée (submission) pour les agents internes qui envoient du courrier. |
|
| `587` | `flotte`, `externe` | starttls | Soumission authentifiée (STARTTLS obligatoire) : agents internes et clients de courriel des utilisateurs. |
|
||||||
|
| `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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,23 @@
|
||||||
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` | clair | Front nginx local des webapps, proxie par l'edge (TLS termine a l'edge). |
|
| `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). |
|
||||||
|
|
||||||
## 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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,21 +8,23 @@
|
||||||
```mermaid
|
```mermaid
|
||||||
graph LR
|
graph LR
|
||||||
R["<b>web_frontal</b>"]
|
R["<b>web_frontal</b>"]
|
||||||
E0["edge"] -->|"80 · clair"| R
|
E0["externe"] -->|"443 · tls-requis"| R
|
||||||
R -.->|"sonde « sites-servis »"| ICINGA[["Icinga"]]
|
E1["externe"] -->|"80 · clair"| R
|
||||||
|
R -.->|"sonde « frontal »"| ICINGA[["Icinga"]]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Qui lui parle
|
## Qui lui parle
|
||||||
|
|
||||||
| port | depuis | chiffrement | pourquoi |
|
| port | depuis | chiffrement | pourquoi |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `80` | `edge` | clair | Contenu statique servi au navigateur via l'edge (TLS termine a l'edge). |
|
| `80` | `externe` | clair | HTTP public, relaye a travers le WAF — et le defi HTTP d'ACME ; renverra vers HTTPS quand le certificat sera la. |
|
||||||
|
| `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 |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `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. |
|
| `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. |
|
||||||
|
|
||||||
## Ce qu'il expose en séries
|
## Ce qu'il expose en séries
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) | 2 | 1 | — |
|
| [`serveur_postfix`](Rôle-serveur_postfix) | 3 | 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 | 1 | — |
|
| [`serveur_web_dorsal`](Rôle-serveur_web_dorsal) | 1 | 2 | — |
|
||||||
| [`serveur_web_frontal`](Rôle-serveur_web_frontal) | 1 | 1 | — |
|
| [`serveur_web_frontal`](Rôle-serveur_web_frontal) | 2 | 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) | — | — | — |
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue