Operate

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

Interactive

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

Linux · terminal
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

Interactive

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

Cipher Station Running
IPFS Gateway Live
Cloudflare URLhorn-netscape…trycloudflare.com
Peer ID12D3KooW…CUYzL
Recent pairing PINs
  • 283 949just now · expires 4m51s
  • 610 0272m ago · expires 2m44s
Storage2.4 GB of 10 GB

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:

PieceWhat it is
Cipher Station serverA 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 identityA 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

UnitCommandNotes
ipfs.serviceipfs daemon --enable-gcAlways enabled
cipherstation.service.venv/bin/python run.pyRequires=ipfs.service, loads .env
cloudflared-tunnel.serviceQuick Tunnel → https://localhost:8443Only if cloudflared installed
cipherstation-backup.timerpython -m cipher_station.backup create --if-presentFires 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 .command is 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/):

LabelWhat it does
com.cipherstation.ipfsipfs daemon --enable-gc
com.cipherstation.stationpython run.py
com.cipherstation.tunnelCloudflare Quick Tunnel (if cloudflared present)
com.cipherstation.trayThe 🛰 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 8443 beyond 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
KeyGotcha
CIPHER_PQC_BACKENDauto 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_PASSWORDEncrypts private keys at rest (Argon2i + SecretBox). No rotation path — change it later and existing key bundles can't be decrypted.
CIPHER_DEV_AUTO_ACCEPT_FOLLOWSMust 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_DIRRelative 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-verify is required — Cipher Station's cert is self-signed.
  • --metrics localhost:40469 lets the station read back the assigned *.trycloudflare.com URL (this is CLOUDFLARE_METRICS_PORT in .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.com URL 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: StorageMax bounds 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.

Source: github.com/CharliePetch/cipher-station