8.4 KiB
Deploying FRX
Three roles: member node (frxd serve), relay (frxd relay), registry (frxd registry, run by the MA).
TLS is only a concern for servers. Members connect outbound and need no domain, port, or certificate.
Member node (zero TLS work)
frxd init --name alice --id alice.frx.federatedsearch.org \
--registry https://ma.federatedsearch.org/registry.json --ma-key <ma-hex> \
--relay https://relay.federatedsearch.org --data-dir ./alice-data
frxd add ~/documents --name docs --shared --exposure full
frxd serve
The node connects outbound over HTTPS, verifies the registry with the pinned MA key, and holds an SSE stream per relay. Nothing inbound, no DNS, no certificates. Members at departmental level can start here.
Joining the network (membership site + wizard)
The apex (federatedsearch.org) proxies the same membership page; ma. is the service host.
Identity registration stays with the MA; the wizard only consumes the credentials it issues.
MA operator — run the signup site:
frxd registry --dir ./ma init --zone frx.federatedsearch.org
frxd registry --dir ./ma serve --listen 127.0.0.1:7800
(put Caddy in front for a real domain). The page at / collects the registration form (short name, organization details) and queues it for MA review — frxd registry --dir <dir> applications lists applications and frxd registry --dir <dir> approve <id> --registry-url <url> creates the member, mints its account credential, and prints the credential block to hand over. The token is reusable: it authorizes key enrollment for every node the member runs (registry token <id> mints an additional one; registry revoke-token <id> revokes all after a leak). A single-use 24h invite (registry invite <id>) remains for constrained handoffs. The block is id=... token=... registry=... ma_key=.... Organization details (legal name, representative, contacts, payment) are recorded privately by the MA in <registry dir>/applications.json — contract data, never in the public signed snapshot.
New member:
frxd --onboarding
The wizard asks for a config path, accepts the pasted credential block (or field-by-field entry), generates keypairs, enrolls the new key with the MA (redeeming the single-use invite token), verifies the signed registry snapshot, takes relay defaults from the registry, optionally shares a directory, writes the config, and offers to start serving. If enrollment is unavailable (file-path registry), it prints the exact registry add command the operator must run.
Relay with TLS (one line)
Run frxd on loopback and terminate TLS with Caddy:
caddy reverse-proxy --from relay.federatedsearch.org --to 127.0.0.1:7700
Caddyfile equivalent (apex is the public front door, ma. the membership service, relay. the relay, git. the code host — all on one host):
federatedsearch.org {
reverse_proxy 127.0.0.1:7800
}
ma.federatedsearch.org {
reverse_proxy 127.0.0.1:7800
}
relay.federatedsearch.org {
reverse_proxy 127.0.0.1:7700
}
git.federatedsearch.org {
reverse_proxy 127.0.0.1:3000
}
Relay command (peers and registry gated by the MA):
frxd relay --listen 127.0.0.1:7700 \
--url https://relay.federatedsearch.org \
--peer https://relay2.federatedsearch.org \
--registry https://ma.federatedsearch.org/registry.json --ma-key <ma-hex>
No domain or open ports? Tunnel it:
frxd relay --listen 127.0.0.1:7700 --allow-insecure
cloudflared tunnel --url http://127.0.0.1:7700
systemd unit example:
[Unit]
Description=FRX relay
After=network-online.target
[Service]
ExecStart=/usr/local/bin/frxd relay --listen 127.0.0.1:7700 \
--url https://relay.federatedsearch.org \
--registry https://ma.federatedsearch.org/registry.json --ma-key <ma-hex>
Restart=on-failure
DynamicUser=yes
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
[Install]
WantedBy=multi-user.target
The same unit shape works for frxd serve (add --config /etc/frxd/frxd.toml).
Registry (MA)
frxd registry --dir /var/lib/frxd/registry init
frxd registry --dir /var/lib/frxd/registry add alice.frx.federatedsearch.org <key> --enc-key <enc>
frxd registry --dir /var/lib/frxd/registry serve --listen 127.0.0.1:7800
Put the same Caddy in front, or distribute registry.json out of band (it is signed, so the channel does not matter). The snapshot is versioned; nodes reject rollback and fail static during outages.
Code hosting and downloads (Gitea)
The public source and release binaries live at git.federatedsearch.org (Gitea), so the
download step on the membership page stays on infrastructure the federation operates.
One-time install on the server (root):
VER=1.27.3
curl -fsSLO https://dl.gitea.com/gitea/$VER/gitea-$VER-linux-amd64{,.sha256}
sha256sum -c gitea-$VER-linux-amd64.sha256
install -m 0755 gitea-$VER-linux-amd64 /usr/local/bin/gitea
adduser --system --shell /bin/bash --gecos 'Gitea' --home /home/git --group git
mkdir -p /var/lib/gitea/{custom,data,log} /etc/gitea && chown -R git:git /var/lib/gitea /etc/gitea
/etc/gitea/app.ini essentials (rest defaults; secrets via gitea generate secret):
WORK_PATH = /var/lib/gitea
[database]
DB_TYPE = sqlite3
PATH = /var/lib/gitea/data/gitea.db
[server]
DOMAIN = git.federatedsearch.org
SSH_DOMAIN = git.federatedsearch.org
ROOT_URL = https://git.federatedsearch.org/
HTTP_ADDR = 127.0.0.1
HTTP_PORT = 3000
[security]
INSTALL_LOCK = true
[service]
DISABLE_REGISTRATION = true
REQUIRE_SIGNIN_VIEW = false
systemd unit (User=git, ExecStart=/usr/local/bin/gitea web --config /etc/gitea/app.ini,
WorkingDirectory=/var/lib/gitea), then gitea migrate --config /etc/gitea/app.ini as the
git user and systemctl enable --now gitea. Git-over-SSH uses the host sshd via the git
user's Gitea-managed authorized_keys; HTTPS pushes can use an access token instead.
Admin bootstrap (as git user):
gitea admin user create --admin --username <you> --email <you>@federatedsearch.org --random-password --config /etc/gitea/app.ini
gitea admin user generate-access-token -u <you> -t bootstrap --scopes all --config /etc/gitea/app.ini
The org is frx, the repo frxd → clone URL
https://git.federatedsearch.org/frx/frxd.git. Membership stays closed (MA-approved
applications); repo reads are public.
Publishing a release (from the checkout):
git tag v0.1.0 && git push gitea v0.1.0
# build static binaries (see next section), then attach via the API:
curl -X POST https://git.federatedsearch.org/api/v1/repos/frx/frxd/releases \
-H "Authorization: token <token>" -H 'content-type: application/json' \
-d '{"tag_name":"v0.1.0","name":"v0.1.0"}'
curl -X POST https://git.federatedsearch.org/api/v1/repos/frx/frxd/releases/<id>/assets?name=frxd-linux-amd64 \
-H "Authorization: token <token>" -F attachment=@frxd-linux-amd64
Asset URLs follow /frx/frxd/releases/download/<tag>/<file> — the membership page pins
those. Bump the page when a release changes.
Private networks and custom CAs
ca_cert = "/etc/ssl/private-ca.pem"in[node], or--ca-certon the relay: adds a private/corporate root CA for relay and registry connections.allow_insecure = true/--allow-insecure: explicit opt-in for plainhttp://on a VPN/LAN. Without it, non-loopbackhttp://endpoints are refused at startup.http://127.0.0.1is always allowed for development.
Static binary
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
[profile.release] enables LTO and stripping. All dependencies are pure Rust, so the musl build has no system-library requirements. ring needs a musl C toolchain (musl-tools); without host sudo, build in a container instead:
docker run --rm -v "$PWD":/src -w /src rust:1-slim-bookworm bash -c \
"apt-get update -qq && apt-get install -y -qq musl-tools && rustup target add x86_64-unknown-linux-musl && cargo build --release --target x86_64-unknown-linux-musl"
Binaries land in target/x86_64-unknown-linux-musl/release/; rename to
frxd-linux-amd64 / frx-linux-amd64 for release assets, with sha256sum sidecar files.
What TLS does and does not cover
Envelopes are signed and registry snapshots are MA-signed, so TLS is not what protects message authenticity or registry integrity. TLS protects traffic from network observers, authenticates the relay endpoint, and hides mailbox metadata. Unicast response bodies are already encrypted end-to-end to the recipient's X25519 key.