OnionShare

Running OnionShare CLI as a systemd service

A worked example of running OnionShare CLI as a boot-time systemd service: a persistent Tor onion address for sharing or receiving files, with no torrc edits and no web server to write.

The scenario

You want a persistent Tor onion address that serves files to people over Tor - or receives files from them - and you want it to come back on its own after a reboot. The host is a headless server with no desktop, so the OnionShare GUI isn’t an option.

You could configure a traditional Tor onion service in torrc, point it at a local web server, and write the upload/download handling yourself. OnionShare’s CLI mode is the shorter path: it bundles the web server, the upload form, the download page, client auth, and the Tor connection into one process, and a single systemd unit file keeps it alive across reboots.

Why not a traditional Tor onion service?

A traditional Tor onion service is the lower-level building block. You configure HiddenServiceDir and HiddenServicePort in torrc, restart Tor, and the onion address is derived from a private key Tor stores on disk. That onion address then forwards to a local host and port - and what’s listening on that port is your problem. For a static website, that means running nginx or another web server. For receiving files, it means writing an upload handler yourself, with all the security review that implies.

OnionShare already does all of this. In Share mode, it serves a download page with compression and an optional auto-stop after the first complete download. In Receive mode, it serves an upload form that writes submitted files to a directory you choose, optionally POSTing a webhook on each submission. In Website mode, it serves a directory of static files with a strict Content Security Policy by default. Client authentication (Tor’s “client auth”) is on by default - the recipient needs a private key in addition to the onion address. And OnionShare can use its own bundled Tor, so you don’t have to manage a separate Tor instance if you don’t want to.

The trade-off is that OnionShare is opinionated. If you need a dynamic web application, a database, or a long-running onion service that does something OnionShare doesn’t, the traditional torrc approach is still the right answer. For a file share, an anonymous dropbox, or a static site, OnionShare is the simpler path - and the CLI mode lets you run it on a server.

Installing the CLI

The cleanest install on a headless server is via pip - the distro onionshare package pulls in Qt GUI dependencies you don’t need here:

sudo apt install tor python3-pip
pip3 install --user onionshare-cli

The onionshare-cli binary lands in ~/.local/bin/. Verify it runs:

onionshare-cli --help

The persistent session

A non-persistent OnionShare service generates a fresh onion address on every start. For a boot-time service, that defeats the point: you want the same address and the same client auth key every time, so the people you’ve shared them with can reach the service after a reboot.

OnionShare handles this with a persistent session JSON file. The file stores the onion’s private key, the client auth keypair, the service ID (the 56-character address body), the mode (share, receive, website, or chat), and the mode-specific settings. Once it exists, OnionShare reads it on start and serves the same onion.

The easiest way to create the persistent file is to start the share once in the OnionShare desktop app, check “Always open this tab when OnionShare is started”, start the service, and stop it. This writes a JSON file with a random name (e.g. share.json) into ~/.config/onionshare/persistent/. Copy that file to your server. If you don’t have the desktop app handy, start the CLI with --persistent pointing at a path that doesn’t exist yet; OnionShare will generate a fresh identity, write it to that path, and reuse it from then on. You’ll see the onion address and client auth private key printed to the journal on that first run - capture them then, because that’s when you share them with your recipients.

A persistent session file for a Share mode service looks like this. Substitute your own values for the keys, the service_id, and the filename - the values below are placeholders, not a real identity:

{
  "onion": {
    "private_key": "<base64-encoded onion service private key>",
    "client_auth_priv_key": "<52-char base32 private key to give to recipients>",
    "client_auth_pub_key": "<52-char base32 public key, kept server-side>"
  },
  "persistent": {
    "mode": "share",
    "enabled": true
  },
  "general": {
    "title": null,
    "public": false,
    "autostart_timer": 0,
    "autostop_timer": 0,
    "service_id": "<56-character onion address body>",
    "qr": false
  },
  "share": {
    "autostop_sharing": true,
    "filenames": [
      "/home/onion/share/release-archive.tar.gz"
    ]
  },
  "receive": {
    "data_dir": "/home/onion/OnionShare",
    "webhook_url": null,
    "disable_text": false,
    "disable_files": false
  },
  "website": {
    "disable_csp": false,
    "custom_csp": null,
    "filenames": []
  },
  "chat": {}
}

Store this at, say, /home/onion/.config/onionshare/persistent/share.json. Lock the file down - it contains the onion service private key:

chmod 0600 /home/onion/.config/onionshare/persistent/share.json

The main OnionShare config at ~/.config/onionshare/onionshare.json also needs to know about the persistent tab. Its persistent_tabs list should contain the filename (without the .json extension) of your persistent session:

{
  "version": "2.6.2",
  "connection_type": "bundled",
  "control_port_address": "127.0.0.1",
  "control_port_port": 9051,
  "socks_address": "127.0.0.1",
  "socks_port": 9050,
  "socket_file_path": "/var/run/tor/control",
  "auth_type": "no_auth",
  "auth_password": "",
  "auto_connect": true,
  "use_autoupdate": true,
  "autoupdate_timestamp": null,
  "bridges_enabled": false,
  "bridges_type": "built-in",
  "bridges_builtin_pt": "obfs4",
  "bridges_moat": "",
  "bridges_custom": "",
  "bridges_builtin": {},
  "persistent_tabs": ["share"],
  "locale": "en",
  "theme": 0
}

The defaults above are what OnionShare writes for itself; the only thing you typically need to edit is persistent_tabs. If you’d rather use the system Tor instead of the bundled one, set connection_type to "socket_file" (and make sure the OnionShare user can read /var/run/tor/control) or "control_port" (with a matching auth_password if you’ve set one in your torrc).

The systemd unit file

With the configs in place, create /etc/systemd/system/onionshare-cli.service. Adjust the User, Group, the path to the onionshare-cli binary, the persistent session path, and the shared file path to match your setup:

[Unit]
Description=OnionShare CLI
After=network.target

[Service]
ExecStart=/home/onion/.local/bin/onionshare-cli --persistent /home/onion/.config/onionshare/persistent/share.json /home/onion/share/release-archive.tar.gz
Restart=on-failure
User=onion
Group=onion

[Install]
WantedBy=multi-user.target

Two things to notice. First, the path to the shared file (/home/onion/share/release-archive.tar.gz) is passed both in the filenames list inside share.json and as a positional argument on the ExecStart line. This is required: OnionShare reads the persistent settings from the JSON, but the CLI still expects the share files on the command line. Second, User and Group should be the unprivileged account that owns the OnionShare config - not root. The bundled Tor and the onion service both run as that user.

Reload systemd, enable the service to start at boot, and start it:

sudo systemctl daemon-reload
sudo systemctl enable --now onionshare-cli.service

Verifying it’s running

Follow the journal:

sudo journalctl -f -u onionshare-cli.service

On a clean start, you’ll see OnionShare boot Tor, build the onion service, and print the onion address and the client auth private key:

Feb 09 10:14:09 onion-host onionshare-cli[18852]: [6.5K blob data]
Feb 09 10:14:18 onion-host onionshare-cli[18852]: Compressing files.
Feb 09 10:14:18 onion-host onionshare-cli[18852]: Give this address and private key to the recipient:
Feb 09 10:14:18 onion-host onionshare-cli[18852]: http://niktadkcp6z7rym3r5o3j2hnmis53mno5ughvur357xo7jkjvmqrchid.onion
Feb 09 10:14:18 onion-host onionshare-cli[18852]: Private key: RHJSN4VI3NKGDSIWK45CCWTLYOJHA6DQQRQXUID3FXMAILYXWVUQ
Feb 09 10:14:18 onion-host onionshare-cli[18852]: Press Ctrl+C to stop the server

Load the .onion address in Tor Browser, enter the private key when prompted, and the download page should appear. After a reboot, systemctl status onionshare-cli.service should show the unit active (running), and the same onion address should still resolve - that’s the persistence doing its job.

If you don’t want recipients to need a private key, set "public": true in the general section of share.json. The onion address is then loadable by anyone who has it, with no key prompt. Use this only if the address itself is the secret.

Switching to Receive mode

The same unit file works for Receive mode with two changes to share.json. Set persistent.mode to "receive" instead of "share", and configure the receive block to point at the directory where uploads should land:

"persistent": {
  "mode": "receive",
  "enabled": true
},
"receive": {
  "data_dir": "/home/onion/uploads",
  "webhook_url": "https://example.net/hooks/onionshare",
  "disable_text": false,
  "disable_files": false
}

Then drop the positional filename argument from the ExecStart line - there’s nothing to share in Receive mode:

ExecStart=/home/onion/.local/bin/onionshare-cli --persistent /home/onion/.config/onionshare/persistent/share.json

Reload systemd and restart the service. The same onion address and the same client auth key now serve an upload form. Files land in /home/onion/uploads, organised into timestamped subfolders; text submissions land there too. The optional webhook_url is POSTed to on each submission, which is useful for piping into a notification pipeline - a Node-RED flow, a Signal bot, anything that accepts an HTTP POST.

A note on the private key

The persistent JSON contains the onion service private key and the client auth private key. Treat the file the way you’d treat an SSH private key: chmod 0600, owned by the OnionShare user only, and never committed to a repository. If the file leaks, the onion address can be impersonated by anyone who holds the key - they can stand up a look-alike service on the same address. Rotating means deleting the JSON, letting OnionShare generate a new one, and re-sharing the new address and client auth key with your recipients.

At a glance
  • Modes: Share, Receive, Website, Chat
  • Persistence: same onion address across reboots
  • Auth: client auth (private key) on by default
  • Tor: bundled, or system Tor via control_port / socket_file
  • Service: onionshare-cli.service (systemd, multi-user.target)
Need a Tor-facing service set up?
I do contract sysadmin and privacy-infrastructure work, and am one of the core developers of OnionShare.
Contact me