Guides
Self-hosting SearXNG on OpenBSD over a Tor onion service
This guide shows how to self-host SearXNG on OpenBSD and expose it through a Tor onion service while keeping privileges separated and permissions minimal.
TL;DR
- Install an explicit Python 3.13 package, Tor, and the build
dependencies, then create a dedicated
_searxnguser. - Install SearXNG inside a Python virtual environment, including
the modules required to bootstrap its non-isolated build. Keep a
small, separate
settings.ymlin/home/searxngand force search traffic through Tor. - Bind Gunicorn to localhost and publish the service via a Tor v3 onion service to avoid exposing SearXNG directly on clearnet.
Context
This guide teaches OpenBSD users how to self-host a SearXNG instance at an onion address. I separate privileges and grant as few permissions as possible. It started as a copy of an outdated-ass tumfatig.net guide, but the commands below have the changes needed for current SearXNG and OpenBSD.
I tested this procedure on a fully patched OpenBSD 7.9 system.
SearXNG is a rolling release: every commit to its master
branch is a release, and infrastructure requirements can change. Read
the upstream maintenance notes before applying these commands to a
later version.
Preliminary steps
Run the commands in this section as root. Patch the base system,
update installed packages, and install the exact Python branch shipped
with OpenBSD 7.9. Using python without a branch can be
ambiguous to pkg_add.
# syspatch
# pkg_add -u
# pkg_add git libxslt python%3.13 tor torsocks
Enable Tor now because both the optional bootstrap proxy and the
SearXNG outgoing proxy use its SOCKS listener on
127.0.0.1:9050.
# rcctl enable tor
# rcctl start tor
# rcctl check tor
tor(ok)
Set up a dedicated user
# install -d -m 0755 /etc/login.conf.d
# vi /etc/login.conf.d/searxng
searxng:\
:openfiles=4096:\
:tc=daemon:
# cap_mkdb /etc/login.conf
# useradd -g =uid -c "SearXNG metasearch engine" \
-L searxng -s /sbin/nologin -d /home/searxng \
-m -r 2000..2500 _searxng
Installation
Install SearXNG sources and prepare the environment
# su -l -s /bin/ksh _searxng
$ echo "umask 077" >> ~/.profile
$ git clone https://github.com/searxng/searxng ~/src
$ python3 -m venv ~/pyenv
$ echo ". ~/pyenv/bin/activate" >> ~/.profile
$ ^D
Install SearXNG in the virtual environment
If you want the bootstrap downloads to use Tor too, put
torsocks before the git and pip
commands. This is optional; routing SearXNG's searches through Tor is
configured separately below.
# su -l -s /bin/ksh _searxng
$ command -v python && python --version
/home/searxng/pyenv/bin/python
$ python -m pip install --upgrade pip setuptools wheel
$ python -m pip install --upgrade \
pyyaml msgspec typing-extensions pybind11
$ cd ~/src
$ python -m pip install --use-pep517 --no-build-isolation --editable .
$ python -m pip check
$ ^D
Do not remove the second bootstrap command while retaining
--no-build-isolation. SearXNG's setup code imports those
modules before pip can install the dependencies declared by the
project. Without that command, a clean installation fails during the
build bootstrap.
Configuration
I keep the configuration outside the source tree so an update cannot overwrite it. More importantly, I use SearXNG's small reference template, which inherits new defaults from the updated source instead of freezing a copy of the enormous default file.
# su -l -s /bin/ksh _searxng
$ install -m 0600 \
~/src/utils/templates/etc/searxng/settings.yml ~/settings.yml
$ sed -i \
-e "s/ultrasecretkey/$(openssl rand -hex 32)/g" \
-e "s/limiter: true/limiter: false/" \
-e "s|url: valkey://localhost:6379/0|url: false|" \
~/settings.yml
$ echo 'export SEARXNG_SETTINGS_PATH=/home/searxng/settings.yml' >> ~/.profile
The reference template enables the limiter and expects a Valkey server. This deliberately small onion-only setup installs no Valkey, so the commands disable both that URL and the limiter. The template keeps the image proxy enabled. If you turn this into a busy multi-user service, install a compatible Valkey service and re-enable the limiter according to the official documentation.
Tor proxy defaults in shell profile
To route network clients that honor standard proxy environment
variables through Tor, add the following to ~/.profile.
This covers tools such as Git and pip without placing
torsocks before every command.
export ALL_PROXY='socks5h://127.0.0.1:9050'
export HTTP_PROXY="$ALL_PROXY" HTTPS_PROXY="$ALL_PROXY"
Configure SearXNG itself in ~/settings.yml, inside the
outgoing section. This explicitly routes requests to
search engines through the local Tor SOCKS proxy:
outgoing:
request_timeout: 10.0
max_request_timeout: 30.0
pool_connections: 200
retries: 2
extra_proxy_timeout: 30
using_tor_proxy: true
proxies:
all://:
- socks5h://127.0.0.1:9050
$ . ~/.profile
$ vi ~/settings.yml
Run a local smoke test from the source directory (use
torsocks if you did not configure ~/.profile
as shown above):
$ cd ~/src
$ python -m searx.webapp
...
* Serving Flask app 'webapp'
* Debug mode: off
^C
$ ^D
Tor onion service configuration
To expose SearXNG through an onion address, add this to
/etc/tor/torrc:
# vi /etc/tor/torrc
HiddenServiceDir /var/tor/searx_hidden_service/
HiddenServicePort 80 127.0.0.1:8888
Version 3 is Tor's default, so no obsolete version directive is necessary. Check the configuration, restart Tor, and read the generated onion hostname:
# tor --verify-config -f /etc/tor/torrc
# rcctl restart tor
# cat /var/tor/searx_hidden_service/hostname
output: example.onion
Add the real hostname to the existing server section of
/home/searxng/settings.yml so SearXNG generates canonical
inbound links correctly:
server:
# Keep the generated secret_key and the other existing settings here.
base_url: "http://example.onion/"
Replace example.onion with the hostname Tor generated.
Never publish or casually copy the other files in the hidden-service
directory: they contain the keys that control the onion address. Also
remember that anybody who learns this address can use the instance
unless you configure Tor client authorization.
Run under rc.d with Gunicorn
Keep It Simple, Stupid! Gunicorn serves SearXNG on localhost. A
short OpenBSD rc.d script delegates process management,
privilege dropping, status checks, and logging to
rc.subr.
# su -l -s /bin/ksh _searxng
$ python -m pip install --upgrade gunicorn
$ vi searxng.conf.py
# Server Socket ========================================================
bind = "127.0.0.1:8888"
backlog = 128
# Worker Processes =====================================================
workers = 1
keepalive = 5
worker_class = "gthread"
threads = 4
timeout = 120
graceful_timeout = 30
# Server Mechanics =====================================================
chdir = "/home/searxng/src"
raw_env = [
"PYTHONPATH=/home/searxng/src",
"SEARXNG_SETTINGS_PATH=/home/searxng/settings.yml",
]
# Logging ==============================================================
accesslog = "-"
errorlog = "-"
$ ^D
Create the service script:
# vi /etc/rc.d/searxng
#!/bin/ksh
daemon="/home/searxng/pyenv/bin/python"
daemon_user="_searxng"
daemon_execdir="/home/searxng/src"
daemon_flags="-m gunicorn -c /home/searxng/searxng.conf.py searx.webapp:application"
daemon_logger="daemon.info"
. /etc/rc.d/rc.subr
rc_bg=YES
rc_reload=NO
rc_cmd "$1"
# chown root:wheel /etc/rc.d/searxng
# chmod 0555 /etc/rc.d/searxng
# rcctl enable searxng
# rcctl start searxng
searxng(ok)
The Python interpreter is the daemon, and Gunicorn is invoked as a
module. That detail matters: rc.subr identifies processes
by their complete command line. Pointing daemon at
Gunicorn's Python wrapper can start the service yet leave
rcctl unable to find it for checks, restarts, or
shutdown.
Verification and troubleshooting
Verify each boundary separately before blaming Tor Browser:
# rcctl check tor
# rcctl check searxng
# ftp -o /dev/null http://127.0.0.1:8888/
# fstat | grep ':8888'
The listener must be on 127.0.0.1:8888, never on a
public address. Then open the generated onion address in Tor Browser
and run a real search. If startup fails, stop guessing and ask
rc.d for the command it ran, then inspect the daemon
log:
# rcctl -d start searxng
# tail -f /var/log/daemon
A dependency error here usually means the bootstrap modules were not
installed before the editable SearXNG package. A proxy error means Tor
is not listening on port 9050, or the outgoing block is
not nested correctly in YAML.
Updating SearXNG
This installation tracks SearXNG's master branch. Read
the upstream maintenance and migration documentation before running
this helper. The git reset --hard command deliberately
discards local changes in the source checkout; the separate minimal
settings.yml remains untouched and continues to inherit
the updated defaults.
# vi /home/searxng/update.sh
#!/bin/ksh
set -eu
cd /home/searxng/src
git fetch --prune origin
git reset --hard origin/master
/home/searxng/pyenv/bin/python -m pip install --upgrade pip setuptools wheel
/home/searxng/pyenv/bin/python -m pip install --upgrade \
pyyaml msgspec typing-extensions pybind11
/home/searxng/pyenv/bin/python -m pip install --upgrade gunicorn
/home/searxng/pyenv/bin/python -m pip install \
--use-pep517 --no-build-isolation --upgrade --editable .
/home/searxng/pyenv/bin/python -m pip check
# chown _searxng:_searxng ~_searxng/update.sh
# chmod 0750 ~_searxng/update.sh
To upgrade:
# su -l -s /bin/ksh _searxng -c /home/searxng/update.sh
# rcctl restart searxng
searxng(ok)
# rcctl check searxng
searxng(ok)
Conclusion
With these steps, I do not need to configure httpd,
acme-client, or relayd merely to put this
service behind public HTTPS. Tor authenticates the onion address and
encrypts traffic between the client and the onion service, while
Gunicorn listens only on loopback. That means less attack surface, more
stability, and easier long-term maintenance.
If you expose SearXNG on clearnet, then you absolutely need to configure HTTPS correctly. That is a different deployment, with more moving parts, and I am deliberately not dragging them into this onion-only guide.
El Psy Kongroo!
References
- Self-hosted SearXNG instance on OpenBSD (tumfatig.net)
- SearXNG documentation: step-by-step installation
- SearXNG source: official installation and update commands
- SearXNG documentation: maintenance and migration notes
- SearXNG documentation: outgoing requests
-
OpenBSD
rc.d(8)manual page -
OpenBSD
rc.subr(8)manual page - Tor Project: onion services overview
- Tor Project: onion service setup and key-handling guidance
- OpenBSD 7.9 release and component versions
-
OpenBSD
pkg_add(1)package and branch selection