Skip to content

Pre-deployment Checklist

Run through this list before exposing Signal Fish Server to the internet. Every item names the real configuration key (or command) involved — see the configuration reference for defaults, Configuration recipes for per-feature snippets, and Run Modes for the run commands.

Start by letting the server validate itself: it exits non-zero on an insecure or malformed config, so this is the single best gate.

Bash
cargo run -- --validate-config
# or against your built image:
docker run --rm -v ./config.json:/app/config.json:ro \
  ghcr.io/ambiguous-interactive/signal-fish-server:latest --validate-config

Then confirm the resolved posture (secrets are redacted):

Bash
cargo run -- --print-config

Application identification

  • App-ID allowlist enabled — security.enforce_app_id_allowlist=true.
  • At least one real app registered in security.allowed_apps with a meaningful app_id / app_name.
  • Browser origins locked down — security.cors_origins set to your HTTP and WebSocket origin(s), not *.
  • security.max_connections_per_ip set to a sane ceiling (default 64).
  • security.max_connections set to a sane server-wide ceiling (default 10000) — per-IP caps alone are multiplied by distinct source IP count.

Secrets protected

  • Allowed app IDs are treated as public accounting labels, not credentials or hostile-client tenant isolation (see Application identification).
  • turn.static_auth_secret (if TURN is enabled) is a strong random value, supplied via SIGNAL_FISH__TURN__STATIC_AUTH_SECRET, not committed to the repo.
  • security.metrics_auth_token (if metrics auth is enabled) is a strong random value (openssl rand -hex 32).
  • --print-config shows every set secret as <redacted> and no plaintext secret leaks into logs or CI output.

TLS or reverse proxy

  • Signaling is served over wss:// — mandatory for WebRTC, because DTLS fingerprints travel through the signaling channel (see signaling must run over wss://).
  • Either built-in TLS is on (security.transport.tls.enabled=true with certificate_path and private_key_path) or TLS terminates at a reverse proxy (see reverse proxy setup).
  • If using mTLS: security.transport.tls.client_auth="require" with a valid client_ca_cert_path.

Metrics auth

  • /metrics, /v1/metrics, /metrics/prom, and /metrics/sessions are not openly reachable — either security.require_metrics_auth=true with a security.metrics_auth_token, or the endpoints are restricted at the reverse proxy (IP allowlist).
  • The metrics token is at least 32 characters (the server warns below 16).
  • Prometheus is configured with the bearer token if auth is on (see the metrics-auth recipe).

Rate limits sane

  • rate_limit.* values fit your traffic — max_room_creations, max_join_attempts, max_signals, max_signal_errors, max_relay_bytes (the per-sender game-data relay byte budget; size it against your games' real submit rates), max_room_relay_bytes (the per-room aggregate relay ceiling; keep it above the joint submit volume your rooms legitimately produce), and time_window (see when to adjust).
  • Per-app overrides (allowed_apps[*].rate_limit_per_minute, max_rooms, max_players_per_room, max_relay_bytes) set where an app needs different limits.
  • security.max_signal_bytes, security.max_connection_info_bytes, inbound security.max_message_size, and security.max_outbound_message_size left at sane nonzero caps (max_signal_bytes and max_connection_info_bytes must be ≤ max_message_size; outbound must keep a 256-byte relay envelope headroom above inbound — max_message_size + 256 ≤ max_outbound_message_size, startup rejects tighter pairings because relayed frames grow by the sender id and delivery stamps; outbound must be no more than 67108864 bytes; the metadata cap times protocol.max_players_limit must not exceed outbound, so the roster's entry payloads alone fit one broadcast — keep additional headroom for per-member envelope fields, and the write-time outbound check stays the backstop).

TURN secret shared and rotated

  • If TURN is enabled (turn.enabled=true): turn.urls lists at least one reachable turn:/turns: URL and turn.static_auth_secret is set.
  • The same static_auth_secret is configured on coturn and the signaling server (the compose turn profile reads TURN_STATIC_AUTH_SECRET).
  • turn.credential_ttl_secs comfortably exceeds your longest expected session (default 3600).
  • A rotation procedure is in place (see rotating the shared secret).
  • coturn's relay port range is published/reachable (see the TURN quick start).

Reconnection token exposure bounded

The reconnection token is a bearer seat credential: possession authorizes seat takeover within the window (see Reconnection security notes).

  • server.reconnection_window is the minimum your game tolerates rather than the 300 s default (a bearer secret should not outlive the need).
  • For hosted or multi-tenant deployments: the mTLS token-binding second factor is on (security.transport.token_binding.enabled=true with require_client_fingerprint=true), so the reconnection identity is the client certificate fingerprint rather than the token alone.
  • Clients are built/audited to redact tokens from logs, screenshares, and persisted game state; you have communicated the "treat it like a password" contract to every SDK consumer.

Log rotation

  • File logging configured for production — logging.enable_file_logging=true, logging.dir, logging.filename.
  • logging.rotation set (daily, hourly, or never) to bound disk usage.
  • logging.format chosen (json for log pipelines, text for humans) and a level set via logging.level or RUST_LOG.

Room cleanup intervals

  • server.room_cleanup_interval is > 0 (default 60) so the cleanup task actually runs.
  • server.empty_room_timeout and server.inactive_room_timeout tuned to your session lengths so stale rooms are reclaimed.
  • If reconnection is enabled (server.enable_reconnection=true): server.reconnection_window and server.event_buffer_size match how long and how much you want to buffer for replay.

Deployment topology

  • One active Signal Fish process serves each routing domain; no generic round-robin or cookie-sticky load balancer fronts interchangeable processes.
  • If operating several isolated deployments, an application-owned directory assigns the room home before the WebSocket upgrade and routes every initial connection and reconnect for that room to the same process.
  • server.room_code_prefix and server.region_id, if set, are treated as routing/observability metadata rather than shared-state coordination; the prefix is ASCII alphanumeric and shorter than protocol.room_code_length.
  • The load balancer stops sending new connections before SIGTERM, and its timeout allows at least server.drain_grace_secs for the close boundary.
  • Review the single-instance deployment contract and scaling architecture notes.

Final smoke test

  • --validate-config passes.
  • Health check responds: curl https://your-host/v2/health returns 200.
  • A real client can authenticate, create/join a room, and (for v3) negotiate its SessionPlan.

See also