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 .onion address 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

  1. Open Tor Browser.
  2. Navigate to http://abc123def456ghi789jkl012.onion (or the address returned above).
  3. 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 HashedControlPassword in torrc if you need Tor control access; never expose the control port without authentication.
  • Volume backups: The tor_data volume contains your .onion private key. Back it up securely if you want to preserve your address across redeployments.
  • Network isolation: Keep nginx listening only on 127.0.0.1 to ensure Tor is the only path to your service.
  • Logs: Monitor docker compose logs tor and docker compose logs web for 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.

me

My name is Adam Lichonvsky and I'm proud father and researcher.