From f965f64b07e8b7a4b7beb6d6300c64a08dab65bb Mon Sep 17 00:00:00 2001 From: Blomios Date: Thu, 16 Jul 2026 23:44:12 +0200 Subject: [PATCH] =?UTF-8?q?chore(ideai):=20cl=C3=B4ture=20de=20#72=20(conf?= =?UTF-8?q?iance=20reverse=20proxy)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #72 passé en `closed` : mergé dans `develop` via `ae01297`, vert hors sandbox, branche supprimée. Carnet complété avec le périmètre livré et les arbitrages. État `.ideai/` indépendant du code de #68, committé directement sur `develop`. Co-Authored-By: Claude Opus 4.8 --- .../97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json | 160 +++++++++++++++++- .ideai/tickets/72/carnet.md | 53 +++++- .ideai/tickets/72/issue.md | 6 +- .ideai/tickets/index.json | 4 +- 4 files changed, 214 insertions(+), 9 deletions(-) diff --git a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json index d3f788b..8228b26 100644 --- a/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json +++ b/.ideai/background-tasks/97b49ac2-8376-4aa3-8ea9-bf3ac81d0023.json @@ -7657,10 +7657,166 @@ "ticket_id": "be71eeb6-f960-43f9-a317-9e76fc30197c", "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" }, - "state": "running", + "state": "completed", "wakePolicy": "recordOnly", "createdAtMs": 1784236059726, - "updatedAtMs": 1784236059726, + "updatedAtMs": 1784236326259, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784236326259, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "#72 est mergé, sans conflit. `develop` est sur **`7fbaa8e`**, vert, arbre propre.\n\n## Commits\n\n| Commit | |\n|---|---|\n| `75d1ce8` | `feat(web-server)` — guard proxy, `--trusted-proxy`, diagnostics, B0 |\n| `42804fc` | `docs(server)` — `--trusted-proxy` et `X-Forwarded-Proto` |\n| `ae01297` | merge `--no-ff` dans `develop` |\n| `7fbaa8e` | `chore(ideai)` — ouverture de #73, à part |\n\nJ'ai séparé le code, la doc et l'état `.ideai/` : la convention du dépôt le demande explicitement, et #73 n'a rien à voir avec #72. Pas de rebase — `develop` n'avait pas bougé depuis `e8a72f5`. Branche supprimée.\n\n## QA refaite hors sandbox, et revue de sécurité\n\n`cargo test -p web-server` → **66 passed**, ton chiffre confirmé. Puis re-vérifié sur `develop` après merge : 66 · 41 · 35, aucun échec sur 24 suites.\n\nComme c'est du code de sécurité, je ne me suis pas contenté du vert. J'ai vérifié quatre choses, toutes bonnes :\n\nLe guard est câblé sur **les trois** surfaces — statique (1389), `/api/*` (1395), WebSocket (853). Aucune route n'y échappe. Le chemin de production propage toujours le vrai pair : `handle_tcp_connection` passe `Some(peer_addr.ip())` à `dispatch_http` et `peer_addr.ip()` à `handle_ws_upgrade`. Le repli `peer_ip.unwrap_or(config.listen.ip())` m'a fait tiquer — un pair inconnu retombant sur l'IP d'écoute serait exactement le genre de trou qu'on cherche — mais les seuls appelants qui passent autre chose sont `handle_request`/`handle_request_from_peer`, tous deux `#[cfg(test)]` : inatteignables en production. Le CIDR est correct, y compris `prefix == 0`, qui aurait débordé au shift s'il n'avait pas été traité à part. Et l'ordre est le bon : pair vérifié **avant** toute lecture de header, `X-Forwarded-For` journalisé sans jamais autoriser.\n\nLes deux faits que tu voulais consignés sont **dans les messages de commit**, pas dans un carnet : l'origine du ticket, et le fait que le durcissement casse volontairement les configurations existantes. Le faux vert et la raison d'exiger le hors-sandbox y sont aussi — c'est la deuxième fois aujourd'hui que ce piège se referme, il fallait que l'historique le dise.\n\n## Pour la suite\n\nJe n'ai pas créé la branche de #68 : tu m'as dit de merger d'abord, et rien ne presse tant que DevBackend n'attaque pas. Quand tu la demandes, ce sera **`feature/ticket68-embedded-server-panel`** — le nom que j'avais réservé dans le plan, cohérent avec `feature/ticket68-embedded-server-core` déjà mergé. La topologie encaisse le réordonnancement de l'utilisateur sans rien changer : c'est précisément l'intérêt d'une branche par lot, chaque étape part d'un `develop` à jour.\n\nUne remarque pour l'utilisateur à son retour, sans valeur bloquante : **#73 (TLS intégré) rendrait une partie de #72 obsolète**. Si `idea-serve` termine lui-même TLS, la cérémonie proxy — et donc `--trusted-proxy`, le guard, les diagnostics — ne concerne plus que les déploiements qui gardent un proxy devant. Ce n'est pas du travail perdu, #72 corrige un vrai trou de sécurité aujourd'hui et B0 était un bug réel, mais l'ordre des deux mérite peut-être un regard.\n\n`develop` est en avance de 20 commits sur `origin/develop`. Aucune action sortante : pas de push, pas de PR.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "f11794f8-2fde-4d7d-8759-83426a12beb2", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "ticket_id": "c54b0a42-ed72-451c-a57c-ecfe4fd0a589", + "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784236378721, + "updatedAtMs": 1784236412760, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784236412760, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "**Une seule branche : `feature/ticket68-embedded-server-desktop`**, créée sur `develop@7fbaa8e`. Elle est prête, DevBackend peut partir.\n\nJ'ai écarté `-panel` : le nom aurait sous-décrit un lot qui porte aussi le cycle de vie et le store. `-desktop` couvre l'ensemble et se lit en paire avec `feature/ticket68-embedded-server-core` déjà mergé — les deux moitiés du même ticket.\n\n**Et une seule, pas deux.** Ma règle n'était pas « une branche par lot » mais une branche par *étape d'intégration* : je l'avais posée parce que **deux tickets** s'entrelaçaient et que la topologie devait garantir l'ordre. Ici il n'y a qu'un ticket et aucun tiers à intercaler. Découper backend/frontend créerait une dépendance séquentielle stricte — F1 ne peut rien consommer avant que B2 n'existe — pour zéro bénéfice : le panneau suit immédiatement, il n'y a rien à merger entre les deux. Et #68 backend seul serait un lot mergeable mais inerte, comme `run_embedded` l'a été pour #65 : acceptable quand un tiers doit passer devant, gratuit ici.\n\nLe précédent du dépôt va dans le même sens : #13, qui portait serveur *et* surface web, a tenu sur une branche unique ; #65 était backend-pur et #69 frontend-pur parce que les tickets l'étaient, pas par principe de séparation.\n\nNoté pour #73 — je ne rouvre rien, l'arbitrage tient, la remarque attendra son retour.\n\nUne modif `.ideai/` non liée (carnet #72, index, journal des tâches de fond) est dans l'arbre et voyage avec ; elle est identique sur les deux branches, donc sans risque. Je la committerai sur `develop` quand tu me diras que #72 est refermé côté ticket.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "b6a934d8-888b-42da-af91-3d1fccdc051f", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "73c853d1-c0fd-463b-ad17-1d24fefa371f", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "73c853d1-c0fd-463b-ad17-1d24fefa371f", + "ticket_id": "b6515a83-ed7e-4e78-9a88-eebf9ab61862", + "conversation_id": "d5068bc8-786e-0428-1d14-83b93961a15e" + }, + "state": "failed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784236449724, + "updatedAtMs": 1784237050020, + "deadlineMs": null, + "result": { + "outcome": "failure", + "finished_at_ms": 1784237050020, + "exit_code": null, + "error": "Timeout: rendezvous inactivity window expired for target DevBackend", + "stdout_tail": null, + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "1d4427a5-9ade-43d5-af99-021137ad53a4", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "73c853d1-c0fd-463b-ad17-1d24fefa371f", + "target_agent_id": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "ticket_id": "23970ad8-575e-4166-b882-0a22d64f0706", + "conversation_id": "bec31f20-db12-0995-00f2-ed824a618dea" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784236460920, + "updatedAtMs": 1784236473789, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784236473789, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Rien à faire : le worktree y était déjà, depuis la création de la branche au tour précédent.\n\n**Branche active** : `feature/ticket68-embedded-server-desktop`\n**HEAD** : `7fbaa8e` (`chore(ideai): ouverture de #73`) — sommet de `develop`, la branche n'a encore aucun commit propre.\n\nAucun commit, aucun merge, aucune bascule effectuée.\n\nModifications non liées présentes et intactes, toutes de l'état `.ideai/` : carnet et issue de #72, `tickets/index.json`, et le journal des tâches de fond. Elles datent de la clôture de #72 et attendent que tu me confirmes que le ticket est refermé pour que je les committe sur `develop`. DevBackend peut attaquer B2.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "78867bcc-d863-4d2f-b723-7a1f78cf9538", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "aefdbd61-e3d4-4bc1-9f42-c259446a97b5", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "73c853d1-c0fd-463b-ad17-1d24fefa371f", + "target_agent_id": "aefdbd61-e3d4-4bc1-9f42-c259446a97b5", + "ticket_id": "6227d1c4-318b-419a-98a7-3c82dbe8df9b", + "conversation_id": "dd35eeb0-2329-0dfa-3255-df7dba90a0aa" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784237121990, + "updatedAtMs": 1784237185553, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784237185553, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Verdict QA ciblé #68 backend desktop embedded server : **pas d’écart bloquant trouvé à la relecture**. Le seul rouge local est bien le test de bind réel `127.0.0.1:0` qui échoue visiblement sur `EPERM`, sans fallback silencieux.\n\nPoints validés :\n- Desktop appelle `web_server::run_embedded_with_core(config, Arc)` dans [embedded_server.rs](/home/anthony/Documents/Projects/IdeA/crates/app-tauri/src/embedded_server.rs:249). Aucun appel à `run_embedded(config)` trouvé côté `app-tauri`.\n- `embedded_server_start` passe `state.core()` donc le serveur partage le `BackendCore` desktop : [commands.rs](/home/anthony/Documents/Projects/IdeA/crates/app-tauri/src/commands.rs:135).\n- Pas de fuite constatée dans `domain` / `application`; le nouveau module reste adapter Tauri.\n- Persistance ciblée sur `/deployment/server-exposure.json`, écriture via temp + rename, permissions `0600` Unix best effort.\n- Le pairing code est uniquement dérivé du handle runtime dans le status, pas dans le DTO persisté.\n- Modes dérivés vers `ServerConfig` cohérents : `localOnly` loopback sans remote, `remoteProxyLocal` loopback + proxy, `remoteProxyOtherMachine` LAN + trusted proxies requis.\n- Commandes enregistrées dans [lib.rs](/home/anthony/Documents/Projects/IdeA/crates/app-tauri/src/lib.rs:288).\n\nCommandes relancées :\n```text\ncargo fmt --check\nOK\n\ngit diff --check -- crates/app-tauri/src/embedded_server.rs crates/app-tauri/src/state.rs crates/app-tauri/src/commands.rs crates/app-tauri/src/lib.rs\nOK\n\ncargo test -p app-tauri --no-run\nOK\n\ncargo test -p app-tauri embedded_server -- --nocapture --test-threads=1 --skip start_is_idempotent_and_stop_stops_running_server\n7 passed\n\ncargo test -p app-tauri -- --test-threads=1\nFAILED uniquement:\nembedded_server::tests::start_is_idempotent_and_stop_stops_running_server\nErr: failed to bind 127.0.0.1:0: Operation not permitted (os error 1)\n42 passed; 1 failed; 1 ignored\n```\n\nTests complémentaires à exécuter hors sandbox :\n```bash\ncargo test -p app-tauri embedded_server::tests::start_is_idempotent_and_stop_stops_running_server -- --nocapture --test-threads=1\ncargo test -p app-tauri -- --test-threads=1\ncargo test -p web-server run_embedded -- --nocapture --test-threads=1\n```\n\nOptionnel mais utile hors sandbox : vérifier que le fichier `deployment/server-exposure.json` créé par `store_round_trips_without_pairing_code` est bien en `0600` sur Unix.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "959c9d9a-9369-40bb-92c8-64e1e679e91c", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "af7f86da-76bc-48e1-9900-71f45a624800", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "af7f86da-76bc-48e1-9900-71f45a624800", + "ticket_id": "ffd71b0a-4751-45bb-9713-16b719e26d28", + "conversation_id": "09b15ec3-ce2f-0af2-2903-ef699df9de41" + }, + "state": "completed", + "wakePolicy": "recordOnly", + "createdAtMs": 1784237158377, + "updatedAtMs": 1784238011507, + "deadlineMs": null, + "result": { + "outcome": "success", + "finished_at_ms": 1784238011507, + "exit_code": null, + "summary": "Headless rendezvous completed with Final", + "stdout_tail": "Done. The Deployment panel is implemented and green — but **your cadrage has a gap that would have broken mode 3**, so read the discrepancies section first.\n\n## Verification (real output)\n\n```\n$ npm run typecheck\n> tsc --noEmit # clean, no errors\n\n$ npx vitest run\n Test Files 87 passed (87)\n Tests 789 passed (789)\n```\n\nBaseline was 774/85 with 2 failures. I added 14 tests, converted 1, and fixed the 2 pre-existing failures (both were contracts I deliberately changed: the gateway inventory, and the \"Close AI Profiles\" label you asked me to remove).\n\nPer the #69 carnet warning, I didn't trust jsdom for CSS that matters. I ran `npm run build` and grepped the bundle: `bg-warning/10` → `background-color:#d299221a` + a `color-mix` fallback, `border-warning/40`, `bg-success`, `size-2` all present as real rules. (`dist/` is gitignored; nothing left behind. Not committed — Git decides.)\n\n## Discrepancies between your brief and the real code\n\n**1. Mode 3 needs a third field you didn't list — this is the important one.** You specified `remoteProxyOtherMachine` as having \"Public origin **et** Authorized proxy IP/CIDR\". But `validate_settings` (`embedded_server.rs:354-364`) also *requires* `lanBindAddress`, and rejects loopback/unspecified. Built to your spec exactly, every save and start in mode 3 would fail with `remoteProxyOtherMachine requires lanBindAddress`. This is also precisely why `candidateLanAddresses` exists — I added a \"LAN address to bind\" select fed from the backend's candidates, so the rule \"the frontend never invents an IP\" still holds.\n\n**2. `preview` validates before previewing, which deadlocks the candidate list.** `preview()` calls `validate_settings` first (line 211). So a mode-3 draft can't be previewed until it already has a `lanBindAddress` — which the user needs the candidate list to choose. I broke the cycle with a separate always-valid `localOnly` probe, since the backend builds that list independent of mode. Upside: `preview` doubles as the UI's validation authority, returning the same message `start` would — which is what makes the errors actionable rather than just red.\n\n**3. Backend dead code:** the `missingTrustedProxy` warning (line 452) is unreachable — `validate_settings` rejects empty `trustedProxies` for mode 3 before `preview_settings` can build it. Worth a backend ticket; I mirrored it in the mock with a comment so the two stay aligned if it's reordered.\n\n**4. No status event exists.** You asked for `onStatusChanged`; the backend emits nothing. I kept the port shape and implemented it as polling *inside the Tauri adapter*, so the schedule stays a transport detail and a real event can replace it without touching the UI.\n\nI also corrected myself mid-review: I initially thought `save_settings` skipped validation, but `store.write` validates (line 153). It's fine.\n\n## Files touched\n\nNew: `features/settings/` (`SettingsView`, `DeploymentSettings`, `useDeployment`, `index.ts`, plus `DeploymentSettings.test.tsx` and `desktop-only.test.ts`), and `adapters/desktopServer.ts`.\n\nModified: `domain/index.ts` and `ports/index.ts` (DTOs + `DesktopServerGateway`), the three adapter registries (tauri/mock/http), `ProjectsView.tsx`, and two tests.\n\n## Notes on choices\n\n**Start persists the draft first.** `embedded_server_start` runs the *persisted* config, so starting without saving would run something other than what's on screen. Start now saves, then starts.\n\n**The web guard is real, not just an absence.** `features/web` never importing the settings surface is pinned by a test, and the web transport gets a `WebDesktopServerGateway` that rejects with `UNSUPPORTED_ON_WEB` — a web client is served *by* this server and shouldn't reconfigure it. I made those stubs `async` on purpose: the existing `unsupported.ts` classes throw *synchronously* from Promise-returning methods, which `.then(ok, err)` wouldn't catch. The pre-existing ones (`WebRemoteGateway`, `WebWindowGateway`) still have that shape — harmless today since callers `await`, but it's a latent trap worth a cleanup ticket.\n\nI did **not** rapatriate `EmbedderSettings`/`ModelServersPanel`; the section list is the seam they'd slot into.", + "stderr_tail": null + }, + "completionDelivered": false + }, + { + "id": "ce91e278-8bce-4a19-98ee-5c0d5e3333f0", + "projectId": "97b49ac2-8376-4aa3-8ea9-bf3ac81d0023", + "ownerAgentId": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "kind": { + "kind": "headlessRendezvous", + "requester_agent_id": "a6ced819-b893-4213-b003-9e9dc79b9641", + "target_agent_id": "cd0b4cf1-1bef-4fae-ade5-f0a6b49bbaf5", + "ticket_id": "e79249c2-3f08-4fb4-b917-c3d7d1f1ea40", + "conversation_id": "6bc594e8-a37c-0dbd-1de6-6e3b73002cb4" + }, + "state": "running", + "wakePolicy": "recordOnly", + "createdAtMs": 1784238075328, + "updatedAtMs": 1784238075328, "deadlineMs": null, "result": null, "completionDelivered": false diff --git a/.ideai/tickets/72/carnet.md b/.ideai/tickets/72/carnet.md index 86b0b69..b522600 100644 --- a/.ideai/tickets/72/carnet.md +++ b/.ideai/tickets/72/carnet.md @@ -1,6 +1,55 @@ --- issueRef: "#72" -version: 1 +version: 3 updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} -updatedAt: 1784218596269 +updatedAt: 1784236365445 --- +# Ticket #72 — Confiance reverse proxy : carnet de chantier + +Statut : **livré et mergé dans `develop` (`7fbaa8e`)** le 2026-07-16. +Commits : `75d1ce8` (code), `42804fc` (doc), `ae01297` (merge `--no-ff`). Branche supprimée. + +## Ce qui a été livré + +- `trusted_proxies` dans `ServerConfig` + flag CLI `--trusted-proxy` (répétable, IP ou CIDR). +- Refus **au démarrage** d'un bind non-loopback distant sans proxy autorisé. +- Guard runtime sur les **trois** surfaces — statique, `/api/*`, WebSocket : le `peer_addr` est vérifié **avant** toute lecture de header. +- `X-Forwarded-Proto: https` obligatoire en mode proxy. +- `X-Forwarded-For` n'autorise **jamais** rien — journalisé uniquement. +- **B0** : réconciliation du port effectif dans `run_server` (le chemin CLI standalone souffrait du même bug que l'embedded : `origin_allowed` comparait à `http://127.0.0.1:0`). +- Diagnostics : `UntrustedProxyPeer`, `ForwardedProtoRejected`, `ForwardedHostMismatch`. + +## Ajustement en cours de route — le hard reject `X-Forwarded-Host` a été RETIRÉ + +Cadré initialement comme un refus, il est devenu un **warning de diagnostic**. Raison (Architect) : redondant avec la vérification d'`Origin` en égalité stricte déjà appliquée à toutes les routes `/api/*`, et il **cassait la configuration par défaut de nginx**, qui n'envoie pas cet en-tête. Le vrai contrôle d'accès reste pairing/session + pair proxy autorisé. Gain trop faible pour la friction créée. + +Le repli de lecture accepte `X-Forwarded-Host`, `Forwarded host=` **ou** `Host`. + +## Vérification réelle (Main, hors sandbox) + +`cargo test -p web-server` → **66 passed / 0 failed**. Re-vérifié sur `develop` après merge : 66 · 41 (backend) · 35 (app-tauri), 0 échec. + +**Comportement exercé sur un serveur réellement lancé**, pas seulement en test : +- proxy autorisé + `X-Forwarded-Proto: https` + **host absent** (= nginx par défaut) → **passe** le guard, `forwardedHostMismatch` en diagnostic. +- host **différent** → passe également, warning seulement. +- `X-Forwarded-Proto` absent → **403**, message : « Configure the reverse proxy to send it; for nginx add: proxy_set_header X-Forwarded-Proto $scheme; ». + +## ⚠️ Piège d'environnement — un faux vert s'est produit ici, deux fois + +**DevBackend et QA sont tous deux bloqués par EPERM sur `TcpListener::bind`.** Un `cargo test -p web-server` sandboxé rend un vert qui ne prouve rien : sur #68 B1, un « 57 passed » sandboxé a masqué un **vrai bug produit** (port éphémère vs `origin_allowed`). Tout vert sur ce crate doit être produit **hors sandbox**. C'est consigné dans les messages de commit, pas seulement ici. + +## Revue de sécurité (Git, au-delà du vert) + +Guard câblé sur les trois surfaces, aucune route qui y échappe. Le chemin de production propage le vrai pair (`handle_tcp_connection` → `Some(peer_addr.ip())`). Le repli `peer_ip.unwrap_or(config.listen.ip())` n'est atteignable que depuis des appelants `#[cfg(test)]`. CIDR correct, y compris `prefix == 0` (qui aurait débordé au shift sans traitement à part). + +## Changement de comportement ASSUMÉ + +Le durcissement **casse volontairement les configurations existantes** : un bind non-loopback distant sans `--trusted-proxy` est désormais refusé au démarrage, avec un message qui dit quoi faire. Consigné dans l'historique git. + +## Origine du ticket + +Trouvé par **Git**, pas par une revue d'architecture : c'est lui qui a établi que `--trust-reverse-proxy` n'avait aucun effet runtime — il n'était lu que pour exiger sa propre présence. La trilogie déclarait une topologie sécurisée sans jamais la garantir. + +## Point ouvert, remonté par Git — à arbitrer par l'utilisateur + +**#73 (TLS intégré) rendrait une partie de #72 obsolète.** Si `idea-serve` termine lui-même TLS, la cérémonie proxy — `--trusted-proxy`, le guard, ces diagnostics — ne concerne plus que les déploiements qui gardent un proxy devant. Ce n'est pas du travail perdu (#72 corrige un vrai trou aujourd'hui, et B0 était un bug réel), mais l'ordre des deux mérite un regard. diff --git a/.ideai/tickets/72/issue.md b/.ideai/tickets/72/issue.md index 1187af4..d9b4a41 100644 --- a/.ideai/tickets/72/issue.md +++ b/.ideai/tickets/72/issue.md @@ -2,7 +2,7 @@ id: "0fba0a7f-8f04-460e-b81a-93790e10b066" number: 72 title: "Sécurité : donner un effet réel à la confiance reverse proxy (--trust-reverse-proxy est un drapeau creux)" -status: "open" +status: "closed" priority: "high" sprint: null links: [{"target":"#68","kind":"blocks"},{"target":"#71","kind":"relatesTo"},{"target":"#65","kind":"relatesTo"}] @@ -10,8 +10,8 @@ agentRefs: [] createdBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} updatedBy: {"kind":"agent","agent_id":"a6ced819-b893-4213-b003-9e9dc79b9641"} createdAt: 1784218596269 -updatedAt: 1784218596269 -version: 1 +updatedAt: 1784236365445 +version: 3 --- **Vulnérabilité de conception existante, dans le mode CLI d'aujourd'hui — indépendante de #68.** Trouvée par Git pendant la mise en place de #68, confirmée et cadrée par Architect. diff --git a/.ideai/tickets/index.json b/.ideai/tickets/index.json index 0cec201..47189e1 100644 --- a/.ideai/tickets/index.json +++ b/.ideai/tickets/index.json @@ -753,11 +753,11 @@ "issueRef": "#72", "path": "72", "title": "Sécurité : donner un effet réel à la confiance reverse proxy (--trust-reverse-proxy est un drapeau creux)", - "status": "open", + "status": "closed", "priority": "high", "sprint": null, "assignedAgentIds": [], - "updatedAt": 1784218596269 + "updatedAt": 1784236365445 }, { "issueRef": "#73",