Serve a web page from a Tor network hidden service.
I would like to solve this challenge using Docker. Selecting the right docker images is by them self a learning process. Finally i worked with 3 different images. One for each server.
My approach wants to set up three Docker;
1.- nginx server. SSH connection allowed to inspect html files.
2.- Tor server.
3.- Python Dashboard server (This is the Bonus part).
Nginx default listen port is 80. I set up in the port 81 the static hidden service accessible thur tor.
Please read inside to learn how to secure it a bit more.
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
# if in the docker file uncomment this, helps to reduce hidden service footprint
# when an outsider request the server to introdude himself.
#RUN sed -i -e '/ keepalive_timeout 65;/a server_tokens off;' nginx.conf
#gzip on;
include /etc/nginx/conf.d/*.conf;
}
This is the configuration file used to render default service.
server {
listen 80;
listen [::]:80;
server_name localhost;
#access_log /var/log/nginx/host.access.log main;
location / {
root /usr/share/nginx/html;
index index.html index.htm;
}
#error_page 404 /404.html;
# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /usr/share/nginx/html;
}
}
This is the configuration file used to render de hidden service thru Tor.
Here is a point for improvement changing 0.0.0.0 for the dark docker ip address.
With this i would restrict incoming ip the request comes from.
server {
# server ip #
# only allows request comming to this address.
listen 0.0.0.0:81;
# virtual server name i.e. domain name #
server_name www.open.net;
# document root #
root /var/www/open;
index index_open.html;
# log files
access_log /var/log/nginx/www.open.net_access.log;
error_log /var/log/nginx/www.open.net_error.log;
# cache files on browser level #
# Directives to send expires headers and turn off 404 error logging. #
location ~* ^.+\.(ogg|ogv|svg|svgz|eot|otf|woff|mp4|ttf|rss|atom|jpg|jpeg|gif|png|ico|zip|tgz|gz|rar|bz2|doc|xls|exe|ppt|tar|mid|midi|wav|bmp|rtf)$
{
access_log off; log_not_found off; expires max;
}
}
Docker file will adapt it the image i will use in this challenge.
Departure point is nginx:1.25.0-alpine-slim image.
you will see 3 sections in this docker file: one for Nginx, one for ssh and one for user creation.
Runnig bash's sed command dockerfile adjust at will sshd_config.
To do: Lock ssh connection to the dir with ChrootDirectory + ForceCommand.
I need to adjust folder ownership correclty.
I suffered some headache here creating the user for the ssh service. The problem is that rutinary i set a /bin/bash shell for the added User. I was not aware that the alpine image comes with /bin/sh only. This was constantly rejecting my ssh connection cause server could not set up the shell for the user.
To start both services, the docker entry point is a sh script.
/usr/sbin/nginx -g 'daemon off;' &
ssh-keygen -A
/usr/sbin/sshd -D -e "$@"
Please read comments inside entrypoint.sh for a better understanding of flags.
I installed tor in a docker debian:bookworm-slim image.
To do that:
1.- Defined Tor source in a tor.list file created/etc/apt/sources.list.d .
2.- Installed gpg, apt-transport-https, and wget to get keyrings.
3.- Installed tor deb.torproject.org-keyring.
4.- Removed gpg, apt-transport-https, and wget.
5.- Copied torrc file from my host to docker /etc/tor folder.
6.- created hidden services folders and chmoed them to 700.
7.- init the service with ENTRYPOINT [ "/bin/tor" ].
The HiddenServicePort directive takes two arguments:
- The port number to listen on.
- An optional IP address or hostname to bind to.
Following syntaxis I defined two hidden services:
HiddenServiceDir /var/lib/tor/hidden_service_bonus/
HiddenServicePort 80 data:84
and
HiddenServiceDir /var/lib/tor/hidden_service_static/
HiddenServicePort 80 open:81
The former binds to a docker Python Dash Dash Board. The Latter binds to a docker nginx with a static web page.
A bash script shows text addresses.
docker exec dark cat /var/lib/tor/hidden_service_bonus/hostname
You will see something like this ajrdqcz5sy4y4fwrdre754vro6jd57e425bzw2z6ei34u6ztkzxos5yd.onion
In this project each time you execute it a new onion address is created. Tor DOcker would have to have a docker volume to make some data persistent.
A python Script shows address's QR CODE
python3 -m onion_qr
I installed my Dashboard in a docker python:3.7-slim .
Manually installed some dependencies: dash, dash_bootstrap_components, numpy, pandas
Numpy links some C libraries non available in slim versions:
Must Install make, gcc, build-essential, dpkg-dev, libjpeg-dev.
No all-Docker python images have tools to build Numpy python library
Alpine based images do not have. You must install missing building tools with APK add Slim tagged images neither do have. you must install them using apt-get.
This is the reason why the Docker file for the Data Python Dash service Installs additional packages that are removed after Numpy installation.
Additionally, Dash app.run_service has a host parameter defaulted to 127.0.0.1. That default configuration did not worked for me.
app.run_server(debug=True, port=1234)
I changed it to
app.run_server(debug=True, host= IPAddr, port=1234)
onion_address = base32(PUBKEY | CHECKSUM | VERSION) + ".onion" CHECKSUM = H(".onion checksum" | PUBKEY | VERSION)[:2]
where:
- PUBKEY is the 32 bytes ed25519 master pubkey of the hidden service.
- VERSION is an one byte version field (default value '\x03')
- ".onion checksum" is a constant string
- CHECKSUM is truncated to two bytes before inserting it in onion_address