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.
- 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)