Run a Station
Step-by-step self-hosting guide for a Cipher Station station: one-click installers for Linux (systemd) and macOS (launchd + menu-bar app), the .env config surface, the zero-config Cloudflare Quick Tunnel for public access, daily USB backups and restore, and storage-cap controls — with the exact install commands.
One-command install
InteractivePick your platform to see the single command that stands up a complete station — IPFS, your post-quantum identity, background services, and a public HTTPS URL. Everything below the command is automated for you.
git clone https://github.com/CharliePetch/cipher-station.git
cd cipher-station
chmod +x install.sh
./install.sh- ✓Any systemd Linux — auto-detects apt / dnf / pacman / zypper (amd64 / arm64 / armhf)
- ✓Installs Python 3.11+, IPFS (Kubo), and cloudflared
- ✓Creates systemd services for IPFS, Cipher Station, and the tunnel
- ✓Bootstraps your identity, opens ufw/firewalld, sets a daily backup timer
Your station at a glance
InteractiveOn macOS a menu-bar app surfaces what you actually need day to day: whether the station and IPFS gateway are live, your current public URL, recent pairing PINs, and how much disk Cipher Station is using.
- 283 949just now · expires 4m51s
- 610 0272m ago · expires 2m44s
Run your own station
A Cipher Station station runs on hardware you control — a Linux box (even a Raspberry Pi), or your Mac — serving your content over IPFS with post-quantum encryption. One script installs IPFS, generates your keys, and brings the services up; about ten minutes on a Pi.
Here's what you end up running:
| Piece | What it is |
|---|---|
| Cipher Station server | A Python (FastAPI/uvicorn) HTTPS service on port 8443, self-signed cert |
| IPFS (Kubo v0.28.0) | Local content daemon; API and gateway bound to 127.0.0.1 only |
| Cloudflare tunnel (optional) | Zero-config public HTTPS via a *.trycloudflare.com URL |
| Your identity | A post-quantum keypair (ML-KEM-768 + ML-DSA-65) generated on first run |
Linux (one command)
This is the recommended way to run a station full-time. The installer auto-detects your package manager and firewall, then wires everything into systemd so it survives reboots. Requires systemd (Alpine, Void, OpenRC need manual steps).
git clone https://github.com/CharliePetch/cipher-station.git
cd cipher-station
chmod +x install.sh && ./install.sh
It needs sudo (installs packages, writes systemd units). It installs Python 3.11+ and build deps, downloads IPFS Kubo v0.28.0 and cloudflared, runs ipfs init --profile=lowpower (API + gateway bound to 127.0.0.1 only, never exposed), creates a .venv, bootstraps your post-quantum identity, and brings up four systemd units.
The post-quantum stack is pure-Python by default — prebuilt wheels, including piwheels on a Pi, so there's no compiler step.
What systemd runs
| Unit | Command | Notes |
|---|---|---|
ipfs.service | ipfs daemon --enable-gc | Always enabled |
cipherstation.service | .venv/bin/python run.py | Requires=ipfs.service, loads .env |
cloudflared-tunnel.service | Quick Tunnel → https://localhost:8443 | Only if cloudflared installed |
cipherstation-backup.timer | python -m cipher_station.backup create --if-present | Fires daily, USB backup |
The firewall is deliberately minimal: ufw opens only 8443/tcp. The IPFS API and gateway stay on localhost. Tail the logs to watch it come up — and to grab your public tunnel URL once it appears:
sudo journalctl -u cipherstation -f | grep 'Tunnel endpoint'
macOS (double-click, no sudo)
macOS installs entirely under ~/.cipherstation and runs as user-session LaunchAgents — no admin password required.
chmod +x install-macos.command && ./install-macos.command
Gatekeeper note: the
.commandis an unsigned local script, so a plain double-click shows a "cannot be opened" warning. The first time, right-click → Open in Finder (or run the command above from Terminal).
Everything is self-contained: binaries in ~/.cipherstation/bin, and a dedicated IPFS repo at ~/.cipherstation/ipfs (not the default ~/.ipfs). If you ever run ipfs by hand on macOS, set IPFS_PATH=~/.cipherstation/ipfs first or you'll hit the wrong repo.
Four LaunchAgents come up (RunAtLoad + KeepAlive, all logging into cipher_station_data/logs/):
| Label | What it does |
|---|---|
com.cipherstation.ipfs | ipfs daemon --enable-gc |
com.cipherstation.station | python run.py |
com.cipherstation.tunnel | Cloudflare Quick Tunnel (if cloudflared present) |
com.cipherstation.tray | The 🛰 menu-bar app |
Manage them with launchctl:
launchctl list | grep com.cipherstation # see what's running
launchctl kickstart -k gui/$(id -u)/com.cipherstation.station # restart the station
launchctl unload ~/Library/LaunchAgents/com.cipherstation.*.plist # stop everything
macOS has no firewall step and no scheduled backup — those are Pi-only. On a Mac you're responsible for not exposing
8443beyond the tunnel, and for running backups yourself.
The menu-bar app
The 🛰 icon is the friendly face of your station. It's a local, owner-only tool — it opens no network ports of its own; it just reads local files and shells out to the ipfs CLI. It refreshes every 15 seconds and the icon flips to 🛰⚠️ if the station's port 8443 stops responding.
From the menu you can:
- See Status (Running / Stopped) and IPFS gateway health (🟢 Live / 🔴 Down)
- Click to copy your Cloudflare URL and Peer ID (read from
public.json) - See recent pairing PINs — the killer feature when adding a new device
- See storage used vs. cap, with an inline Edit Storage Limit…
- Open the data folder, restart the station, or refresh
When you pair a device, the tray surfaces the 6-digit PIN with a live 5-minute expiry countdown — click to copy.
Configure with .env
Both installers write a default .env on first run and never overwrite an existing one — so re-running the installer keeps your edits. Here's the full surface:
# --- Identity ---
CIPHER_PASSWORD= # encrypts the station private key (empty = no encryption)
CIPHER_PQC_BACKEND=auto # auto | liboqs | python
# --- Server ---
CIPHER_PORT=8443
CIPHER_HOST=0.0.0.0
# --- TLS (auto-generated on first run if missing) ---
SSL_CERTFILE=./cipher_station_data/ssl/cert.pem
SSL_KEYFILE=./cipher_station_data/ssl/key.pem
# --- IPFS ---
IPFS_API_URL=http://127.0.0.1:5001
IPFS_TIMEOUT=30
IPFS_MAX_RETRIES=3
# --- Limits ---
MAX_UPLOAD_SIZE=104857600 # 100 MB
CORS_ORIGINS=*
# --- Follow requests ---
CIPHER_DEV_AUTO_ACCEPT_FOLLOWS=0 # 1 = DEV ONLY: auto-accept follows (never in prod)
LOG_LEVEL=INFO
# --- Cloudflare Tunnel ---
CLOUDFLARE_TUNNEL_ENABLED=false # flipped to true on fresh install if cloudflared present
CLOUDFLARE_METRICS_PORT=40469
# --- Backup ---
CIPHER_BACKUP_DEST= # blank = auto-detect a single USB under /media/<user>
# --- Data ---
CIPHER_BASE_DIR=./cipher_station_data # relative paths resolve against the project, not cwd
| Key | Gotcha |
|---|---|
CIPHER_PQC_BACKEND | auto prefers hardened, constant-time liboqs, falls back to pure-Python; liboqs/python force one. Both are FIPS-interoperable, but changing this needs a restart (chosen once at startup). |
CIPHER_PASSWORD | Encrypts private keys at rest (Argon2i + SecretBox). No rotation path — change it later and existing key bundles can't be decrypted. |
CIPHER_DEV_AUTO_ACCEPT_FOLLOWS | Must stay 0 in production. 1 auto-approves any stranger and hands them content keys. Default is fail-closed: follows land Pending until you approve. |
CIPHER_BASE_DIR | Relative paths resolve against the project root (where run.py lives), not cwd — stable no matter how systemd/launchd sets the working directory. Use an absolute path (e.g. /var/lib/cipherstation) to store data elsewhere. |
The TLS cert is handled for you: on first run, run.py generates a self-signed RSA-2048 cert (CN cipherstation, 10-year validity) if SSL_CERTFILE / SSL_KEYFILE are missing. That self-signed cert is exactly why the tunnel and health checks use --no-tls-verify / curl -k.
Go public with the Cloudflare tunnel
You don't need a static IP, a domain, port forwarding, or even a Cloudflare account. The Quick Tunnel gives you public HTTPS in one command:
cloudflared tunnel --url https://localhost:8443 --no-tls-verify --metrics localhost:40469
--no-tls-verifyis required — Cipher Station's cert is self-signed.--metrics localhost:40469lets the station read back the assigned*.trycloudflare.comURL (this isCLOUDFLARE_METRICS_PORTin.env).
On the Pi this runs as cloudflared-tunnel.service; on macOS as com.cipherstation.tunnel. On a fresh Pi install, the installer flips CLOUDFLARE_TUNNEL_ENABLED=false to true automatically when cloudflared is present.
The tunnel URL is ephemeral — it changes every time cloudflared restarts. So never hand the
trycloudflare.comURL to followers as a permanent address.
Followers find you by your permanent address
Share https://ipfs.io/ipns/<PeerID> (get it via ipfs id -f='<id>') — your IPFS Peer ID never changes, even across tunnel restarts or a full reinstall from backup, and clients self-heal via IPNS when your endpoint rotates. See Storage & Discovery for the full resolution ladder.
Backups and restore (Pi)
On the Pi, cipherstation-backup.timer fires daily and runs:
python -m cipher_station.backup create --if-present
The --if-present flag makes it a no-op (clean exit) when no USB drive is mounted — so a missing drive never causes a failure. Backups go to CIPHER_BACKUP_DEST, or to an auto-detected USB under /media/<user> if that's left blank. Set a passphrase and archives are written encrypted (.enc).
Restoring is just the installer with a flag:
./install.sh --restore # auto-detect a backup on a mounted USB
./install.sh --restore=/path/to/cipherstation-backup-*.tar.gz # restore a specific archive
The important detail: restore runs before identity bootstrap. So your keys, your IPFS peer identity, and your pinned content all come back exactly as they were — your station keeps its permanent IPNS address instead of becoming a brand-new one. Encrypted archives prompt for the passphrase.
Storage controls
IPFS stores your encrypted content locally, and you decide how much room it gets via Datastore.StorageMax:
ipfs config Datastore.StorageMax <value>
Then restart: systemctl restart ipfs.service on the Pi, or launchctl kickstart -k gui/$(id -u)/com.cipherstation.ipfs on macOS (the menu-bar app's Edit Storage Limit… does this for you in two clicks).
One thing to know:
StorageMaxbounds what garbage collection keeps — it does not instantly delete pinned content. Lowering it below current usage won't immediately free space; it caps future growth and what GC retains.
You're live
That's the whole loop: run one script, watch the logs for your tunnel URL, copy your IPNS address from the menu bar (or ipfs id), and hand it to a friend.