TOR Onion Services
Onion Services (v3) — hosting on Tor
Onion Services allow a server to be reachable over Tor without revealing its network location. A service advertises introduction points and clients connect through rendezvous points; the server never exposes its IP address.
Basic torrc for an onion service
# enable a v3 onion service
HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServiceVersion 3
HiddenServicePort 443 127.0.0.1:8443
After restarting Tor, the file /var/lib/tor/hidden_service/hostname will contain your .onion address.
Example: Nginx + TLS inside an onion service (TLS bridging)
Run your web app locally (e.g., on 127.0.0.1:8080) and terminate TLS with nginx on 127.0.0.1:8443. Tor will forward incoming onion connections to nginx.
Nginx site (listen on localhost only):
server {
listen 127.0.0.1:8443 ssl;
server_name _;
ssl_certificate /etc/ssl/certs/example.crt;
ssl_certificate_key /etc/ssl/private/example.key;
access_log /var/log/nginx/onion_access.log;
error_log /var/log/nginx/onion_error.log;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
Notes:
- You can use a CA-signed certificate if you want browser validation; many operators use self-signed certs because onion authenticity is provided by the onion address itself.
- Do not bind nginx to 0.0.0.0 for the onion-facing listener; keep it on localhost so only Tor can reach it.
Verification and publishing
- Keep your
.onionaddress private until you’re ready to publish. When publishing, link the onion address from a trusted clearnet page and consider posting signed statements to verify authenticity. - Rotate keys by recreating the HiddenServiceDir (this changes the onion address).
Docker Compose Onion Service
A complete, production-ready onion service deployment using Docker Compose is available in the project repository at deployments/tor/.
Quick Start
cd deployments/tor
# Start the stack
docker compose up -d
# Wait a few seconds for Tor to initialize, then retrieve the .onion address
docker compose exec tor cat /var/lib/tor/hidden_service/hostname
Output example:
abc123def456ghi789jkl012.onion
Access the Service
- Open Tor Browser.
- Navigate to
http://abc123def456ghi789jkl012.onion(or the address returned above). - You should see the sample welcome page served by nginx through Tor.
Architecture
┌────────────────────────────────────┐
│ Tor Container │
│ • HiddenService v3 │
│ • Forwards port 80 → localhost:80 │
│ • tor_data volume (persistent) │
└────────────────────────────────────┘
│
▼
┌────────────────────────────────────┐
│ Nginx Container │
│ • Listens on 127.0.0.1:8080 │
│ • Serves sample HTML │
│ • Optional TLS on localhost │
└────────────────────────────────────┘
Configuration Files
torrc— Minimal Tor configuration with HiddenService v3 enabled.docker-compose.yml— Service orchestration with volumes and networking.nginx/default.conf— HTTP and optional HTTPS server config.html/index.html— Sample landing page.
Customization
Use Your Own Web Application
Replace the nginx container with your application:
web:
image: your-app:latest
# ... rest of your app config
networks:
- tor_net
Ensure your app listens on 127.0.0.1:8080 or update the Tor HiddenServicePort directive accordingly.
Add TLS to the Onion Service
Generate a self-signed certificate:
mkdir -p deployments/tor/certs
openssl req -x509 -newkey rsa:4096 -keyout deployments/tor/certs/server.key \
-out deployments/tor/certs/server.crt -days 365 -nodes -subj "/CN=localhost"
Update nginx/default.conf to include:
server {
listen 127.0.0.1:8443 ssl;
server_name _;
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
Update torrc:
HiddenServicePort 443 127.0.0.1:8443
Restart the stack:
docker compose down
docker compose up -d
Hardening & Production Notes
- Replace the Tor image: Use a maintained, security-hardened Tor Docker image instead of the default.
- Control port authentication: Set
HashedControlPasswordintorrcif you need Tor control access; never expose the control port without authentication. - Volume backups: The
tor_datavolume contains your.onionprivate key. Back it up securely if you want to preserve your address across redeployments. - Network isolation: Keep nginx listening only on
127.0.0.1to ensure Tor is the only path to your service. - Logs: Monitor
docker compose logs toranddocker compose logs webfor errors and anomalies.
Verification
Validate docker-compose syntax:
docker compose -f docker-compose.yml config
Test connectivity from inside the Tor container:
docker compose exec tor curl -s http://127.0.0.1:8080
Cleanup
To stop and remove the deployment:
docker compose down
# To also remove persistent data (tor_data volume):
docker compose down -v
For detailed setup and troubleshooting, see deployments/tor/README.md.