labfy-investigation/docs/architecture/PRIVACY_EGRESS.md

95 lines
4.6 KiB
Markdown

# Egress Internet confidentialité V1
**État : `CURRENT local` pour le contact passif via Tor rootless.** Le smoke
Tor live et le smoke Qwen opérationnel unifié ont réussi sur `SPECIMEN` avec
`TOR_EGRESS_VERIFIED`, `direct_fallback: false` et nettoyage du conteneur possédé.
Le profil produit est `PRIVACY_TOR`, jamais « anonymous guaranteed ». Tor réduit
l'exposition de l'adresse IP source mais ne garantit ni anonymat, ni innocuité du
contenu distant. Les pages, snippets, redirections et résultats restent
`UNTRUSTED_DATA` et ne créent ni fait validé, ni observation, ni autorisation.
## Runtime possédé
`PrivacyEgressSupervisor.start_owned_tor()` démarre un `PodmanTorRuntime` et en
possède le cycle de vie. Les états sont `STOPPED`, `STARTING`, `BOOTSTRAPPING`,
`READY` et `FAILED`. `READY` n'est atteint qu'après :
1. preuve que Podman fonctionne en mode rootless ;
2. démarrage d'un conteneur portant un nom aléatoire et un label de propriété ;
3. message Tor `Bootstrapped 100% (done)` ;
4. découverte d'un port SOCKS éphémère publié exactement sur `127.0.0.1` ;
5. requête bornée vers `https://check.torproject.org/api/ip` par SOCKS5h et
réponse `IsTor: true`.
L'adresse éventuellement contenue dans cette réponse de contrôle n'est ni
retournée, ni journalisée, ni placée dans la provenance. Une erreur à l'une de
ces étapes produit `FAILED`; aucun transport direct n'est alors créé.
Le nettoyage inspecte le label avant `podman rm --force`. Un conteneur absent ou
portant un autre label n'est jamais supprimé. Le runtime ne recherche et ne
nettoie aucun conteneur par préfixe global.
## Image et confinement
L'image locale `localhost/labfy-privacy-tor:1` est construite depuis
`docker.io/library/debian:12.12-slim`, base officielle versionnée, par
`prototypes/web-graph/privacy-egress/Containerfile`. Elle installe la version
Debian explicitement fixée de Tor et s'exécute sous `debian-tor`, avec UID/GID
non-root déterministes `10001:10001`.
Le lancement n'utilise ni réseau hôte, ni mode privilégié, ni montage hôte. Il
applique un filesystem racine en lecture seule, `cap-drop ALL`,
`no-new-privileges`, des limites mémoire/PID et seulement deux `tmpfs` privés
bornés pour `/run/tor` et `/var/lib/tor`. Le SOCKS interne écoute dans le réseau
du conteneur ; Podman publie `127.0.0.1::9050`, donc choisit un port hôte
éphémère. La configuration Tor active `SafeSocks`, `TestSocks`,
`ClientRejectInternalAddresses` et `ClientDNSRejectInternalAddresses`.
## Transport et refus
`CurlSocksTransport` utilise exclusivement `socks5h://127.0.0.1:<port>` : la
résolution DNS a lieu via Tor et ne fuit pas vers le résolveur de l'hôte. Il
limite les protocoles à HTTP(S), désactive les redirections automatiques de
curl, borne connexion, durée, en-têtes et corps, et ne lance jamais de shell.
Chaque redirection est revalidée par `PrivacyEgressSupervisor` avant une nouvelle
requête.
Seuls `GET` et `HEAD` sont permis. Sont refusés : userinfo, fragments, schémas
non HTTP, localhost, loopback, RFC1918, link-local, multicast, réservé, non
spécifié et noms metadata connus. Tor refuse en plus les résolutions distantes
vers les adresses internes. Une réponse trop grande est supprimée et rend
`RESPONSE_LIMIT`. Tor absent, arrêté ou non validé rend
`PRIVACY_EGRESS_UNAVAILABLE` ou un échec de transport avec
`direct_fallback: false`.
`InternetResearch.search()` refuse également d'appeler un provider lorsque
l'egress `PRIVACY_TOR` est indisponible. Le provider reçoit le superviseur
Privacy comme dépendance obligatoire ; son contrat interdit tout transport
réseau parallèle.
La provenance `labfy.privacy_egress.v1` contient mode, raison et chaîne de
redirections bornée. Pour SOCKS5h, le pair est noté `privacy-proxy`; aucune
adresse de sortie Tor n'est exposée.
## Validation
Les tests unitaires utilisent seulement des fakes `SPECIMEN` et couvrent la
propriété, la commande Podman, l'indisponibilité sans fallback, SSRF,
redirections et dépassement de taille :
```bash
python3 -m unittest prototypes/web-graph/tests/test_privacy_egress.py \
prototypes/web-graph/tests/test_privacy_tor_runtime.py \
prototypes/web-graph/tests/test_internet_research.py
```
Le smoke réel construit l'image, contacte uniquement les endpoints bénins
ci-dessus et ferme dans `finally` seulement le conteneur possédé :
```bash
python3 prototypes/web-graph/tests/manual_privacy_tor_smoke.py
```
`--skip-build` réutilise explicitement l'image locale. Un échec Podman, build,
réseau ou bootstrap reste un échec ; il ne doit jamais être transformé en PASS.