SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

Add an Onion-Location header to your site

Publish an Onion-Location header in nginx so Tor Browser offers your onion address, then close the leaks that undo it: redirects and third-party assets.

What the Onion-Location header does

The Onion-Location header is one line in your clearnet vhost that advertises your onion address to Tor Browser. A visitor who reaches https://example.com over Tor sees a purple pill in the address bar reading .onion available, and one click moves them to your onion service. It is a discovery mechanism and nothing else. The header does not create the onion service, and it does not hide anything about you.

This guide assumes both halves already exist. You have a site on a VPS behind nginx, and you have a working v3 onion service pointing at it. If you do not have the second half yet, build it first: hosting an onion site on a VPS covers the torrc lines and the first hostname file. What follows is about joining the two without leaking one into the other.

What Tor Browser requires before it honours the header

The Tor Project documents three conditions. All three must hold or the pill never appears.

  • The Onion-Location value must be a valid URL with an http: or https: scheme and a .onion hostname.
  • The webpage defining the header must be served over HTTPS.
  • The webpage defining the header must not itself be an onion site.

The second condition is the one that catches people, and the third explains why you never set this header on the onion vhost. There is a fourth rule that is not in the prose docs but is in the implementation: Tor Browser only acts on the header for the top-level document. The code compares the load target against the document before doing anything, so a header returned on a stylesheet, an image or an API response is ignored.

By default the browser shows the pill and waits for a click. A reader who wants an automatic jump enables it under Settings, then Privacy and Security, then Onion Services, where "Prioritize .onion sites when known" can be set to "Always". You cannot force that from the server side. Treat the header as an offer, not a redirect.

Add the Onion-Location header in nginx

The header belongs in the server block that terminates TLS for your clearnet domain. Put it in the port 80 block instead and nothing happens, because that block only issues a redirect and requirement two rules out a header defined on a plain HTTP page.

server {
    listen 443 ssl;
    server_name example.com;

    ssl_certificate     /etc/letsencrypt/live/example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;

    add_header Onion-Location http://<your-onion-address>.onion$request_uri always;

    root /srv/example.com/public;
}

$request_uri carries the path and the query string, so a reader on https://example.com/guides/tor is offered the same path on the onion. Leave it out and every visitor lands on the onion home page instead of the page they were reading.

always matters because of a documented limit in nginx. add_header adds the field only when the response code is 200, 201, 204, 206, 301, 302, 303, 304, 307 or 308. Your 404 page is a real entry point from search results, and without always it carries no header at all.

The second nginx trap is inheritance, and it fails silently. add_header directives are inherited from the previous configuration level only if there are no add_header directives at the current level. So a location /assets/ { add_header Cache-Control ...; } block discards the server-level Onion-Location for every URL under it. If you set per-location headers anywhere, repeat the Onion-Location line inside each of those blocks. How nginx picks a server and location block is worth one read if that behaviour is new to you.

Reload and check both a normal page and a missing one:

sudo nginx -t && sudo systemctl reload nginx
curl -sI https://example.com/ | grep -i onion-location
curl -sI https://example.com/no-such-page | grep -i onion-location

Both commands should print an onion-location: line. The second one is the proof that always is working. No output from the second means the flag is missing or a location block is shadowing the directive.

The HTML meta tag when you cannot set headers

Static hosts and some CDN dashboards will not let you add an arbitrary response header. The same value works as a meta element in the document head, because the browser reads it through the same document header data whether it arrived over HTTP or as an http-equiv tag.

<meta http-equiv="onion-location" content="http://<your-onion-address>.onion" />

The three requirements still apply. The page carrying the tag must be HTTPS and must not be an onion. The difference is that the tag holds one fixed address with no path, because there is no server-side variable to expand. Every page that carries it offers the onion home page. That is the cost of the fallback, so prefer the header whenever you control the server.

Serve the onion from its own nginx vhost

The clearnet site and the onion must not share a server block. Tor Browser sends Host: <your-onion-address>.onion. If no server block claims that name, nginx falls back to the default server, which is your clearnet vhost, and every URL that vhost generates names your domain.

Point the hidden service at a port that only the loopback interface answers on:

HiddenServiceDir /var/lib/tor/onion_site/
HiddenServicePort 80 127.0.0.1:8080

Then give that port its own vhost:

server {
    listen 127.0.0.1:8080;
    server_name <your-onion-address>.onion;

    absolute_redirect off;
    port_in_redirect off;

    root /srv/example.com/public;
}

listen 127.0.0.1:8080 keeps this vhost off your public IP, so someone scanning the VPS address cannot fetch it and compare it byte for byte with the clearnet copy. absolute_redirect off makes nginx issue relative Location values, so the trailing-slash redirect on a directory returns Location: /guides/ and not a full URL. nginx already builds absolute redirects from the Host header rather than from server_name, because server_name_in_redirect defaults to off, but a relative redirect removes the question entirely.

Why does the onion page still send visitors to the clearnet site?

nginx is rarely the leak. Your application is. Anything that builds an absolute URL from a configured site address will name your domain no matter which vhost served the request.

  • A rel="canonical" link tag pointing at https://example.com/.... This is the most common one, and it names the exact clearnet page to anyone who views source.
  • Redirects generated by the framework instead of by nginx, such as Django's SECURE_SSL_REDIRECT or WordPress's home and siteurl options.
  • og:url and the other social card meta tags.
  • Sitemap and RSS entries, which are absolute by specification.
  • Application error pages, which usually include a "back to the home page" link built from the same setting.

The fix depends on your stack and there is no generic one. The check is generic. Fetch the onion page through Tor and search the response for your domain.

curl -s --socks5-hostname 127.0.0.1:9050 http://<your-onion-address>.onion/ \
  | grep -i 'example\.com'

--socks5-hostname sends the name to tor's SOCKS port for resolution, which is required because nothing on your machine can resolve a .onion name locally. Port 9050 is the default for a packaged tor daemon. An empty result is the pass. Any hit is a page that hands your clearnet domain to every onion visitor. Run it against the home page, then against a URL that produces a 404.

Check the redirect chain separately, because a redirect body is usually empty:

curl -sI --socks5-hostname 127.0.0.1:9050 http://<your-onion-address>.onion/guides \
  | grep -i '^location'

A Location value naming example.com means a redirect is pushing your onion visitor back onto the clearnet, through an exit node, on a request they believed stayed inside Tor.

Do not present your clearnet certificate on the onion

A v3 onion address is derived from the service's own public key, so Tor authenticates and encrypts the circuit to that specific service before any HTTP request is sent. Plain HTTP inside an onion service is the normal configuration, and it is not the same thing as plain HTTP across the internet.

If you build the onion vhost by copying the clearnet one, you copy ssl_certificate with it. Now the onion presents a certificate whose subject alternative names list example.com. Two things go wrong. The browser shows a name mismatch, because the URL is the onion address and the certificate does not cover it. And every visitor who clicks through receives a signed statement that these two sites are the same machine. Keep the onion vhost in its own file with its own server_name, which also keeps it clear of the Certbot nginx plugin, since that plugin edits the server block matching the domain you request a certificate for.

Which tor package, and keeping the service key safe

The tor package in the Ubuntu archive works for this and needs no extra setup. It lags the current stable series, so for a service you plan to keep running, use the Tor Project's own Debian repository and let apt upgrade it along with everything else. As of August 2026 the documented steps are these:

sudo apt install apt-transport-https
wget -qO- https://deb.torproject.org/torproject.org/A3C4F0F979CAA22CDBA8F512EE8CBC9E886DDD89.asc \
  | gpg --dearmor \
  | sudo tee /usr/share/keyrings/deb.torproject.org-keyring.gpg >/dev/null

Write /etc/apt/sources.list.d/tor.sources, replacing the suite with your release codename from lsb_release -c:

Types: deb deb-src
URIs: https://deb.torproject.org/torproject.org/
Suites: noble
Components: main
Signed-By: /usr/share/keyrings/deb.torproject.org-keyring.gpg
sudo apt update
sudo apt install tor deb.torproject.org-keyring

The deb.torproject.org-keyring package keeps the signing key current, so the repository does not stop verifying a year from now. Pick one source and stay on it. The archive package and the repository package carry different versions, and apt will move you between them on upgrades if both are enabled.

The HiddenServiceDir holds your service identity. The hs_ed25519_secret_key file in that directory is your onion address, because the address is the public half of that key pair. Lose the file and the address is gone permanently, since there is no authority that can reissue it. Copy the file somewhere careless and whoever holds the copy can run your onion service.

tor refuses to use a directory that other users can read. Check the mode and the owner before you check anything else:

sudo ls -ld /var/lib/tor/onion_site
sudo -u debian-tor cat /var/lib/tor/onion_site/hostname

The listing should read drwx------ with owner and group debian-tor on Debian and Ubuntu. If it is wider, tor logs a line like Permissions on directory /var/lib/tor/onion_site/ are too permissive. and the service does not come up. Repair it with sudo chown -R debian-tor:debian-tor /var/lib/tor/onion_site followed by sudo chmod 700 /var/lib/tor/onion_site, restart with sudo systemctl restart tor, and read the outcome with sudo journalctl -u tor@default -n 30.

Back that directory up the way you back up a private key: off the box and encrypted. Never commit it to the repository that holds your site. If the same box also needs administrative access over Tor, reaching SSH through an onion service is a cleaner separation than exposing a management path on the public site.

Analytics and third-party assets leak more than the header

This is the part that matters most, and it has nothing to do with Onion-Location. Every third-party asset your page references is a request the visitor's browser makes out of the onion and back onto the clearnet through an exit node. A font from a public CDN, a hosted analytics script, an embedded video player, a comment widget: each one tells that third party that somebody is loading your page, on a session the reader deliberately routed through Tor.

There are two consequences. The third party learns about the visit. And because your clearnet copy loads the same assets from the same providers, anyone with a view of either side can associate the two properties without any effort.

Serve everything from the same origin. Self-host your fonts. Drop the hosted analytics tag, or move it onto your own box, where self-hosted analytics on a VPS keeps the request inside the onion. Expect Tor Browser's defaults to block or flatten a lot of what any analytics tool tries to collect, which is the correct outcome. If a page cannot function without a third-party script, do not publish that page on the onion.

List what a page actually pulls:

curl -s --socks5-hostname 127.0.0.1:9050 http://<your-onion-address>.onion/ \
  | grep -oE '(src|href)="https?://[^"]+"' | sort -u

Every line this prints is an absolute URL your page asks the browser to fetch. Anything that is not your own onion address is an outbound clearnet request you are asking your readers to make on your behalf.

The threat model, stated plainly

Onion-Location makes an onion service easy to find, and that is all it does. It does not anonymise you as the operator, because your clearnet domain still carries registrar records, DNS records, a certificate published in the Certificate Transparency logs, and a VPS account with your billing details behind it. It does not anonymise the onion either, because you have just published, from that clearnet domain, a durable public statement that the two addresses are the same site. The benefit goes to the reader: someone who arrives over Tor can stay inside Tor, with no exit node in the path and no DNS lookup for your domain. If your goal is an onion service nobody can connect to you, do not publish this header, and do not run the two copies on one machine.

Two related questions come up here and both have their own answers. The difference between Tor and a VPN decides what you use for your own traffic, which is a separate decision from what you publish. And if readers in a censored network cannot reach the clearnet site at all, they never see the header, which is where bridges and pluggable transports matter more than anything on this page.

Verify the whole setup once

Run these in order. Each one has an answer you can see.

  1. curl -sI https://example.com/ | grep -i onion-location prints the header.
  2. The same command against a URL that 404s prints it too.
  3. curl -s --socks5-hostname 127.0.0.1:9050 http://<your-onion-address>.onion/ returns your page.
  4. Grepping that output for your clearnet domain returns nothing.
  5. Tor Browser on https://example.com shows the .onion available pill.

If steps 1 through 4 pass and step 5 does not, the cause is almost always where the header is served from rather than the header itself. Confirm the browser really loaded the HTTPS page and not a cached redirect, then run curl -sI against the exact URL you opened, because a location block on that specific path may be discarding the server-level directive.

FAQ

Why does Tor Browser not show the ".onion available" pill?

Check the three documented requirements first. The value must be a full URL with an http: or https: scheme and a .onion host, so a bare address with no scheme fails and prints no error. The page must be served over HTTPS, so a header set on your port 80 redirect block is never read. And the page must not itself be an onion. After that, look at nginx: any add_header inside the matching location block discards every add_header from the server level, and without the always flag the header is absent from 404 and 500 responses. Run curl -sI against the exact URL you loaded in the browser and confirm the header is actually on the wire.

Do I need a TLS certificate for my onion site?

No. A v3 onion address is derived from the service's public key, so the circuit is authenticated to that specific service and encrypted end to end before any HTTP request is sent. Plain HTTP inside an onion service is the normal configuration. What you must avoid is presenting your clearnet certificate on the onion. Its subject alternative names list your domain, which triggers a name mismatch warning in the browser and confirms to every visitor that the two sites run on one machine.

Does publishing Onion-Location make my site anonymous?

No. The header is a public statement from your clearnet domain that a particular onion address belongs to you, and anyone can fetch it. The gain belongs to the reader, who can move into the onion and remove the exit node and the DNS lookup from their path. As the operator you gain no anonymity, and you permanently link the two addresses. An onion service that must not be traceable to you should be published somewhere else, on hardware that shares nothing with the clearnet site.

Can I use the meta tag instead of the HTTP header?

Yes, when you cannot set response headers, which is the usual situation on a static host. Put <meta http-equiv="onion-location" content="http://youraddress.onion" /> in the document head. The same three requirements apply, so the page must be HTTPS and must not be an onion. The one real difference is that the tag holds a fixed address with no path, while the nginx header can append $request_uri and offer the visitor the same page on the onion rather than the home page.