I was recently asked how I setup my websites to:
- Redirect HTTP to HTTPS when not accessed via an onion service.
- Serve the website over HTTPS when not accessed via an onion service.
- Serve the website over HTTP when accessed via an onion service.
I will further explain:
- How the .onion available button is obtained in my setup.
- How to add an onion
Alt-Svc
that works.
I have a very simple setup. I have a tor daemon running on the same machine as nginx. As most of my websites are static, nginx serves their files directly in most cases. There is no software between tor and nginx; if there is for you, that drastically changes things and this post may be of little use to you. If you have extra software "behind" nginx (e.g. a python app generating a dynamic website), most likely this post will still be useful to you. For example, instead of telling nginx this like I do:
location / {
try_files $uri $uri/ =404;
}
You might be telling nginx this:
location / {
include proxy_params;
proxy_pass http://unix:/path/to/some/app.sock;
}
I use Certbot (Let's Encrypt) for my CA, and it automatically generates some of the nginx config you will see below.
All of the nginx config blocks are in one file, /etc/nginx/sites-available/flashflow.pastly.xyz
.
As is standard with nginx on Debian, there's a symlink to that file in /etc/nginx/sites-enabled/
and /etc/nginx/nginx.conf
was already set to load files in /etc/nginx/sites-enabled/
.
This post uses flashflow.pastly.xyz and its onion address as an example. Whenever you see flashflow.pastly.xyz or its onion address, mentally replace the domains with your own.
Redirect HTTP to HTTPS when not accessed via an onion service.
This is entirely handled by nginx and uses a server {}
block automatically
generated by Certbot. It is this:
server {
if ($host = flashflow.pastly.xyz) {
return 301 https://$host$request_uri;
} # managed by Certbot
listen 80;
listen [::]:80;
server_name flashflow.pastly.xyz;
return 404; # managed by Certbot
}
All this block does is redirect to HTTPS. It is used when the user is visiting
flashflow.pastly.xyz
on port 80, as indicated by the server_name
and
listen
lines.
Serve the website over HTTPS when not accessed via an onion service.
This is entirely handled by nginx. Again as the server_name
and listen
lines indicate, this block is used when the user is visiting
flashflow.pastly.xyz
on port 443 (using TLS). This is overwhelmingly
automatically generated by Certbot too.
I slightly simplified this block as presented here. We will edit this block
later in this post to add Onion-Location
and Alt-Svc
headers.
server {
server_name flashflow.pastly.xyz;
root /var/www/flashflow.pastly.xyz;
index index.html;
location / {
try_files $uri $uri/ =404;
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
ssl_certificate /etc/letsencrypt/live/flashflow.pastly.xyz/fullchain.pem; # managed by Certbot
ssl_certificate_key /etc/letsencrypt/live/flashflow.pastly.xyz/privkey.pem; # managed by Certbot
include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
}
Serve the website over HTTP when accessed via an onion service.
This is the nginx config block. It is a simplified version of the previous one, as it is also actually serving the website, but with plain HTTP and when the user is visiting the onion service, not flashflow.pastly.xyz.
server {
listen 80;
listen [::]:80;
server_name jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion;
root /var/www/flashflow.pastly.xyz;
index index.html;
location / {
try_files $uri $uri/ =404;
}
}
These are the relevant lines from the tor's torrc.
We will edit this block later in this post to add Alt-Svc
support.
HiddenServiceDir /var/lib/tor/flashflow.pastly.xyz_service
HiddenServicePort 80
In this post I've shared two server {}
blocks that tell nginx to listen on port 80. Nginx
knows to use this block for onion service connections because the
server_name
(the hostname that the user's browser is telling nginx it wants
to visit) is the onion service. Nginx uses the other server {}
block
with port 80 when the user's browser tells nginx that it wants to visit
flashflow.pastly.xyz.
After adding those lines to the torrc, I reloaded tor (restart not required). Then I could learn what the onion address is:
$ cat /var/lib/tor/flashflow.pastly.xyz_service/hostname
jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion
And from there knew what to put on the server_name
line.
Whenever I edited nginx's config, I reloaded nginx when done (systemctl reload nginx
) and verified it didn't say there was an error.
Whenever I edited tor's config, I reloaded tor when done (systemctl reload tor@default
) and verified by checking tor's logs that there was no error
(journalctl -eu tor@default
) and that tor is still running (systemctl status tor@default
).
How the .onion available button is obtained in my setup.
Verify that the preceeding steps are working. Verify that:
- Visiting http://flashflow.pastly.xyz redirects to https://flashflow.pastly.xyz to serve the website.
- Visiting http://jsd33qlp6[...]d.onion serves the website.
This button advertises the fact that the website is also available at an onion service, which improves users' security and may even improve their performance. Further, if they've configured Tor Browser to do so, Tor Browser can automatically redirect to the onion service instead of presenting a button for the user to maybe click.
Find the 2nd server {}
block you added, the one that listens on port 443. We
are now going to add a single line to it that instructs nginx to add an HTTP
header in its responses.
server {
server_name flashflow.pastly.xyz;
[... lines omitted ...]
location / {
try_files $uri $uri/ =404;
# ADD THE BELOW LINE
add_header Onion-Location http://jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion$request_uri;
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
[... lines omitted ...]
}
Reload nginx and verify it didn't say there was an error.
Visiting https://flashflow.pastly.xyz should now result in a purple .onion available button appearing in the URL bar when the page is done loading. Clicking it will take the user from https://flashflow.pastly.xyz/foo/bar to http://jsd33qlp6[...]d.onion/foo/bar.
How to add an onion Alt-Svc
that works.
Verify that the preceeding steps are working. Verify that:
- Visiting http://flashflow.pastly.xyz redirects to https://flashflow.pastly.xyz to serve the website.
- Visiting http://jsd33qlp6[...]d.onion serves the website.
- (Optional) visiting https://flashflow.pastly.xyz results in a purple .onion available button in the URL bar.
This is another HTTP header that tells the browser there is another way to
fetch the given resource that it should consider using in the future instead.
The Alt-Svc
header is used in contexts entirely outside of Tor, but it can
also be used to tell Tor Browser to consider secretly fetching content from
this host from an onion service in the future.
Common gotcha: The onion service must also support HTTPS. The onion
service does not need a TLS certificate that is valid for the onion
address: it should just use the same certificate as the regular web service,
even though it is invalid for the onion service. The browser verifies that the
certificate it gets from jsd33qlp6[...]d.onion is valid for flashflow.pastly.xyz when using the
.onion as an Alt-Svc
for the .xyz.
Add to the torrc the following line:
HiddenServiceDir /var/lib/tor/flashflow.pastly.xyz_service
HiddenServicePort 80
# ADD THE BELOW LINE
HiddenServicePort 443
Reload tor when done (systemctl reload tor@default
) and verify by checking
tor's logs that there was no error (journalctl -eu tor@default
) and that tor
is still running (systemctl status tor@default
).
Find the 2nd server {}
block you added, the one that listens on port 443. We
are now going to add a single line to it that instructs nginx to add an HTTP
header in its responses, and edit the server_name
line to list the onion
service.
server {
# EDIT THE BELOW LINE
server_name flashflow.pastly.xyz jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion;
[... lines omitted ...]
location / {
try_files $uri $uri/ =404;
# ADD THE BELOW LINE
add_header Alt-Svc 'h2="jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion:443"; ma=86400;';
}
listen [::]:443 ssl; # managed by Certbot
listen 443 ssl; # managed by Certbot
[... lines omitted ...]
}
Reload nginx and verify it didn't say there was an error.
You can verify the Alt-Svc
header is being sent by, well, inspecting the
headers that nginx sends when you request either https://flashflow.pastly.xyz
or https://jsd33qlp6[...]d.onion.
$ curl --head https://flashflow.pastly.xyz
HTTP/2 200
server: nginx/1.14.2
[... lines omitted ...]
onion-location: http://jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion/
alt-svc: h2="jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion:443"; ma=86400;
[... lines omitted ...]
# the --insecure flag tells curl to keep going even though it will see a
# cert that isn't valid for the onion service. This is expected, as
# explained previously.
$ torsocks curl --insecure --head https://jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion
HTTP/2 200
server: nginx/1.14.2
[... lines omitted ...]
onion-location: http://jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion/
alt-svc: h2="jsd33qlp6p2t3snyw4prmwdh2sukssefbpjy6katca5imn4zz4pepdid.onion:443"; ma=86400;
[... lines omitted ...]
Verifying that Tor Browser actually uses the headers is harder and beyond the
scope of this post. The basic idea is to abuse Alt-Svc
to serve something
different up via the onion service and check that you get the different content
after a couple of page refreshes.