.

Configurer Nginx pour servir un service .onion

Un service Onion ne publie pas directement Nginx sur Internet. Tor reçoit les connexions du réseau Onion, puis les transmet à un port local sur lequel Nginx écoute.

Tor Browser → réseau Tor → service Onion → 127.0.0.1:8081 → Nginx

Le point essentiel est de ne pas exposer le port interne sur le réseau local : Nginx doit écouter explicitement sur 127.0.0.1, et non sur toutes les interfaces.

Préparer le contenu public

Créons un répertoire qui ne contiendra que les fichiers destinés aux visiteurs :

sudo mkdir -p /var/www/my-onion-site
sudo chown -R www-data:www-data /var/www/my-onion-site
sudo chmod -R 0755 /var/www/my-onion-site

Le compte utilisé par Nginx varie selon le système. Il faut donc vérifier le nom de l’utilisateur et du groupe avant d’appliquer ces commandes. Ne placez jamais dans cette racine une configuration Tor, une clé privée, un mot de passe, une sauvegarde ou un journal.

Le virtual host Nginx

L’exemple suivant sert un site statique. Remplacez l’adresse composée de x par la véritable adresse Onion v3, sans http:// et sans barre oblique finale.

server {
    listen 127.0.0.1:8081;
    server_name xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion;

    root /var/www/my-onion-site;
    index index.html;

    server_tokens off;
    autoindex off;
    client_max_body_size 1m;

    access_log /var/log/nginx/my-onion-site-access.log onion_min;
    error_log  /var/log/nginx/my-onion-site-error.log warn;

    add_header Content-Security-Policy "default-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'" always;
    add_header Referrer-Policy "no-referrer" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;

    limit_req zone=onion_site burst=40 nodelay;
    limit_req_status 429;

    location / {
        limit_except GET HEAD {
            deny all;
        }

        try_files $uri $uri/ =404;
    }

    location ~ /\. {
        deny all;
    }

    location /assets/ {
        expires 7d;
        try_files $uri =404;
    }
}

server

Ouvre un virtual host. Une seule instance Nginx peut héberger plusieurs services Onion, chacun dans son propre bloc server.

listen 127.0.0.1:8081

Limite l’écoute à l’interface locale IPv4 et au port interne 8081. Écrire seulement listen 8081 risquerait d’exposer ce port sur les interfaces réseau. Les visiteurs utiliseront malgré tout le port virtuel 80 du service Onion.

server_name

Associe ce virtual host à l’adresse reçue dans l’en-tête HTTP Host. Nginx sélectionne d’abord l’adresse et le port d’écoute, puis le nom du serveur. Si aucun nom ne correspond, il utilise le serveur par défaut de ce port. C’est pourquoi nous ajouterons aussi un bloc de rejet. Voir comment Nginx traite une requête.

root

Définit la racine publique. Pour /images/photo.jpg, Nginx cherchera normalement /var/www/my-onion-site/images/photo.jpg.

index index.html

Indique le fichier à retourner lorsqu’une URL correspond à un répertoire. Une requête sur / pourra ainsi servir index.html.

server_tokens off

Masque le numéro de version de Nginx dans ses pages d’erreur et dans l’en-tête Server. L’édition libre peut toutefois conserver l’identifiant générique Server: nginx : cette directive réduit la précision de l’empreinte sans la faire disparaître. Voir la directive server_tokens.

autoindex off

Empêche la génération automatique d’une liste de fichiers lorsqu’un répertoire ne possède pas de fichier d’index. Une galerie publique doit être générée explicitement par une application contrôlée.

client_max_body_size 1m

Limite le corps d’une requête à 1 Mio. C’est suffisant pour un site statique, mais une application d’upload devra choisir une limite adaptée et ajouter quotas, isolation, validation des formats et analyse antivirus.

access_log

Écrit les accès dans un journal utilisant ici le format minimal onion_min. Nginx voit normalement Tor comme origine locale, mais les URL, référents et agents utilisateurs peuvent malgré tout être sensibles. On peut désactiver ce journal avec access_log off. Voir le module de journalisation Nginx.

error_log ... warn

Définit un journal d’erreurs propre au site et conserve les événements de niveau warn ou supérieur, sans activer les traces très détaillées de débogage.

Les quatre directives add_header

Content-Security-Policy limite les ressources à la même origine, interdit les objets anciens, la modification de l’URL de base et l’affichage du site dans une frame. Cette politique devra être adaptée si le site charge des ressources externes.

Referrer-Policy "no-referrer" empêche le navigateur de transmettre l’adresse de la page d’origine lorsqu’un lien est suivi.

X-Content-Type-Options "nosniff" demande au navigateur de respecter le type MIME annoncé au lieu de deviner le contenu.

Permissions-Policy interdit ici caméra, microphone et géolocalisation. Il faut retirer une interdiction si l’application utilise réellement la fonction.

Le paramètre always applique ces en-têtes également aux réponses d’erreur. Sans lui, add_header ne concerne que certains codes de réponse. Voir le module officiel des en-têtes.

limit_req

Applique la zone de limitation onion_site. burst=40 accepte une courte pointe de 40 requêtes et nodelay traite immédiatement les requêtes admises dans cette pointe.

Tor transmettant les connexions depuis l’hôte local, $remote_addr ne permet pas de distinguer correctement les visiteurs. La limite présentée ici est donc volontairement globale par site. Une limite trop basse pourrait faciliter un déni de service en permettant à un attaquant d’épuiser le quota commun. Voir le module limit_req.

limit_req_status 429

Retourne 429 Too Many Requests pour une requête refusée par la limitation. La valeur par défaut de Nginx est 503.

location /

Définit le traitement général des URL du site.

limit_except GET HEAD et deny all

N’autorise que la lecture d’une ressource avec GET et la lecture de ses seuls en-têtes avec HEAD. Cette restriction ne convient pas à une application qui utilise POST, une API ou des uploads.

try_files $uri $uri/ =404

Cherche successivement le fichier demandé, puis le répertoire correspondant, et retourne 404 si aucun n’existe. Cette directive ne sert donc que des ressources présentes sous root. Voir la documentation de try_files.

location ~ /\. et deny all

Interdit les chemins contenant un élément caché comme .git, .env ou .htaccess. Ces fichiers ne devraient jamais être placés dans la racine publique, mais ce bloc apporte une protection supplémentaire.

location /assets/, expires et try_files

Ce bloc traite les ressources statiques placées sous /assets/. expires 7d permet au navigateur de les conserver sept jours, ce qui réduit les transferts sur Tor. Une longue durée convient surtout aux fichiers versionnés dont le nom change avec le contenu. try_files $uri =404 retourne immédiatement 404 si la ressource n’existe pas. Voir la directive expires.

Les directives communes du contexte http

Certaines directives ne sont pas autorisées dans un bloc server. Ajoutez-les au bloc http existant de Nginx : il ne faut pas créer un second bloc http.

http {
    server_tokens off;

    log_format onion_min
        '$time_iso8601 $request_method $status '
        '$body_bytes_sent $request_time';

    limit_req_zone $server_name
        zone=onion_site:1m
        rate=20r/s;

    gzip on;
    gzip_min_length 1024;
    gzip_vary on;
    gzip_types
        text/css
        application/javascript
        application/json
        image/svg+xml;

    include /etc/nginx/conf.d/*.conf;
}

log_format onion_min

Crée un format qui conserve la date ($time_iso8601), la méthode HTTP, le code de réponse, le nombre d’octets envoyés et la durée de traitement. Il omet volontairement l’adresse cliente, l’URL, les paramètres, le référent et l’agent utilisateur. log_format doit être placé dans le contexte http.

limit_req_zone

Crée une zone de mémoire partagée nommée onion_site. $server_name constitue la clé, 1m réserve 1 Mio et 20r/s fixe une moyenne de 20 requêtes par seconde. Il s’agit d’une limite globale par virtual host, pas d’une limite fiable par visiteur Tor.

gzip on

Active la compression dynamique. Elle est particulièrement utile sur Tor pour HTML, CSS, JavaScript, JSON et SVG.

gzip_min_length 1024

Évite de compresser les réponses inférieures à 1 Kio, pour lesquelles le coût de compression peut être disproportionné.

gzip_vary on

Ajoute Vary: Accept-Encoding lorsque nécessaire afin que les caches distinguent les réponses compressées et non compressées.

gzip_types

Ajoute les types MIME à compresser. text/html est déjà traité spécialement par Nginx. JPEG, PNG, WebP, ZIP et vidéos sont déjà compressés et ne devraient généralement pas être recompressés. Cette configuration dynamique ne nécessite pas de placer des fichiers .gz publics dans la racine du site.

include

Charge les fragments correspondant au motif, ce qui permet de conserver un fichier par service. Le chemin dépend de l’installation : Debian, Homebrew et d’autres systèmes n’utilisent pas nécessairement la même arborescence.

Rejeter les noms de serveur inattendus

Ajoutez un virtual host par défaut sur le même port :

server {
    listen 127.0.0.1:8081 default_server;
    server_name "";
    access_log off;
    return 444;
}

default_server reçoit les requêtes dont le nom ne correspond à aucun autre virtual host de ce port. server_name "" cible notamment les requêtes sans en-tête Host. access_log off évite de journaliser ces rejets. return 444 ferme la connexion sans réponse HTTP ; 444 est un code propre à Nginx.

Configurer le service Onion dans Tor

Ajoutez ces deux lignes à torrc ou à un fragment inclus :

HiddenServiceDir /var/lib/tor/my-onion-site/
HiddenServicePort 80 127.0.0.1:8081

HiddenServiceDir

Définit le répertoire d’identité du service. Tor y crée notamment hostname, qui contient l’adresse .onion, et les clés cryptographiques qui prouvent son identité. Chaque service doit avoir son propre répertoire.

Les clés doivent rester privées : une personne qui les obtient peut usurper le service. Ce répertoire canonique ne doit pas être déplacé, copié ou sauvegardé comme une simple configuration. Voir le guide officiel des Onion Services.

HiddenServicePort

Fait correspondre le port virtuel 80, visible par les visiteurs, à la destination locale 127.0.0.1:8081. Cette directive n’ouvre pas le port 80 sur l’interface réseau de la machine.

Une instance Tor peut héberger plusieurs services avec plusieurs HiddenServiceDir. Plusieurs HiddenServicePort peuvent également appartenir au même service ; ils s’appliquent au dernier HiddenServiceDir déclaré.

Variante avec un socket Unix

Le projet Tor recommande un socket Unix pour réduire encore le risque d’exposition accidentelle sur le réseau local :

HiddenServiceDir /var/lib/tor/my-onion-site/
HiddenServicePort 80 unix:/run/tor/my-onion-site.sock

Nginx écoute alors sur le même socket :

server {
    listen unix:/run/tor/my-onion-site.sock;
    server_name xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion;
    root /var/www/my-onion-site;
    index index.html;
}

Cette variante exige une coordination précise des propriétaires, groupes et permissions. La liaison TCP sur 127.0.0.1 reste plus simple à diagnostiquer et n’est pas exposée au réseau lorsqu’elle est correctement liée à la boucle locale.

Vérifier avant de recharger

Ne rechargez jamais une configuration invalide.

sudo nginx -t
sudo tor --verify-config -f /etc/tor/torrc

Les chemins peuvent différer avec Homebrew ou une installation personnalisée. Après validation, utilisez le gestionnaire de services du système. Préférez un rechargement gracieux à un redémarrage lorsqu’il est pris en charge, afin de ne pas interrompre les autres sites hébergés par les mêmes processus.

Testez d’abord Nginx localement :

curl \
  -H 'Host: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx.onion' \
  http://127.0.0.1:8081/

Testez ensuite l’adresse dans Tor Browser. Enfin, depuis une autre machine du réseau local, vérifiez que le port interne 8081 n’est pas accessible.

Publier une application locale

Pour une application écoutant sur 127.0.0.1:9000, remplacez le traitement statique de location / par :

location / {
    proxy_pass http://127.0.0.1:9000;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Connection "";
}

proxy_pass transmet la requête à l’application locale, qui doit elle aussi rester liée à 127.0.0.1 ou à un socket Unix. proxy_http_version 1.1 favorise la réutilisation des connexions. proxy_set_header Host transmet l’adresse Onion demandée, X-Forwarded-Proto indique le protocole reçu par Nginx et Connection "" retire l’en-tête propre au saut précédent.

L’application doit valider l’en-tête Host et ne pas lui faire confiance pour construire sans contrôle des liens, redirections ou décisions de sécurité.

Conclusion

Une configuration Onion robuste repose sur des frontières simples : Tor possède l’identité Onion, Nginx écoute uniquement localement, chaque site a son virtual host, le répertoire public ne contient aucun secret, les journaux sont minimisés et toutes les configurations sont validées avant rechargement.

Une seule instance Tor et une seule instance Nginx peuvent héberger plusieurs services Onion. Une isolation par processus, conteneur ou machine virtuelle est surtout pertinente lorsque les sites exécutent du code non fiable ou ont des niveaux de confiance différents.

Sources