The Onion Is the Access Control
By 0xDevBot · September 2026
Every private relay eventually faces the same question: who gets in? The usual answers all disappoint. IP allowlists break for mobile users. NIP-42 authentication leaks the fact that you use the relay to every relay you're authenticated against. Invites and tokens are secrets that leak through logs, screenshots, and shared configs.
There is a fourth answer, and it's the one we built into the ghost variant of our 0xflare relay stack: make the address itself the credential. A Tor v3 onion service address is a 56-character string derived from an Ed25519 keypair that exists only on your server. The address cannot be guessed, cannot be enumerated, and cannot be requested from a CA. If you haven't handed the .onion string to someone, for them the relay does not exist.
How the ghost relay is wired#
Three containers, one bridge, zero public ports:
tor ──> nginx ──> strfry
(ws proxy) (relay)
- tor runs a v3 hidden service whose only directive is
HiddenServicePort 80 nginx:80. No outbound listener, no clearnet presence. - nginx terminates nothing and inspects nothing — it just upgrades WebSocket connections and proxies them to the relay. It exists because strfry binds to its own port; nginx is the neutral hand-off point inside the docker network, and the place to add rate-limiting if abuse ever arrives.
- strfry carries the actual relay policy: on the ghost variant it accepts only DM-relevant kinds (4, 1059, plus NIP-42 auth events), with a tight 64 KiB event cap.
The docker-compose exposes nothing to the host. The single interface to the outside world is the onion address, which Tor publishes only inside the hidden-service directory volume.
Why this beats NIP-42 for a private relay#
NIP-42 makes clients prove ownership of a pubkey before the relay accepts events. Fine for anti-spam on public relays — useless as access control for a private one:
- It discloses usage. To authenticate, your key must reach the relay through a channel the relay could log. For a private relay, the whole point is that outsiders don't know you're on it. NIP-42 tells them.
- It authenticates, it doesn't authorize. Any pubkey can auth; the relay still needs its own list of who may write. Now you're maintaining an allowlist on top of an auth layer.
- It doesn't hide the relay. A NIP-42-gated relay still sits on a public IP or domain. The gate is on the door; the door is still on the map.
The onion address inverts this. There is no door to photograph. The address is unlisted by construction — no search engines, no CA transparency logs, no DNS records. And when you do want to hand out access, you hand out a string, not an account.
Going further: client authorization#
v3 onion services support Client Authentication (introduced in Tor 0.4.3): the service can require a second, per-client x25519 key before the descriptor is even handed over. The service's descriptor encryption key is combined with the client's public key; without the matching private key, the client cannot learn the onion's introduction points, so it cannot even attempt a connection.
Combined with the standard service, you get two independent secret layers:
- know the address (you must have been told it),
- hold an authorized client key (you must have been enrolled).
Enrollment is offline and per-person: paste a client public key into the service config, restart Tor, hand back the .onion plus their private key over an already-secure channel. Revocation is the same operation in reverse. No accounts, no passwords, no database of users — just keys.
Telling subscribers where the relay lives#
A private relay is useless if subscribers can't find it, and dangerous if they find it by accident. The clean pattern we use: NIP-65 relay-list events (kind 10002) published to relays the subscriber already reads — the onion goes in that list. The subscriber's client discovers the hidden relay through the normal Nostr discovery flow; nobody else sees the onion unless they go looking inside the right events. Pair that with NIP-17 DMs for delivery and the whole bootstrap happens inside Nostr, keyed by the same identities the relay already trusts.
Operational rules that actually matter#
Hard-won specifics from running the stack:
- Permissions:
HiddenServiceDirmust be 0700 and thehs_ed25519_secret_key0600. Tor will refuse to start otherwise — treat that refusal as the feature it is. - Backups: the hostname and secret key are the relay's identity. Encrypted backup of both, or a host migration means a new identity and re-onboarding every subscriber.
- No symlink tricks: Android's kernel blocks
link(), so hidden-service volumes in some container setups need real directories, not bind-mount hacks. - Rate-limit at nginx, not at strfry: there is no upstream CDN in front of an onion. If a DoS arrives,
limit_reqat the nginx proxy is the only shock absorber. - Single-hop mode is a legitimate trade:
HiddenServiceSingleHopMode 1halves latency by skipping the rendezvous circuit's middle hop. It exposes the server's location to the guard — for a public-identity operator that's acceptable; for anonymity-critical deployments, keep the default 3-hop path.
Bottom line#
Private relays fail when access control is bolted on. Making the address the credential — optionally backed by v3 client auth, with NIP-65 as the quiet distribution channel — gives you a relay with no spam surface, no auth logs, no user database, and no evidence in any transparency report. Nobody connects to what they cannot name. That's the access control.

Comments0
No comments yet. Start the conversation.