@nodefony/devkit 10.0.0-beta.3 → 10.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +146 -4
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorate.js +1 -2
- package/dist/_virtual/{_@oxc-project_runtime@0.153.0 → _@oxc-project_runtime@0.155.0}/helpers/esm/decorateParam.js +1 -2
- package/dist/index.js +24 -11
- package/dist/nodefony/config/config.js +16 -137
- package/dist/nodefony/config/defineModuleConfig.js +1 -2
- package/dist/nodefony/controllers/DevkitController.js +7 -14
- package/dist/nodefony/controllers/McpController.js +25 -226
- package/dist/nodefony/controllers/ProfileSessionController.js +47 -0
- package/dist/nodefony/mcp/observe.js +235 -0
- package/dist/nodefony/mcp/profileReport.js +242 -0
- package/dist/nodefony/mcp/profileRoute.js +521 -0
- package/dist/nodefony/mcp/shotClient.js +84 -0
- package/dist/nodefony/mcp/shutdown.js +44 -0
- package/dist/nodefony/mcp/tools.js +696 -0
- package/dist/nodefony/service/DevkitService.js +93 -29
- package/dist/nodefony/src/errors/DevkitError.js +1 -2
- package/dist/nodefony/src/profileSession.js +14 -0
- package/dist/types/index.d.ts +15 -1
- package/dist/types/nodefony/config/config.d.ts +1 -1
- package/dist/types/nodefony/controllers/McpController.d.ts +11 -26
- package/dist/types/nodefony/controllers/ProfileSessionController.d.ts +31 -0
- package/dist/types/nodefony/interfaces/IDevkitService.d.ts +22 -1
- package/dist/types/nodefony/mcp/observe.d.ts +83 -0
- package/dist/types/nodefony/mcp/profileReport.d.ts +138 -0
- package/dist/types/nodefony/mcp/profileRoute.d.ts +87 -0
- package/dist/types/nodefony/mcp/shotClient.d.ts +53 -0
- package/dist/types/nodefony/mcp/shutdown.d.ts +25 -0
- package/dist/types/nodefony/mcp/tools.d.ts +109 -0
- package/dist/types/nodefony/service/DevkitService.d.ts +41 -4
- package/dist/types/nodefony/src/profileSession.d.ts +12 -0
- package/docs/index.md +160 -11
- package/package.json +16 -11
- package/skills/nodefony-add-crud/SKILL.md +5 -0
- package/skills/nodefony-add-service/SKILL.md +3 -3
- package/skills/nodefony-authentication/SKILL.md +5 -5
- package/skills/nodefony-browser/SKILL.md +20 -0
- package/skills/nodefony-browser/references/socket.md +13 -4
- package/skills/nodefony-browser/scripts/inspect.mjs +1 -1
- package/skills/nodefony-browser/scripts/lib/probes.mjs +53 -0
- package/skills/nodefony-browser/scripts/socket.mjs +70 -4
- package/skills/nodefony-browser/scripts/watch.mjs +101 -0
- package/skills/nodefony-dev/SKILL.md +31 -1
- package/skills/nodefony-dev/scripts/docs.mjs +3 -3
- package/skills/nodefony-devops/SKILL.md +85 -64
- package/skills/nodefony-devops/references/dimensionnement.md +122 -0
- package/skills/nodefony-devops/references/frontal.md +277 -47
- package/skills/nodefony-devops/references/kubernetes.md +41 -14
- package/skills/nodefony-devops/references/secrets.md +13 -2
- package/skills/nodefony-devops/references/variables.md +14 -12
- package/skills/nodefony-protect-route/SKILL.md +6 -4
- package/skills/nodefony-security-review/scripts/review-diff.mjs +4 -4
- package/skills/nodefony-troubleshoot/SKILL.md +86 -12
- package/dist/_virtual/_@oxc-project_runtime@0.153.0/helpers/esm/decorateMetadata.js +0 -6
|
@@ -1,28 +1,49 @@
|
|
|
1
|
-
# Derrière un frontal — nginx,
|
|
1
|
+
# Derrière un frontal — nginx, HAProxy, h2c, Ingress, Gateway API, Traefik
|
|
2
2
|
|
|
3
3
|
> **Maintenance** : vérité courante. Éditer en place.
|
|
4
4
|
|
|
5
5
|
## 1. La configuration se DÉRIVE, elle ne s'écrit pas
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx nodefony proxy:generate nginx
|
|
9
|
-
npx nodefony proxy:generate haproxy
|
|
8
|
+
npx nodefony proxy:generate nginx # sur la sortie standard, pour lire avant d'installer
|
|
9
|
+
npx nodefony proxy:generate haproxy -o haproxy.cfg
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
| Option | Rôle |
|
|
13
|
+
| ----------------------------------------- | ----------------------------------------------------------------------------- |
|
|
14
|
+
| `-o, --out <fichier>` | écrire dans un fichier plutôt que sur la sortie standard |
|
|
15
|
+
| `-b, --backend <hôte>` | l'adresse à laquelle le frontal joint l'application (défaut `127.0.0.1`) |
|
|
16
|
+
| `-l, --listen <port>` | le port d'écoute du frontal (défaut `80`) |
|
|
17
|
+
| `--reencrypt` | TLS jusqu'à l'application (port HTTPS) au lieu du clair sur le réseau interne |
|
|
18
|
+
| `--tls-cert`, `--tls-key`, `--tls-listen` | nginx seulement : le frontal termine le TLS (écoute `443` par défaut) |
|
|
19
|
+
| `--assets-root <dossier>` | servir tous les fichiers statiques depuis un seul arbre (`assets:publish`) |
|
|
20
|
+
|
|
21
|
+
La commande **démarre l'application sans ouvrir de port** et lit sa configuration **réelle** :
|
|
22
|
+
ports, domaines de confiance, dossiers statiques montés par chaque module, taille de corps
|
|
23
|
+
maximale, battement de cœur WebSocket, port h2c. 🔴 Elle lit la configuration **de l'environnement
|
|
24
|
+
où elle tourne** : lance-la avec les variables de production, sinon tu obtiens la configuration
|
|
25
|
+
d'un autre déploiement — par exemple sans h2c.
|
|
14
26
|
|
|
15
27
|
**Pourquoi ça compte** : une configuration écrite à la main est juste le jour où on l'écrit. Au
|
|
16
28
|
premier dossier statique ajouté, à la première route de téléversement, elle devient fausse — et
|
|
17
|
-
elle ne le dit pas : elle sert un 404 ou un 413 que personne ne rattache
|
|
18
|
-
frontal.
|
|
29
|
+
elle ne le dit pas : elle sert un 404 ou un 413 que personne ne rattache au frontal.
|
|
19
30
|
|
|
20
|
-
|
|
21
|
-
moment où l'image est construite**, donc à partir du code exact qu'elle embarque.
|
|
31
|
+
## 2. Ce que le générateur pose, et pourquoi
|
|
22
32
|
|
|
23
|
-
|
|
33
|
+
| Ce qui est généré | Ce que ça évite |
|
|
34
|
+
| --------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- |
|
|
35
|
+
| `X-Forwarded-For` **écrasé** avec l'adresse vue du client | un client qui s'invente une adresse |
|
|
36
|
+
| `Upgrade` / `Connection` relayés | une WebSocket qui « ne se connecte pas », sans le moindre message |
|
|
37
|
+
| inactivité = **4 battements WebSocket, plancher 300 s** | un frontal qui coupe des sockets **vivantes** ; nginx `proxy_read_timeout`/`proxy_send_timeout`, HAProxy `timeout client`/`server` |
|
|
38
|
+
| HAProxy : `timeout tunnel` à la même valeur | une fois passée en WebSocket, la connexion n'obéit plus à `timeout server` : sans cette ligne, le défaut la coupe |
|
|
39
|
+
| `client_max_body_size` dérivé de la taille de corps de l'application | un `413` de nginx (1 Mo par défaut) que l'application n'a jamais vu passer |
|
|
40
|
+
| une chaîne de `try_files` sur **toutes** les racines statiques | les fichiers d'un module en `404` alors qu'ils sont bien montés |
|
|
41
|
+
| HAProxy : `Forwarded` entrant **effacé** avant le nôtre | un en-tête forgé conservé à côté du vrai |
|
|
42
|
+
| HAProxy : le schéma **constaté** sur la connexion entrante (`ssl_fc`) | annoncer `https` pour un client venu en clair — cookies `Secure` posés sur du clair |
|
|
43
|
+
| HAProxy : `option httpchk GET /readyz` dans chaque backend | un exemplaire qui draine gardé dans la table — un contrôle TCP le croit sain tant que son port accepte (§6) |
|
|
44
|
+
| nginx avec TLS : HTTP/2 côté navigateur (`http2 on`), HTTP/1.1 vers l'application | le plafond d'environ six connexions par origine est celui du navigateur envers le **frontal** : c'est là que le multiplexage compte |
|
|
24
45
|
|
|
25
|
-
|
|
46
|
+
## 3. Le point qui casse tout si on l'oublie — `NF__HTTP__TRUSTPROXY`
|
|
26
47
|
|
|
27
48
|
Quand ton frontal termine le TLS, l'application reçoit du **HTTP en clair**, plus un en-tête qui
|
|
28
49
|
annonce le schéma d'origine. Sans réglage, elle ne croit pas cet en-tête — et c'est la bonne
|
|
@@ -30,10 +51,12 @@ position par défaut, parce qu'un en-tête est trivialement falsifiable par un c
|
|
|
30
51
|
|
|
31
52
|
Conséquence si tu ne le poses pas : le schéma constaté est `http`, donc :
|
|
32
53
|
|
|
33
|
-
- **le cookie
|
|
34
|
-
sécurisée.
|
|
35
|
-
|
|
36
|
-
|
|
54
|
+
- **le cookie de session part sans le préfixe `__Host-`** — ce préfixe n'est posé que sur une
|
|
55
|
+
origine que l'application sait sécurisée. La session marche, mais perd ce qu'il garantit (pas de
|
|
56
|
+
`Domain`, donc pas de fixation de session depuis un sous-domaine), **sans aucun message** ;
|
|
57
|
+
- l'adresse cliente enregistrée est celle du frontal : l'audit, la limitation de débit et le
|
|
58
|
+
plafond de connexions par adresse portent sur **une seule** IP — une vague de reconnexions
|
|
59
|
+
WebSocket se fait alors refuser en `429` pour tout le monde ;
|
|
37
60
|
- les redirections peuvent pointer sur `http://` ;
|
|
38
61
|
- **toute connexion par mot de passe répond `403`** (« doit passer par HTTPS ») : en production,
|
|
39
62
|
un secret reçu sur une requête que l'application croit en clair est refusé avant toute
|
|
@@ -44,8 +67,12 @@ Conséquence si tu ne le poses pas : le schéma constaté est `http`, donc :
|
|
|
44
67
|
NF__HTTP__TRUSTPROXY: uniquelocal # le frontal est sur un réseau privé
|
|
45
68
|
```
|
|
46
69
|
|
|
47
|
-
|
|
48
|
-
|
|
70
|
+
Jamais `true` si l'application est joignable par un autre chemin que le frontal : n'importe quel
|
|
71
|
+
client pourrait alors écrire sa propre adresse. Dans une **cascade** (équilibreur d'hébergeur →
|
|
72
|
+
frontal → application), chaque saut doit être de confiance : la remontée de l'adresse se fait de
|
|
73
|
+
droite à gauche et s'arrête au premier inconnu.
|
|
74
|
+
|
|
75
|
+
**Le prouver, plutôt que le croire** :
|
|
49
76
|
|
|
50
77
|
```bash
|
|
51
78
|
curl -ksSI https://<hôte>/
|
|
@@ -55,19 +82,20 @@ curl -ksSI https://<hôte>/
|
|
|
55
82
|
Puis **débranche-le** : retire la variable, redéploie, refais la requête. Le cookie doit tomber. Un
|
|
56
83
|
réglage qu'on n'a jamais vu manquer n'est pas un réglage prouvé.
|
|
57
84
|
|
|
58
|
-
##
|
|
85
|
+
## 4. Les autres réglages du mode « derrière un frontal »
|
|
59
86
|
|
|
60
|
-
| Variable | À poser | Ce qu'elle fait
|
|
61
|
-
| ---------------------------- | --------------------------- |
|
|
62
|
-
| `NF__HTTP__TRUSTEDHOSTS` | les hôtes réellement servis | Un `Host` étranger est refusé — coupe l'empoisonnement d'en-tête
|
|
63
|
-
| `NF__APP__DOMAINCHECK` | `"true"` | Contrôle du domaine actif
|
|
64
|
-
| `NF__HTTP__STATICS__ENABLED` | `"false"` | **Le frontal sert les fichiers statiques** ; l'application cesse de les servir en double
|
|
87
|
+
| Variable | À poser | Ce qu'elle fait |
|
|
88
|
+
| ---------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
89
|
+
| `NF__HTTP__TRUSTEDHOSTS` | les hôtes réellement servis | Un `Host` étranger est refusé — coupe l'empoisonnement d'en-tête |
|
|
90
|
+
| `NF__APP__DOMAINCHECK` | `"true"` | Contrôle du domaine actif |
|
|
91
|
+
| `NF__HTTP__STATICS__ENABLED` | `"false"` | **Le frontal sert les fichiers statiques** ; l'application cesse de les servir en double |
|
|
92
|
+
| `NF_OAUTH_REDIRECT_BASE` | l'URL publique, sans chemin | Connexion par un fournisseur d'identité : sans elle, l'adresse de retour vaut `https://localhost:<port>` et le fournisseur refuse (`redirect_uri`) |
|
|
65
93
|
|
|
66
94
|
Laisser l'application servir les statiques derrière un frontal qui les sert aussi n'est pas une
|
|
67
95
|
erreur fonctionnelle — c'est un gaspillage silencieux : chaque fichier peut être servi par le
|
|
68
96
|
mauvais des deux, avec des en-têtes de cache différents.
|
|
69
97
|
|
|
70
|
-
##
|
|
98
|
+
## 5. Ne publie aucun port de l'application
|
|
71
99
|
|
|
72
100
|
Derrière un frontal, l'application **ne doit publier aucun port**. C'est la définition d'être
|
|
73
101
|
derrière quelque chose : le seul chemin doit passer par le frontal. C'est ce que fait le profil de
|
|
@@ -77,43 +105,228 @@ Si tu publies quand même un port « pour déboguer », tu as deux portes d'entr
|
|
|
77
105
|
sans limitation et sans les en-têtes que ton frontal pose. Utilise `docker compose exec` ou un
|
|
78
106
|
transfert de port temporaire.
|
|
79
107
|
|
|
80
|
-
##
|
|
81
|
-
|
|
82
|
-
Ton frontal a besoin de savoir quand retirer un exemplaire. Les deux sondes ne servent pas à la
|
|
83
|
-
même chose :
|
|
108
|
+
## 6. Les sondes, vues du frontal
|
|
84
109
|
|
|
85
110
|
| Sonde | Pour quoi | Pendant l'arrêt |
|
|
86
111
|
| --------- | ------------------------------- | --------------- |
|
|
87
112
|
| `/livez` | « le process est-il vivant ? » | **reste `200`** |
|
|
88
113
|
| `/readyz` | « peut-il prendre du trafic ? » | passe à `503` |
|
|
89
114
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
115
|
+
Un frontal qui retire les exemplaires doit sonder **`/readyz`**, jamais `/livez` : c'est `/readyz`
|
|
116
|
+
qui bascule dès le début de l'arrêt. Les deux répondent **avant la limitation de débit** — un `429`
|
|
117
|
+
sur une sonde veut dire que tu n'interroges pas le chemin nu (une réécriture ajoute un préfixe).
|
|
118
|
+
|
|
119
|
+
🔴 **Ce que les configurations générées font réellement** — à savoir dès que tu mets
|
|
120
|
+
**plusieurs** exemplaires derrière un même frontal :
|
|
121
|
+
|
|
122
|
+
| Frontal généré | Contrôle de santé |
|
|
123
|
+
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| HAProxy | `option httpchk GET /readyz` dans **chaque** backend : l'exemplaire est retiré dès que `/readyz` répond `503`, alors que son port accepte encore. En h2c, la sonde part sur le port HTTP en HTTP/1.1 (`check port 5151 check-proto h1`) |
|
|
125
|
+
| nginx | nginx libre n'a **pas** de contrôle actif : il retire un amont après des échecs constatés, jamais sur `/readyz` |
|
|
126
|
+
|
|
127
|
+
Le chemin vient de la configuration (`health.readinessPath`) ; sondes désactivées, le générateur
|
|
128
|
+
retombe sur un contrôle de connexion, plutôt que d'interroger un chemin que plus rien ne sert.
|
|
129
|
+
Éprouvé par un HAProxy 3.0 réel sur la configuration générée : un exemplaire dont la sonde ne
|
|
130
|
+
répond pas `200` sort du répartiteur alors que son port accepte.
|
|
131
|
+
|
|
132
|
+
Ce que la sonde ne règle pas seule : le frontal ne la lit que toutes les deux secondes. Pose un
|
|
133
|
+
**délai avant drain** (`shutdownDelay`, en millisecondes) au moins égal à deux
|
|
134
|
+
intervalles de contrôle, pour que le frontal ait vu le `503` avant que l'exemplaire cesse
|
|
135
|
+
d'accepter.
|
|
136
|
+
|
|
137
|
+
## 7. Les flux SSE derrière le frontal
|
|
138
|
+
|
|
139
|
+
Un flux SSE est une réponse HTTP qui ne finit pas. Trois réglages décident s'il est bien relayé :
|
|
140
|
+
|
|
141
|
+
| Réglage | Qui le pose | Pourquoi |
|
|
142
|
+
| ---------------------- | -------------------------------------------- | ------------------------------------------------------------------------------------------------- |
|
|
143
|
+
| pas de mise en tampon | l'application envoie `X-Accel-Buffering: no` | nginx relaie chaque événement au lieu d'attendre de remplir son tampon. Ne filtre pas cet en-tête |
|
|
144
|
+
| inactivité ≥ 300 s | le générateur | un flux muet n'est tenu que par son battement de 15 s : vingt battements de marge |
|
|
145
|
+
| HTTP/2 côté navigateur | le générateur (nginx avec TLS) | au-delà de six flux par origine en HTTP/1.1, le navigateur met les suivants en attente |
|
|
146
|
+
|
|
147
|
+
Entre le frontal et l'application, chaque flux SSE occupe **une connexion** en HTTP/1.1. Beaucoup
|
|
148
|
+
de flux longs par pod : c'est le cas où h2c (§8) vaut la peine.
|
|
149
|
+
|
|
150
|
+
## 8. HTTP/2 jusqu'à l'application — le port h2c
|
|
151
|
+
|
|
152
|
+
Par défaut le frontal parle HTTP/1.1 à l'application. Un frontal qui termine le TLS peut aussi lui
|
|
153
|
+
parler **HTTP/2 en clair** (h2c, connaissance préalable) sur un port **dédié**, désactivé par défaut :
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
NF__APP__SERVERS__H2C__ENABLED=true # port 5153 ; NF__APP__SERVERS__H2C__PORT pour le changer
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**h2c n'est pas « plus rapide » par principe.** Mesuré sur une route triviale, un seul process :
|
|
160
|
+
débit h2c rapporté à HTTP/1.1 = **−48 %** à forme égale (64 connexions × 1 flux), **−18 %** dans la
|
|
161
|
+
forme d'un frontal (4 connexions × 16 flux). Le frontal garde déjà un pool de connexions HTTP/1.1
|
|
162
|
+
qui masque le blocage en tête de file, et HTTP/2 coûte du travail par requête (trames, compression
|
|
163
|
+
des en-têtes). Le choix se fait sur la **forme du trafic** :
|
|
164
|
+
|
|
165
|
+
| Forme du trafic | h2c ? | Pourquoi |
|
|
166
|
+
| -------------------------------------------- | ------------- | --------------------------------------------------------------------------------------------- |
|
|
167
|
+
| gRPC | **requis** | gRPC n'existe qu'en HTTP/2 |
|
|
168
|
+
| Beaucoup de flux SSE longs par pod | **oui** | en HTTP/1.1 chaque flux tient une connexion du frontal ; en h2c ils partagent |
|
|
169
|
+
| En-têtes lourds et répétés (cookies, jetons) | probable | la compression HPACK les factorise d'une requête à l'autre |
|
|
170
|
+
| REST classique, petites réponses | **mesurer** | souvent neutre, parfois perdant |
|
|
171
|
+
| WebSocket | **non** | reste sur le port HTTP |
|
|
172
|
+
| Plusieurs workers dans un pod | **attention** | le cluster Node répartit des connexions : avec les rares connexions h2c, un worker prend tout |
|
|
173
|
+
|
|
174
|
+
**Trois règles valent pour TOUS les frontaux :**
|
|
175
|
+
|
|
176
|
+
| Règle | Pourquoi |
|
|
177
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
178
|
+
| les requêtes vont au port **h2c**, les **WebSocket** au port **HTTP** | le port h2c ne sert ni `Upgrade` ni le CONNECT étendu (RFC 8441), seul chemin d'une WebSocket en HTTP/2 |
|
|
179
|
+
| les **sondes** visent le port **HTTP** | le port h2c refuse HTTP/1.1, et une sonde de kubelet parle HTTP/1.1. Garde le port HTTP **actif** |
|
|
180
|
+
| le schéma passe par `X-Forwarded-Proto` **et** `NF__HTTP__TRUSTPROXY` | en clair, l'application ne croit jamais le `:scheme` que le frontal écrit |
|
|
181
|
+
|
|
182
|
+
Vérifier le port, puis comparer avec le même instrument :
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
curl --http2-prior-knowledge http://127.0.0.1:5153/readyz
|
|
186
|
+
h2load --h1 -n 100000 -c 64 http://127.0.0.1:5151/<route> # HTTP/1.1
|
|
187
|
+
h2load -n 100000 -c 4 -m 16 http://127.0.0.1:5153/<route> # h2c, forme d'un frontal
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
### HAProxy — généré
|
|
191
|
+
|
|
192
|
+
Lancé avec h2c actif, `proxy:generate haproxy` envoie les requêtes au port h2c en `proto h2` et
|
|
193
|
+
garde les WebSocket sur le port HTTP. Rien à écrire :
|
|
194
|
+
|
|
195
|
+
```text
|
|
196
|
+
use_backend be_nodefony if { hdr(upgrade) -i websocket }
|
|
197
|
+
default_backend be_nodefony_h2c
|
|
198
|
+
|
|
199
|
+
backend be_nodefony_h2c
|
|
200
|
+
option httpchk GET /readyz
|
|
201
|
+
http-check expect status 200
|
|
202
|
+
# la sonde vise le port HTTP en HTTP/1.1 : le port h2c refuse HTTP/1.1
|
|
203
|
+
server nodefony_h2c 10.0.0.12:5153 check port 5151 check-proto h1 proto h2
|
|
204
|
+
```
|
|
94
205
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
206
|
+
Avec `--reencrypt`, h2c est ignoré : le port TLS négocie déjà HTTP/2. **C'est la seule cible jouée
|
|
207
|
+
contre un frontal réel** (HAProxy 3.0, configuration générée) : requête servie en h2c, sonde lue
|
|
208
|
+
sur le port HTTP, WebSocket ouverte sur le port HTTP.
|
|
98
209
|
|
|
99
|
-
|
|
210
|
+
### nginx — reste en HTTP/1.1 vers l'application
|
|
211
|
+
|
|
212
|
+
nginx ne parle pas HTTP/2 à ses amonts (hors gRPC) : `proxy:generate nginx` ignore h2c.
|
|
213
|
+
|
|
214
|
+
### Envoy
|
|
215
|
+
|
|
216
|
+
Un cluster h2c pour les requêtes, un cluster HTTP/1.1 pour les WebSocket :
|
|
217
|
+
|
|
218
|
+
```yaml
|
|
219
|
+
routes:
|
|
220
|
+
- match:
|
|
221
|
+
prefix: "/"
|
|
222
|
+
headers:
|
|
223
|
+
[
|
|
224
|
+
{
|
|
225
|
+
name: upgrade,
|
|
226
|
+
string_match: { exact: websocket, ignore_case: true },
|
|
227
|
+
},
|
|
228
|
+
]
|
|
229
|
+
route:
|
|
230
|
+
{ cluster: nodefony_http, upgrade_configs: [{ upgrade_type: websocket }] }
|
|
231
|
+
- match: { prefix: "/" }
|
|
232
|
+
route: { cluster: nodefony_h2c }
|
|
233
|
+
clusters:
|
|
234
|
+
- name: nodefony_h2c # port 5153
|
|
235
|
+
typed_extension_protocol_options:
|
|
236
|
+
envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
|
|
237
|
+
"@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
|
|
238
|
+
explicit_http_config: { http2_protocol_options: {} }
|
|
239
|
+
- name: nodefony_http # port 5151, HTTP/1.1 par défaut
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Traefik
|
|
243
|
+
|
|
244
|
+
```yaml
|
|
245
|
+
labels:
|
|
246
|
+
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
|
|
247
|
+
- "traefik.http.routers.app.service=app-h2c"
|
|
248
|
+
- "traefik.http.services.app-h2c.loadbalancer.server.port=5153"
|
|
249
|
+
- "traefik.http.services.app-h2c.loadbalancer.server.scheme=h2c"
|
|
250
|
+
- "traefik.http.routers.app-ws.rule=Host(`app.example.com`) && HeaderRegexp(`Upgrade`, `(?i)^websocket$`)"
|
|
251
|
+
- "traefik.http.routers.app-ws.priority=100"
|
|
252
|
+
- "traefik.http.routers.app-ws.service=app-http"
|
|
253
|
+
- "traefik.http.services.app-http.loadbalancer.server.port=5151"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
> Envoy, Istio, Gateway API et Traefik : recettes tirées de leur documentation, **pas encore jouées
|
|
257
|
+
> contre un frontal réel**. Les éprouver avant de les déclarer justes.
|
|
258
|
+
|
|
259
|
+
## 9. Traefik sans h2c
|
|
260
|
+
|
|
261
|
+
`proxy:generate` ne connaît que nginx et HAProxy. Avec Traefik, la déclaration vit avec les
|
|
262
|
+
conteneurs :
|
|
263
|
+
|
|
264
|
+
```yaml
|
|
265
|
+
labels:
|
|
266
|
+
- "traefik.enable=true"
|
|
267
|
+
- "traefik.http.routers.app.rule=Host(`app.example.com`)"
|
|
268
|
+
- "traefik.http.routers.app.entrypoints=websecure"
|
|
269
|
+
- "traefik.http.routers.app.tls.certresolver=le"
|
|
270
|
+
- "traefik.http.services.app.loadbalancer.server.port=5151"
|
|
271
|
+
- "traefik.http.services.app.loadbalancer.responseforwarding.flushinterval=100ms"
|
|
272
|
+
- "traefik.http.services.app.loadbalancer.healthcheck.path=/readyz"
|
|
273
|
+
- "traefik.http.services.app.loadbalancer.healthcheck.interval=5s"
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Traefik relaie `Upgrade` de lui-même et ne croit les `X-Forwarded-*` entrants que des adresses
|
|
277
|
+
déclarées dans `entryPoints.<nom>.forwardedHeaders.trustedIPs` : derrière un équilibreur
|
|
278
|
+
d'hébergeur, déclarer cet amont, sinon l'adresse cliente est perdue.
|
|
279
|
+
|
|
280
|
+
## 10. En Kubernetes — pas de configuration nginx générée
|
|
100
281
|
|
|
101
282
|
Le frontal dérivé sert pour une machine, un compose, une machine virtuelle. **Dans un cluster, ce
|
|
102
|
-
rôle appartient au contrôleur d'Ingress
|
|
283
|
+
rôle appartient au contrôleur d'entrée** (Ingress ou Gateway API), que l'application ne connaît pas.
|
|
103
284
|
|
|
104
|
-
|
|
105
|
-
|
|
285
|
+
> 🔴 **ingress-nginx est retiré depuis mars 2026** (annonce du projet Kubernetes, novembre 2025) :
|
|
286
|
+
> plus de version, plus de correctif de sécurité. Les déploiements existants tournent encore, mais
|
|
287
|
+
> un nouveau déploiement vise la **Gateway API** ou un autre contrôleur.
|
|
106
288
|
|
|
107
|
-
|
|
289
|
+
| Point | Avec un frontal à toi | Avec un contrôleur d'entrée |
|
|
290
|
+
| ------------------------------- | ----------------------- | ---------------------------------------------------- |
|
|
291
|
+
| `NF__HTTP__TRUSTPROXY` | **obligatoire** | **obligatoire** — le CIDR des pods du contrôleur |
|
|
292
|
+
| `NF__HTTP__TRUSTEDHOSTS` | recommandé | recommandé |
|
|
293
|
+
| Configuration dérivée | oui | non — `Ingress`, `HTTPRoute` |
|
|
294
|
+
| Inactivité ≥ 300 s | posée par le générateur | **à poser toi-même** — sinon les WebSocket tombent |
|
|
295
|
+
| Taille de corps | dérivée | **à poser toi-même**, égale à celle de l'application |
|
|
296
|
+
| Qui sert les fichiers statiques | le frontal | l'application, ou un service dédié |
|
|
108
297
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
298
|
+
**Si tu restes sur ingress-nginx**, les réglages que le générateur aurait posés s'écrivent en
|
|
299
|
+
annotations (la WebSocket, elle, passe sans rien demander) :
|
|
300
|
+
|
|
301
|
+
```yaml
|
|
302
|
+
metadata:
|
|
303
|
+
annotations:
|
|
304
|
+
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
|
305
|
+
nginx.ingress.kubernetes.io/proxy-send-timeout: "300"
|
|
306
|
+
nginx.ingress.kubernetes.io/proxy-buffering: "off" # flux SSE
|
|
307
|
+
nginx.ingress.kubernetes.io/proxy-body-size: "<taille de corps de l'application>"
|
|
308
|
+
```
|
|
115
309
|
|
|
116
|
-
|
|
310
|
+
**h2c sous Kubernetes** : c'est le `Service` qui déclare le protocole de chaque port, puis la route
|
|
311
|
+
envoie les requêtes au port h2c et `Upgrade: websocket` au port HTTP (`VirtualService` sous Istio,
|
|
312
|
+
`HTTPRoute` avec une règle sur l'en-tête `upgrade` sous Gateway API) :
|
|
313
|
+
|
|
314
|
+
```yaml
|
|
315
|
+
apiVersion: v1
|
|
316
|
+
kind: Service
|
|
317
|
+
spec:
|
|
318
|
+
ports:
|
|
319
|
+
- name: http # sondes, WebSocket
|
|
320
|
+
port: 5151
|
|
321
|
+
appProtocol: http
|
|
322
|
+
- name: h2c
|
|
323
|
+
port: 5153
|
|
324
|
+
appProtocol: kubernetes.io/h2c # Gateway API ; Istio : http2
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
Les sondes du `Deployment` visent le port **5151**, jamais 5153.
|
|
328
|
+
|
|
329
|
+
## 11. Sous Podman sans privilèges — ce qu'on ne peut pas réparer
|
|
117
330
|
|
|
118
331
|
L'adresse source du client **est perdue** : le trafic arrive depuis la passerelle de redirection.
|
|
119
332
|
Derrière un frontal, l'en-tête d'adresse d'origine porte donc la passerelle, pas le client.
|
|
@@ -121,3 +334,20 @@ Derrière un frontal, l'en-tête d'adresse d'origine porte donc la passerelle, p
|
|
|
121
334
|
Ce n'est pas un réglage à trouver : c'est une propriété du mode sans privilèges. **Énonce-le** —
|
|
122
335
|
ton audit et ta limitation par adresse sont faux, sans aucune erreur pour le signaler. Voir
|
|
123
336
|
[`podman.md`](podman.md).
|
|
337
|
+
|
|
338
|
+
## 12. Symptôme → cause → correction
|
|
339
|
+
|
|
340
|
+
| Symptôme | Cause | Correction |
|
|
341
|
+
| -------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
342
|
+
| La WebSocket se ferme seule au bout d'une minute | l'inactivité du frontal est plus courte que le battement | régénérer ; sur un contrôleur d'entrée, `proxy-read-timeout` ≥ 300 s |
|
|
343
|
+
| La WebSocket ne s'ouvre pas, aucune erreur nulle part | `Upgrade`/`Connection` non relayés | régénérer ; sur une configuration écrite à la main, les ajouter |
|
|
344
|
+
| Depuis h2c, la WebSocket ne s'ouvre plus | le frontal l'envoie au port h2c | router `Upgrade: websocket` vers le port HTTP |
|
|
345
|
+
| Depuis h2c, le pod n'est jamais prêt | la sonde vise le port h2c, qui refuse HTTP/1.1 | sonder `/readyz` sur le port HTTP |
|
|
346
|
+
| Un flux SSE n'arrive qu'en bloc, ou à la fin | le frontal met la réponse en tampon | ne pas filtrer `X-Accel-Buffering` ; sur ingress-nginx, `proxy-buffering: "off"` |
|
|
347
|
+
| `413` sur un envoi que l'application accepte | limite de corps du frontal plus basse | régénérer ; sur un contrôleur d'entrée, `proxy-body-size` |
|
|
348
|
+
| Les statiques d'un module en `404` derrière nginx | une seule racine statique déclarée | régénérer : la chaîne de `try_files` vient des montages réels |
|
|
349
|
+
| Des `5xx` à chaque déploiement, plusieurs exemplaires | le frontal ne lit pas `/readyz` (§6), ou pas de délai avant drain | régénérer le frontal (sonde `/readyz`) + `shutdownDelay` |
|
|
350
|
+
| `429` en rafale juste après un déploiement | reconnexions comptées sur la seule adresse du frontal | `NF__HTTP__TRUSTPROXY` |
|
|
351
|
+
| Toutes les adresses des journaux sont celles du frontal | `NF__HTTP__TRUSTPROXY` absent — le défaut sûr | déclarer l'adresse ou le CIDR du frontal |
|
|
352
|
+
| Une adresse cliente **forgée** apparaît dans l'audit | confiance accordée alors que l'application est joignable directement | fermer l'accès direct, ne jamais poser `true` dans ce cas |
|
|
353
|
+
| `Invalid parameter: redirect_uri` en ligne, pas en local | `NF_OAUTH_REDIRECT_BASE` absente | la poser à l'URL publique de l'application |
|
|
@@ -21,14 +21,19 @@ qui n'existent pas.
|
|
|
21
21
|
|
|
22
22
|
## 2. Le contexte de sécurité — politique Restricted
|
|
23
23
|
|
|
24
|
-
**Commence par lire `deploy/migrate-job.yaml` :
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
24
|
+
**Commence par lire `deploy/migrate-job.yaml` : il passe la politique Restricted.** Recopie-en le
|
|
25
|
+
contexte de sécurité — mais pas son `readOnlyRootFilesystem: false`, qui convient à un travail
|
|
26
|
+
éphémère et pas à un exemplaire qui sert du trafic.
|
|
27
|
+
|
|
28
|
+
Restricted exige **quatre** champs, tous portés par le **manifeste** — aucun n'est « hérité de
|
|
29
|
+
l'image » : `runAsNonRoot: true` (et un `runAsUser`, s'il est posé, différent de 0),
|
|
30
|
+
`allowPrivilegeEscalation: false`, `capabilities.drop: ["ALL"]`, `seccompProfile` `RuntimeDefault`.
|
|
31
|
+
Ce que l'image apporte, c'est de les rendre satisfaisables sans contorsion : elle déclare
|
|
29
32
|
`USER 1000:1000`, donc l'identifiant numérique existe ; elle n'écoute qu'au-dessus de 1024, donc
|
|
30
|
-
aucune capacité n'est nécessaire.
|
|
31
|
-
|
|
33
|
+
aucune capacité n'est nécessaire. Aucun ne coûte rien.
|
|
34
|
+
|
|
35
|
+
**`readOnlyRootFilesystem: true` n'est PAS exigé par Restricted** : c'est un durcissement en plus,
|
|
36
|
+
recommandé, et le seul qui se travaille — il demande les deux volumes.
|
|
32
37
|
|
|
33
38
|
```yaml
|
|
34
39
|
spec:
|
|
@@ -69,8 +74,10 @@ kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted
|
|
|
69
74
|
kubectl apply -f <ton-deployment>.yaml # doit passer SANS avertissement
|
|
70
75
|
```
|
|
71
76
|
|
|
72
|
-
Puis **débranche** : retire
|
|
73
|
-
qu'on n'a jamais vue refuser quelque chose n'est pas une politique.
|
|
77
|
+
Puis **débranche** : retire `seccompProfile`, réapplique. L'admission doit **refuser** le pod. Une
|
|
78
|
+
politique qu'on n'a jamais vue refuser quelque chose n'est pas une politique. (Retirer un
|
|
79
|
+
`emptyDir` fait échouer le **démarrage**, pas l'admission : c'est une autre preuve, celle des
|
|
80
|
+
volumes.)
|
|
74
81
|
|
|
75
82
|
## 3. Les sondes
|
|
76
83
|
|
|
@@ -93,6 +100,8 @@ livenessProbe: { httpGet: { path: /livez, port: 5151 }, periodSeconds: 10 }
|
|
|
93
100
|
son arrêt propre — exactement ce qu'on cherche à éviter.
|
|
94
101
|
- Les deux répondent **avant la limitation de débit** : un kubelet limité recevrait `429` et
|
|
95
102
|
conclurait à un échec.
|
|
103
|
+
- **Toujours le port HTTP (5151)**, même si h2c est actif : le port h2c (5153) refuse HTTP/1.1,
|
|
104
|
+
la langue d'une sonde de kubelet. Un pod qui sonde 5153 n'est jamais prêt.
|
|
96
105
|
|
|
97
106
|
## 4. L'arrêt — ne perdre aucune requête
|
|
98
107
|
|
|
@@ -130,6 +139,11 @@ resources:
|
|
|
130
139
|
processeur ne tue pas — elle **ralentit en permanence**, et ça ne ressemble pas à une limite, ça
|
|
131
140
|
ressemble à une application lente. Les demandes suffisent à garantir la part.
|
|
132
141
|
|
|
142
|
+
Ces valeurs sont un **départ** pour un trafic modeste. Un pod qui doit tenir son plein débit
|
|
143
|
+
demande environ **un cœur** (`requests.cpu: "1"`) : ton code tourne sur une seule boucle. Combien
|
|
144
|
+
de pods, quelle mémoire par pod, et pourquoi c'est souvent le temps réel qui décide →
|
|
145
|
+
[`dimensionnement.md`](dimensionnement.md).
|
|
146
|
+
|
|
133
147
|
Si tu bornes le tas de la machine virtuelle JavaScript face à une limite mémoire de cgroup,
|
|
134
148
|
dimensionne-le avec une mesure, pas au jugé.
|
|
135
149
|
|
|
@@ -141,8 +155,10 @@ C'est la différence la plus structurante avec le compose de développement.
|
|
|
141
155
|
2. **Ne reporte jamais l'hôte du compose.** `compose.yaml` pointe un **service du compose** ; le
|
|
142
156
|
même nom dans un cluster désigne un Service qui n'existe pas. Rien ne casse au déploiement, tout
|
|
143
157
|
casse au premier accès.
|
|
144
|
-
3. **L'URL passe par un Secret
|
|
145
|
-
|
|
158
|
+
3. **L'URL passe par un Secret** — elle porte un mot de passe, elle n'a rien à faire dans un
|
|
159
|
+
ConfigMap. Deux formes, au choix : `valueFrom.secretKeyRef` (ce que fait
|
|
160
|
+
`deploy/migrate-job.yaml`) ou le Secret monté en fichier et `NF_DATABASE_URL_FILE`. Ne pose
|
|
161
|
+
jamais les deux : `NF_DATABASE_URL` et `NF_DATABASE_URL_FILE` ensemble arrêtent le démarrage.
|
|
146
162
|
4. **Le plafond de connexions.** Une base administrée les compte strictement. N exemplaires × le
|
|
147
163
|
pool de chacun peuvent la saturer, et l'erreur sort de **son** côté, pas du tien. Reste prudent
|
|
148
164
|
sur le nombre d'exemplaires.
|
|
@@ -164,14 +180,25 @@ env:
|
|
|
164
180
|
- { name: NF_POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } }
|
|
165
181
|
- { name: NF__HTTP__TRUSTPROXY, value: uniquelocal }
|
|
166
182
|
- { name: NF_CSRF_SECRET_FILE, value: /run/secrets/csrf }
|
|
167
|
-
-
|
|
183
|
+
- name: NF_DATABASE_URL # ou NF_DATABASE_URL_FILE vers un Secret monté (§6)
|
|
184
|
+
valueFrom: { secretKeyRef: { name: <app>-db, key: url } }
|
|
185
|
+
# plusieurs exemplaires + temps réel : un bus partagé, sinon chaque pod parle seul
|
|
186
|
+
- { name: NF_REALTIME_DRIVER, value: redis }
|
|
187
|
+
- name: NF_REDIS_URL
|
|
188
|
+
valueFrom: { secretKeyRef: { name: <app>-redis, key: url } }
|
|
168
189
|
```
|
|
169
190
|
|
|
170
191
|
`NF_POD_NAME` vient de l'API descendante. Elle dérive l'identité d'origine du bus temps réel : sans
|
|
171
192
|
elle, plusieurs exemplaires se confondent et les messages s'écoutent eux-mêmes.
|
|
172
193
|
|
|
173
|
-
`
|
|
174
|
-
|
|
194
|
+
`NF_REALTIME_DRIVER=redis` n'est utile qu'à partir de **deux** exemplaires : avec un seul, le bus
|
|
195
|
+
local suffit. Sans lui à deux exemplaires, un message n'atteint que les clients du pod qui l'émet,
|
|
196
|
+
**sans erreur** — détail dans [`dimensionnement.md`](dimensionnement.md) §4.
|
|
197
|
+
|
|
198
|
+
`NF__HTTP__TRUSTPROXY` est **obligatoire** dès qu'un contrôleur d'entrée termine le TLS —
|
|
199
|
+
[`frontal.md`](frontal.md) dit ce qui tombe sans lui, et comment le voir tomber. L'Ingress ou la
|
|
200
|
+
Gateway API, les annotations d'inactivité qui gardent les WebSocket, et le `Service` à deux ports
|
|
201
|
+
pour h2c : [`frontal.md`](frontal.md) §10.
|
|
175
202
|
|
|
176
203
|
## 8. Éprouver sans cluster de production
|
|
177
204
|
|
|
@@ -59,13 +59,23 @@ dès que l'application émet des jetons. Sans `NF_TOTP_KEY` ou `NF_WEBHOOK_KEY`,
|
|
|
59
59
|
démarre mais la brique se désactive (double authentification, webhooks), avec un journal CRITIC.
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
npx nodefony security:secrets --env # les secrets, en lignes CLÉ=valeur
|
|
62
|
+
npx nodefony security:secrets --env # les secrets, en lignes CLÉ=valeur — pour un `docker --env-file` ou un gestionnaire
|
|
63
63
|
npx nodefony security:secrets --jwt-keyset # la valeur de NF_JWT_KEYSET — une ligne JSON, clé PRIVÉE
|
|
64
64
|
```
|
|
65
65
|
|
|
66
66
|
Une valeur par ENVIRONNEMENT (staging ≠ production), générée une fois, rangée dans le
|
|
67
67
|
gestionnaire de secrets ; ne laisse traîner aucun fichier qui la contient.
|
|
68
68
|
|
|
69
|
+
**En intégration continue**, la CI générée (preset `complete`) tire ces clés à CHAQUE exécution,
|
|
70
|
+
juste avant `test:e2e` : `security:secrets --env >> "$GITHUB_ENV"` sous GitHub, une boucle
|
|
71
|
+
`export "$ligne"` sous GitLab. Tirées pour un run, elles ne protègent rien — elles n'ont rien à
|
|
72
|
+
faire dans les `secrets` du dépôt. Un `CRITIC … AUCUNE clé de chiffrement` dans le journal de
|
|
73
|
+
l'e2e veut dire que l'étape a sauté : la suite est verte sur une production amputée.
|
|
74
|
+
|
|
75
|
+
> 🔴 **Ne jamais charger ces lignes par `set -a && . fichier`.** Le shell interprète les
|
|
76
|
+
> guillemets du JSON de `NF_JWT_KEYSET` et le rend illisible. `docker run --env-file`, un
|
|
77
|
+
> `env_file:` de compose ou `export "$ligne"` ligne à ligne gardent la valeur telle quelle.
|
|
78
|
+
|
|
69
79
|
`NF_ADMIN_PASSWORD` a un défaut en développement, aucun en production : sans lui, le premier
|
|
70
80
|
démarrage ne crée **aucun** compte d'administration — plutôt qu'un compte au mot de passe connu de
|
|
71
81
|
tout le monde. Il ne sert qu'à ce premier démarrage ; `npx nodefony security:user:add <id> --admin`
|
|
@@ -73,7 +83,8 @@ fait le même geste à la main.
|
|
|
73
83
|
|
|
74
84
|
## 5. Les monter, pas les poser en clair
|
|
75
85
|
|
|
76
|
-
Le suffixe `_FILE` marche sur
|
|
86
|
+
Le suffixe `_FILE` marche sur les variables du catalogue, URL de base et de cache comprises —
|
|
87
|
+
l'URL d'une base porte un mot de passe, c'est elle qu'on monte en premier. Détail complet dans
|
|
77
88
|
[`variables.md`](variables.md). Ce qu'il faut retenir ici :
|
|
78
89
|
|
|
79
90
|
```yaml
|
|
@@ -80,8 +80,10 @@ production : `platform` (variables du déploiement), `secrets` (gestionnaire de
|
|
|
80
80
|
|
|
81
81
|
## 3. Le suffixe `_FILE` — les secrets montés
|
|
82
82
|
|
|
83
|
-
|
|
84
|
-
|
|
83
|
+
Les variables du catalogue acceptent un `_FILE`, **URL d'infrastructure comprises**
|
|
84
|
+
(`NF_DATABASE_URL_FILE`, `NF_REDIS_URL_FILE`, et leurs alias de plateforme). Le contenu du fichier
|
|
85
|
+
est lu, retour à la ligne final retiré. Une seule règle les tranche toutes : le tableau ci-dessous
|
|
86
|
+
vaut pour l'URL de la base comme pour un secret de session.
|
|
85
87
|
|
|
86
88
|
```yaml
|
|
87
89
|
env:
|
|
@@ -117,12 +119,12 @@ Les seules qui te concernent au déploiement. Le catalogue complet, c'est `npx n
|
|
|
117
119
|
|
|
118
120
|
### Derrière un frontal — **les plus importantes**
|
|
119
121
|
|
|
120
|
-
| Variable | Rôle | Absente ⇒
|
|
121
|
-
| ---------------------------- | ------------------------------------------------- |
|
|
122
|
-
| `NF__HTTP__TRUSTPROXY` | Quels intermédiaires sont crus sur leurs en-têtes | **Le schéma constaté est `http`** → cookie `__Host
|
|
123
|
-
| `NF__HTTP__TRUSTEDHOSTS` | Les hôtes servis | Un `Host` étranger n'est pas refusé
|
|
124
|
-
| `NF__APP__DOMAINCHECK` | Contrôle du domaine | Pas de contrôle
|
|
125
|
-
| `NF__HTTP__STATICS__ENABLED` | L'application sert-elle les fichiers statiques | Elle les sert — donc en double avec ton frontal
|
|
122
|
+
| Variable | Rôle | Absente ⇒ |
|
|
123
|
+
| ---------------------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `NF__HTTP__TRUSTPROXY` | Quels intermédiaires sont crus sur leurs en-têtes | **Le schéma constaté est `http`** → cookie de session sans `__Host-`, mot de passe refusé `403`, adresse client fausse |
|
|
125
|
+
| `NF__HTTP__TRUSTEDHOSTS` | Les hôtes servis | Un `Host` étranger n'est pas refusé |
|
|
126
|
+
| `NF__APP__DOMAINCHECK` | Contrôle du domaine | Pas de contrôle |
|
|
127
|
+
| `NF__HTTP__STATICS__ENABLED` | L'application sert-elle les fichiers statiques | Elle les sert — donc en double avec ton frontal |
|
|
126
128
|
|
|
127
129
|
### Données
|
|
128
130
|
|
|
@@ -170,10 +172,10 @@ Tous en `_FILE` en production. Détail et rotation → [`secrets.md`](secrets.md
|
|
|
170
172
|
|
|
171
173
|
### Multi-process sur une seule machine
|
|
172
174
|
|
|
173
|
-
| Variable | Rôle
|
|
174
|
-
| ------------ |
|
|
175
|
-
| `NF_CLUSTER` |
|
|
176
|
-
| `NF_WORKERS` | Nombre d'exécutants — sinon dérivé des limites du cgroup
|
|
175
|
+
| Variable | Rôle |
|
|
176
|
+
| ------------ | ------------------------------------------------------------------------------ |
|
|
177
|
+
| `NF_CLUSTER` | Posée par `nodefony cluster` dans chaque worker — **ne pas la poser soi-même** |
|
|
178
|
+
| `NF_WORKERS` | Nombre d'exécutants — sinon dérivé des limites du cgroup |
|
|
177
179
|
|
|
178
180
|
En Kubernetes, **laisse ça tranquille** : un exemplaire = un processus = un pod, et c'est
|
|
179
181
|
l'ordonnanceur qui multiplie. Le mode multi-process sert sur une machine nue.
|
|
@@ -135,10 +135,12 @@ n'en envoie aucun, et la défense de provenance le laisse alors passer — c'est
|
|
|
135
135
|
distingue les sites, pas les intentions. Résultat mesuré : un `POST /api/cart/items` sans jeton rend
|
|
136
136
|
`201`, et l'application croit avoir une défense.
|
|
137
137
|
|
|
138
|
-
Toute action qui ÉCRIT porte donc `@CsrfProtect` explicitement.
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
138
|
+
Toute action qui ÉCRIT porte donc `@CsrfProtect` explicitement. N'écris AUCUNE route pour distribuer
|
|
139
|
+
le jeton : `GET /nodefony/security/api/auth/me` le sème déjà (cookie lisible `csrf-token`), comme
|
|
140
|
+
toute requête sûre vers une route décorée — et il vaut pour TOUTES les routes protégées. La
|
|
141
|
+
mutation le rejoue dans l'en-tête `x-csrf-token` — c'est le double-submit, sinon `403`. Un test le
|
|
142
|
+
récupère par `csrfHeaders(base, cookie)` de `nodefony/testing`. La provenance et le jeton se
|
|
143
|
+
**cumulent** ; l'une ne remplace jamais l'autre.
|
|
142
144
|
|
|
143
145
|
## 🔴 Ce qu'il ne faut jamais écrire
|
|
144
146
|
|