Guides

Self-hosting SearXNG on OpenBSD over a Tor onion service

Written by FlyWithMe (a SimpleX Chat user) -

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 _searxng user.
  • Install SearXNG inside a Python virtual environment, including the modules required to bootstrap its non-isolated build. Keep a small, separate settings.yml in /home/searxng and 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