Operational security guidance for running freeq in production.
By default, any iroh peer can connect to your server. This is convenient
for development but inappropriate for production.
# DEVELOPMENT ONLY — open federation (default)
freeq-server --iroh
Use --s2s-allowed-peers to restrict which peers can connect:
freeq-server \
--iroh \
--s2s-peers <peer-id-1>,<peer-id-2> \
--s2s-allowed-peers <peer-id-1>,<peer-id-2>
When --s2s-allowed-peers is set:
--s2s-peers (these should beNote:
--s2s-allowed-peerscurrently only enforces incoming connections.
Outgoing connections go to whatever--s2s-peersspecifies. Ensure both
flags are consistent.
All S2S connections are rate-limited to 100 events/second per peer.
Events exceeding this rate are dropped with a warning log. This prevents
a compromised or misbehaving peer from overwhelming your server.
The following operations from federated peers are authorized before execution:
| Operation | Authorization |
|---|---|
| Mode changes (+o, +v, etc.) | Sender must be a channel op |
| Kicks | Kicker must be an op or channel founder |
| Topic changes (+t channels) | Setter must be an op |
| Joins | Checked against bans and invite-only (+i) |
| Bans | Authorized set/remove via S2S Ban variant |
Server A (peer-id-a):
freeq-server \
--iroh \
--s2s-peers <peer-id-b> \
--s2s-allowed-peers <peer-id-b>
Server B (peer-id-b):
freeq-server \
--iroh \
--s2s-peers <peer-id-a> \
--s2s-allowed-peers <peer-id-a>
freeq supports three SASL verification methods:
crypto) — Client signs challenge with private keypds-session) — Client provides app-password JWTpds-oauth) — Client provides DPoP-bound OAuth tokenAT Protocol PDS servers use DPoP (Demonstrating Proof of Possession) with
rotating nonces. The nonce rotation can cause SASL verification to fail if
the client's nonce has expired.
How the retry works:
use_dpop_nonce errordpop-nonce headerNOTICE <nick> :DPOP_NONCE <nonce> to the clientThis is handled automatically by the SDK (freeq-sdk) and TUI client.
Web clients use the broker OAuth flow which handles nonces server-side.
--challenge-timeout-secs)Both TCP and WebSocket listeners enforce a per-IP connection limit:
| Transport | Limit | Behavior |
|---|---|---|
| TCP | 20 connections/IP | Connection refused with log warning |
| WebSocket | 20 connections/IP | WebSocket upgrade rejected (429) |
These limits are hardcoded. Connections from the same IP beyond the limit
are immediately closed. The limit applies to concurrent connections, not
rate.
Channel operators can ban users:
/MODE #channel +b nick!*@* # Ban by nick
/MODE #channel +b *!*@host # Ban by host mask
Bans are:
- Persisted to the database
- Synchronized across federated servers via S2S
- Enforced on join (including S2S joins)
- Checkable via /MODE #channel b (ban list)
Server operators (configured via --oper-dids or the OPER command) can:
# Auto-grant oper to specific DIDs
freeq-server --oper-dids did:plc:abc123,did:plc:def456
# Or via environment variable
OPER_DIDS=did:plc:abc123 freeq-server
# Or via OPER command (requires --oper-password)
freeq-server --oper-password "secret"
# Then in client: /OPER admin secret
Authenticated users get cloaked hostnames:
freeq/plc/xxxxxxxx (truncated DID hash)freeq/guestReal IP addresses are never exposed to other users.
The server generates the following key files automatically on first run:
| File | Purpose | Rotation |
|---|---|---|
msg-signing-key.secret |
Server message signatures (ed25519) | Replace file + restart |
verifier-signing-key.secret |
Credential verifier signatures | Replace file + restart |
db-encryption-key.secret |
Database encryption at rest (AES-256-GCM) | Cannot rotate without re-encrypting all data |
iroh-key.secret |
iroh QUIC endpoint identity | Replace file + restart (changes your peer ID) |
All key files are automatically excluded from git:
# In .gitignore
*.secret
iroh-key.secret
verifier-signing-key.secret
freeq-server/certs/*.pem
freeq-server/certs/*.key
⚠️ WARNING: Never commit
*.secretfiles or TLS private keys to version
control. If a key is accidentally committed, rotate it immediately:
- Delete the compromised key file
- Restart the server (a new key is generated)
- Use
git filter-branchor BFG Repo-Cleaner to purge from history- Force-push and notify collaborators
TLS certificates and keys are specified via command-line flags:
freeq-server \
--tls-cert /etc/letsencrypt/live/example.com/fullchain.pem \
--tls-key /etc/letsencrypt/live/example.com/privkey.pem
Message signing key (msg-signing-key.secret):
1. Stop the server
2. Delete msg-signing-key.secret
3. Start the server (new key generated)
4. The public key endpoint (/api/v1/signing-key) updates automatically
5. Old signatures remain valid for verification (clients cache public keys)
iroh endpoint key (iroh-key.secret):
1. Stop the server
2. Delete iroh-key.secret
3. Start the server (new key generated, new peer ID)
4. Update --s2s-peers and --s2s-allowed-peers on all peer servers
Database encryption key (db-encryption-key.secret):
- ⚠️ Rotating this key makes all existing encrypted messages unreadable
- Back up the key file securely
- There is currently no re-encryption utility
Set restrictive permissions on key files:
chmod 600 *.secret
chmod 600 /path/to/tls-key.pem
chown freeq:freeq *.secret
Production deployment checklist:
--tls-cert, --tls-key)--s2s-allowed-peers) if federating--oper-dids or OPER_DIDS env)*.secret files in version controlchmod 600 permissions"Rejecting S2S connection" and "per-IP limit reached"BROKER_SHARED_SECRET)